Skip to content

Commit

Permalink
Support pattern for duration serializer (#194)
Browse files Browse the repository at this point in the history
* Extract duration unit converter to util package. ref #189
* Support unit pattern in duration serializer. ref #189
  • Loading branch information
obarcelonap authored Oct 29, 2020
1 parent d04e695 commit e404c02
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
import com.fasterxml.jackson.datatype.jsr310.util.DurationUnitConverter;

/**
* Deserializer for Java 8 temporal {@link Duration}s.
Expand Down Expand Up @@ -90,8 +91,8 @@ protected DurationDeserializer withLeniency(Boolean leniency) {
return new DurationDeserializer(this, leniency);
}

protected DurationDeserializer withConverter(DurationUnitConverter pattern) {
return new DurationDeserializer(this, pattern);
protected DurationDeserializer withConverter(DurationUnitConverter converter) {
return new DurationDeserializer(this, converter);
}

@Override
Expand All @@ -113,8 +114,8 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
if (p == null) {
ctxt.reportBadDefinition(getValueType(ctxt),
String.format(
"Bad 'pattern' definition (\"%s\") for `Duration`: expected one of [%s]",
pattern, DurationUnitConverter.descForAllowed()));
"Bad 'pattern' definition (\"%s\") for `Duration`: expected one of [%s]",
pattern, DurationUnitConverter.descForAllowed()));
}
deser = deser.withConverter(p);
}
Expand Down Expand Up @@ -185,49 +186,4 @@ protected Duration _fromTimestamp(DeserializationContext ctxt, long ts) {
}
return Duration.ofMillis(ts);
}

protected static class DurationUnitConverter {
private final static Map<String, ChronoUnit> PARSEABLE_UNITS;
static {
Map<String, ChronoUnit> units = new LinkedHashMap<>();
for (ChronoUnit unit : new ChronoUnit[] {
ChronoUnit.NANOS,
ChronoUnit.MICROS,
ChronoUnit.MILLIS,
ChronoUnit.SECONDS,
ChronoUnit.MINUTES,
ChronoUnit.HOURS,
ChronoUnit.HALF_DAYS,
ChronoUnit.DAYS
}) {
units.put(unit.name(), unit);
}
PARSEABLE_UNITS = units;
}

final TemporalUnit unit;

DurationUnitConverter(TemporalUnit unit) {
this.unit = unit;
}

public Duration convert(long value) {
return Duration.of(value, unit);
}

/**
* @return Description of all allowed valued as a sequence of
* double-quoted values separated by comma
*/
public static String descForAllowed() {
return "\"" + PARSEABLE_UNITS.keySet().stream()
.collect(Collectors.joining("\", \""))
+"\"";
}

static DurationUnitConverter from(String unit) {
ChronoUnit chr = PARSEABLE_UNITS.get(unit);
return (chr == null) ? null : new DurationUnitConverter(chr);
}
}
}
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
@@ -0,0 +1,82 @@
package com.fasterxml.jackson.datatype.jsr310.util;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.fasterxml.jackson.datatype.jsr310.util.DurationUnitConverter.DurationSerialization.deserializer;

/**
* Handles the conversion of the duration based on the API of {@link Duration} for a restricted set of {@link ChronoUnit}.
* Only the units considered as accurate are supported in this converter since are the only ones capable of handling
* deserialization in a precise manner (see {@link ChronoUnit#isDurationEstimated}).
*
* @since 2.12
*/
public class DurationUnitConverter {

protected static class DurationSerialization {
final Function<Duration, Long> serializer;
final Function<Long, Duration> deserializer;

DurationSerialization(
Function<Duration, Long> serializer,
Function<Long, Duration> deserializer) {
this.serializer = serializer;
this.deserializer = deserializer;
}

static Function<Long, Duration> deserializer(TemporalUnit unit) {
return v -> Duration.of(v, unit);
}
}

private final static Map<String, DurationSerialization> UNITS;

static {
Map<String, DurationSerialization> units = new LinkedHashMap<>();
units.put(ChronoUnit.NANOS.name(), new DurationSerialization(Duration::toNanos, deserializer(ChronoUnit.NANOS)));
units.put(ChronoUnit.MICROS.name(), new DurationSerialization(d -> d.toNanos() / 1000, deserializer(ChronoUnit.MICROS)));
units.put(ChronoUnit.MILLIS.name(), new DurationSerialization(Duration::toMillis, deserializer(ChronoUnit.MILLIS)));
units.put(ChronoUnit.SECONDS.name(), new DurationSerialization(Duration::getSeconds, deserializer(ChronoUnit.SECONDS)));
units.put(ChronoUnit.MINUTES.name(), new DurationSerialization(Duration::toMinutes, deserializer(ChronoUnit.MINUTES)));
units.put(ChronoUnit.HOURS.name(), new DurationSerialization(Duration::toHours, deserializer(ChronoUnit.HOURS)));
units.put(ChronoUnit.HALF_DAYS.name(), new DurationSerialization(d -> d.toHours() / 12, deserializer(ChronoUnit.HALF_DAYS)));
units.put(ChronoUnit.DAYS.name(), new DurationSerialization(Duration::toDays, deserializer(ChronoUnit.DAYS)));
UNITS = units;
}


final DurationSerialization serialization;

DurationUnitConverter(DurationSerialization serialization) {
this.serialization = serialization;
}

public Duration convert(long value) {
return serialization.deserializer.apply(value);
}

public long convert(Duration duration) {
return serialization.serializer.apply(duration);
}

/**
* @return Description of all allowed valued as a sequence of
* double-quoted values separated by comma
*/
public static String descForAllowed() {
return "\"" + UNITS.keySet().stream()
.collect(Collectors.joining("\", \""))
+ "\"";
}

public static DurationUnitConverter from(String unit) {
DurationSerialization def = UNITS.get(unit);
return (def == null) ? null : new DurationUnitConverter(def);
}
}
Loading

0 comments on commit e404c02

Please sign in to comment.