From c8f9733ebcaa09c81d3f0b2a76b45caae129fa0e Mon Sep 17 00:00:00 2001 From: Oriol Barcelona Date: Wed, 21 Oct 2020 18:01:34 +0200 Subject: [PATCH] Support unit pattern in duration serializer. ref #189 --- .../jsr310/ser/DurationSerializer.java | 83 ++++++-- .../ser/JSR310FormattedSerializerBase.java | 33 ++-- .../datatype/jsr310/ser/DurationSerTest.java | 182 ++++++++++++++++++ 3 files changed, 270 insertions(+), 28 deletions(-) diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerializer.java index c7a08df1..2ebac2c5 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerializer.java @@ -21,14 +21,17 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; import com.fasterxml.jackson.datatype.jsr310.DecimalUtils; +import com.fasterxml.jackson.datatype.jsr310.util.DurationUnitConverter; import java.io.IOException; import java.math.BigDecimal; @@ -52,6 +55,16 @@ public class DurationSerializer extends JSR310FormattedSerializerBase public static final DurationSerializer INSTANCE = new DurationSerializer(); + /** + * When defined (not {@code null}) duration values will be converted into integers + * with the unit configured for the converter. + * Only available when {@link SerializationFeature#WRITE_DURATIONS_AS_TIMESTAMPS} is enabled + * and {@link SerializationFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} is not enabled + * since the duration converters do not support fractions + * @since 2.12 + */ + private DurationUnitConverter _durationUnitConverter; + private DurationSerializer() { super(Duration.class); } @@ -66,45 +79,80 @@ protected DurationSerializer(DurationSerializer base, super(base, useTimestamp, useNanoseconds, dtf, null); } + public DurationSerializer(DurationSerializer base, DurationUnitConverter converter) { + super(base, base._useTimestamp, base._useNanoseconds, base._formatter, base._shape); + _durationUnitConverter = converter; + } + @Override protected DurationSerializer withFormat(Boolean useTimestamp, DateTimeFormatter dtf, JsonFormat.Shape shape) { return new DurationSerializer(this, useTimestamp, dtf); } + protected DurationSerializer withConverter(DurationUnitConverter converter) { + return new DurationSerializer(this, converter); + } + // @since 2.10 @Override protected SerializationFeature getTimestampsFeature() { return SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS; } + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { + DurationSerializer ser = (DurationSerializer) super.createContextual(prov, property); + JsonFormat.Value format = findFormatOverrides(prov, property, handledType()); + if (format != null && format.hasPattern()) { + final String pattern = format.getPattern(); + DurationUnitConverter p = DurationUnitConverter.from(pattern); + if (p == null) { + prov.reportBadDefinition(handledType(), + String.format( + "Bad 'pattern' definition (\"%s\") for `Duration`: expected one of [%s]", + pattern, DurationUnitConverter.descForAllowed())); + } + + ser = ser.withConverter(p); + } + return ser; + } + @Override public void serialize(Duration duration, JsonGenerator generator, SerializerProvider provider) throws IOException { if (useTimestamp(provider)) { if (useNanoseconds(provider)) { - // 20-Oct-2020, tatu: [modules-java8#165] Need to take care of - // negative values too, and without work-around values - // returned are wonky wrt conversions - BigDecimal bd; - if (duration.isNegative()) { - duration = duration.abs(); - bd = DecimalUtils.toBigDecimal(duration.getSeconds(), - duration.getNano()) - .negate(); + generator.writeNumber(_toNanos(duration)); + } else { + if (_durationUnitConverter != null) { + generator.writeNumber(_durationUnitConverter.convert(duration)); } else { - bd = DecimalUtils.toBigDecimal(duration.getSeconds(), - duration.getNano()); + generator.writeNumber(duration.toMillis()); } - generator.writeNumber(bd); - } else { - generator.writeNumber(duration.toMillis()); } } else { - // Does not look like we can make any use of DateTimeFormatter here? generator.writeString(duration.toString()); } } + // 20-Oct-2020, tatu: [modules-java8#165] Need to take care of + // negative values too, and without work-around values + // returned are wonky wrt conversions + private BigDecimal _toNanos(Duration duration) { + BigDecimal bd; + if (duration.isNegative()) { + duration = duration.abs(); + bd = DecimalUtils.toBigDecimal(duration.getSeconds(), + duration.getNano()) + .negate(); + } else { + bd = DecimalUtils.toBigDecimal(duration.getSeconds(), + duration.getNano()); + } + return bd; + } + @Override protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { @@ -135,4 +183,9 @@ protected JsonToken serializationShape(SerializerProvider provider) { protected JSR310FormattedSerializerBase withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) { return new DurationSerializer(this, _useTimestamp, writeNanoseconds, _formatter); } + + @Override + protected DateTimeFormatter _useDateTimeFormatter(SerializerProvider prov, JsonFormat.Value format) { + return null; + } } diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/JSR310FormattedSerializerBase.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/JSR310FormattedSerializerBase.java index 631c7547..bd2f49f8 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/JSR310FormattedSerializerBase.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/JSR310FormattedSerializerBase.java @@ -144,18 +144,7 @@ public JsonSerializer createContextual(SerializerProvider prov, // If not, do we have a pattern? if (format.hasPattern()) { - final String pattern = format.getPattern(); - final Locale locale = format.hasLocale() ? format.getLocale() : prov.getLocale(); - if (locale == null) { - dtf = DateTimeFormatter.ofPattern(pattern); - } else { - dtf = DateTimeFormatter.ofPattern(pattern, locale); - } - //Issue #69: For instant serializers/deserializers we need to configure the formatter with - //a time zone picked up from JsonFormat annotation, otherwise serialization might not work - if (format.hasTimeZone()) { - dtf = dtf.withZone(format.getTimeZone().toZoneId()); - } + dtf = _useDateTimeFormatter(prov, format); } JSR310FormattedSerializerBase ser = this; if ((shape != _shape) || (useTimestamp != _useTimestamp) || (dtf != _formatter)) { @@ -211,7 +200,7 @@ protected JavaType _integerListType(SerializerProvider prov) { } return t; } - + /** * Overridable method that determines {@link SerializationFeature} that is used as * the global default in determining if date/time value serialized should use numeric @@ -265,4 +254,22 @@ protected boolean useNanoseconds(SerializerProvider provider) { return (provider != null) && provider.isEnabled(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); } + + // modules-java8#189: to be overridden by other formatters using this as base class + protected DateTimeFormatter _useDateTimeFormatter(SerializerProvider prov, JsonFormat.Value format) { + DateTimeFormatter dtf; + final String pattern = format.getPattern(); + final Locale locale = format.hasLocale() ? format.getLocale() : prov.getLocale(); + if (locale == null) { + dtf = DateTimeFormatter.ofPattern(pattern); + } else { + dtf = DateTimeFormatter.ofPattern(pattern, locale); + } + //Issue #69: For instant serializers/deserializers we need to configure the formatter with + //a time zone picked up from JsonFormat annotation, otherwise serialization might not work + if (format.hasTimeZone()) { + dtf = dtf.withZone(format.getTimeZone().toZoneId()); + } + return dtf; + } } diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerTest.java index 52c5bc28..a418f70c 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerTest.java @@ -1,5 +1,6 @@ package com.fasterxml.jackson.datatype.jsr310.ser; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; @@ -161,4 +162,185 @@ public void testSerializationWithTypeInfo03() throws Exception assertEquals("The value is not correct.", "[\"" + Duration.class.getName() + "\",\"" + duration.toString() + "\"]", value); } + + /* + /********************************************************** + /* Tests for custom patterns (modules-java8#189) + /********************************************************** + */ + + @Test + public void shouldSerializeInNanos_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("NANOS"); + + Duration duration = Duration.ofHours(1); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "3600000000000", value); + } + + @Test + public void shouldSerializeInMicros_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MICROS"); + + Duration duration = Duration.ofMillis(1); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1000", value); + } + + @Test + public void shouldSerializeInMicrosDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MICROS"); + + Duration duration = Duration.ofNanos(1500); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1", value); + } + + @Test + public void shouldSerializeInMillis_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MILLIS"); + + Duration duration = Duration.ofSeconds(1); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1000", value); + } + + @Test + public void shouldSerializeInMillisDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MILLIS"); + + Duration duration = Duration.ofNanos(1500000); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1", value); + } + + @Test + public void shouldSerializeInSeconds_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("SECONDS"); + + Duration duration = Duration.ofMinutes(1); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "60", value); + } + + @Test + public void shouldSerializeInSecondsDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("SECONDS"); + + Duration duration = Duration.ofMillis(1500); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1", value); + } + + @Test + public void shouldSerializeInMinutes_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MINUTES"); + + Duration duration = Duration.ofHours(1); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "60", value); + } + + @Test + public void shouldSerializeInMinutesDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("MINUTES"); + + Duration duration = Duration.ofSeconds(90); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1", value); + } + + @Test + public void shouldSerializeInHours_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("HOURS"); + + Duration duration = Duration.ofDays(1); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "24", value); + } + + @Test + public void shouldSerializeInHoursDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("HOURS"); + + Duration duration = Duration.ofMinutes(90); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1", value); + } + + @Test + public void shouldSerializeInHalfDays_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("HALF_DAYS"); + + Duration duration = Duration.ofDays(1); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "2", value); + } + + @Test + public void shouldSerializeInHalfDaysDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("DAYS"); + + Duration duration = Duration.ofHours(30); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1", value); + } + + @Test + public void shouldSerializeInDays_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("DAYS"); + + Duration duration = Duration.ofDays(1); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1", value); + } + + @Test + public void shouldSerializeInDaysDiscardingFractions_whenSetAsPattern() throws Exception + { + ObjectMapper mapper = _mapperForPatternOverride("DAYS"); + + Duration duration = Duration.ofHours(36); + String value = mapper.writeValueAsString(duration); + + assertEquals("The value is not correct.", "1", value); + } + + protected ObjectMapper _mapperForPatternOverride(String pattern) { + ObjectMapper mapper = mapperBuilder() + .enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .build(); + mapper.configOverride(Duration.class) + .setFormat(JsonFormat.Value.forPattern(pattern)); + return mapper; + } }