Skip to content

Commit

Permalink
Support unit pattern in duration serializer. ref #189
Browse files Browse the repository at this point in the history
  • Loading branch information
obarcelonap committed Oct 27, 2020
1 parent f2f7686 commit 77cb103
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -52,6 +55,16 @@ public class DurationSerializer extends JSR310FormattedSerializerBase<Duration>

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);
}
Expand All @@ -66,45 +79,80 @@ protected DurationSerializer(DurationSerializer base,
super(base, useTimestamp, useNanoseconds, dtf, null);
}

protected 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
{
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}
}

0 comments on commit 77cb103

Please sign in to comment.