Skip to content

Commit

Permalink
Support for jsonformat in duration deserializer based on Duration::of…
Browse files Browse the repository at this point in the history
…(long,TemporalUnit). ref #184 (#187)
  • Loading branch information
obarcelonap committed Oct 20, 2020
1 parent 9b363bf commit 1754c4b
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 17 deletions.
14 changes: 14 additions & 0 deletions datetime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ times but are supported with this module nonetheless.
[`LocalDateTime`](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html), and
[`OffsetTime`](https://docs.oracle.com/javase/8/docs/api/java/time/OffsetTime.html), which cannot portably be converted to
timestamps and are instead represented as arrays when `WRITE_DATES_AS_TIMESTAMPS` is enabled.
* [`Duration`](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html), which unit can be configured in `JsonFormat` using a subset of [`ChronoUnit`](https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html) as `pattern`.
As the underlying implementation is based on `Duration::of` supported units are: `NANOS`, `MICROS`, `MILLIS`, `SECONDS`, `MINUTES`, `HOURS`, `HALF_DAYS` and `DAYS`.
For instance:

```java
@JsonFormat(pattern="MILLIS")
long millis;

@JsonFormat(pattern="SECONDS")
long seconds;

@JsonFormat(pattern="DAYS")
long days;
```

## Usage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
import java.math.BigDecimal;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;


/**
Expand All @@ -49,8 +56,17 @@ public class DurationDeserializer extends JSR310DeserializerBase<Duration>

public static final DurationDeserializer INSTANCE = new DurationDeserializer();

private DurationDeserializer()
{
/**
* Since 2.12
* When set, integer values will be deserialized using the specified unit. Using this parser will tipically
* override the value specified in {@link DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS} as it is
* considered that the unit set in {@link JsonFormat#pattern()} has precedence since is more specific.
*
* @see [jackson-modules-java8#184] for more info
*/
private DurationUnitParser _durationUnitParser;

private DurationDeserializer() {
super(Duration.class);
}

Expand All @@ -61,6 +77,11 @@ protected DurationDeserializer(DurationDeserializer base, Boolean leniency) {
super(base, leniency);
}

protected DurationDeserializer(DurationDeserializer base, DurationUnitParser durationUnitParser) {
super(base, base._isLenient);
_durationUnitParser = durationUnitParser;
}

@Override
protected DurationDeserializer withLeniency(Boolean leniency) {
return new DurationDeserializer(this, leniency);
Expand All @@ -79,10 +100,19 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
deser = deser.withLeniency(leniency);
}
}
if (format.hasPattern()) {
deser = DurationUnitParser.from(format.getPattern())
.map(deser::withPattern)
.orElse(deser);
}
}
return deser;
}

private DurationDeserializer withPattern(DurationUnitParser pattern) {
return new DurationDeserializer(this, pattern);
}

@Override
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
Expand All @@ -92,7 +122,11 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
BigDecimal value = parser.getDecimalValue();
return DecimalUtils.extractSecondsAndNanos(value, Duration::ofSeconds);
case JsonTokenId.ID_NUMBER_INT:
return _fromTimestamp(context, parser.getLongValue());
long intValue = parser.getLongValue();
if (_durationUnitParser != null) {
return _durationUnitParser.parse(intValue);
}
return _fromTimestamp(context, intValue);
case JsonTokenId.ID_STRING:
return _fromString(parser, context, parser.getText());
// 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
Expand All @@ -103,9 +137,9 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
// 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
// values quite easily
return (Duration) parser.getEmbeddedObject();

case JsonTokenId.ID_START_ARRAY:
return _deserializeFromArray(parser, context);
return _deserializeFromArray(parser, context);
}
return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING,
JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT);
Expand Down Expand Up @@ -141,4 +175,33 @@ protected Duration _fromTimestamp(DeserializationContext ctxt, long ts) {
}
return Duration.ofMillis(ts);
}

protected static class DurationUnitParser {
final static Set<ChronoUnit> PARSEABLE_UNITS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
ChronoUnit.NANOS,
ChronoUnit.MICROS,
ChronoUnit.MILLIS,
ChronoUnit.SECONDS,
ChronoUnit.MINUTES,
ChronoUnit.HOURS,
ChronoUnit.HALF_DAYS,
ChronoUnit.DAYS
)));
final TemporalUnit unit;

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

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

static Optional<DurationUnitParser> from(String unit) {
return PARSEABLE_UNITS.stream()
.filter(u -> u.name().equals(unit))
.map(DurationUnitParser::new)
.findFirst();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
package com.fasterxml.jackson.datatype.jsr310.deser;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
import org.junit.Test;

import java.math.BigInteger;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAmount;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;

public class DurationDeserTest extends ModuleTestBase
{
private final ObjectReader READER = newMapper().readerFor(Duration.class);

private final TypeReference<Map<String, Duration>> MAP_TYPE_REF = new TypeReference<Map<String, Duration>>() { };

final static class Wrapper {
public Duration value;

public Wrapper() { }
public Wrapper(Duration v) { value = v; }
}


@Test
public void testDeserializationAsFloat01() throws Exception
{
Expand Down Expand Up @@ -420,4 +428,140 @@ public void testStrictDeserializeFromEmptyString() throws Exception {
String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
objectReader.readValue(valueFromEmptyStr);
}

@Test
public void shouldDeserializeInNanos_whenNanosUnitAsPattern_andValueIsInteger() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("NANOS"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);

assertEquals(Duration.ofNanos(25), wrapper.value);
}

@Test
public void shouldDeserializeInMicros_whenMicrosUnitAsPattern_andValueIsInteger() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("MICROS"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);

assertEquals(Duration.of(25, ChronoUnit.MICROS), wrapper.value);
}

@Test
public void shouldDeserializeInMillis_whenMillisUnitAsPattern_andValueIsInteger() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("MILLIS"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);

assertEquals(Duration.ofMillis(25), wrapper.value);
}

@Test
public void shouldDeserializeInSeconds_whenSecondsUnitAsPattern_andValueIsInteger() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("SECONDS"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);

assertEquals(Duration.ofSeconds(25), wrapper.value);
}

@Test
public void shouldDeserializeInMinutes_whenMinutesUnitAsPattern_andValueIsInteger() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("MINUTES"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);

assertEquals(Duration.ofMinutes(25), wrapper.value);
}

@Test
public void shouldDeserializeInHours_whenHoursUnitAsPattern_andValueIsInteger() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("HOURS"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);

assertEquals(Duration.ofHours(25), wrapper.value);
}

@Test
public void shouldDeserializeInHalfDays_whenHalfDaysUnitAsPattern_andValueIsInteger() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("HALF_DAYS"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);

assertEquals(Duration.of(25, ChronoUnit.HALF_DAYS), wrapper.value);
}

@Test
public void shouldDeserializeInDays_whenDaysUnitAsPattern_andValueIsInteger() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("DAYS"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);

assertEquals(Duration.ofDays(25), wrapper.value);
}

@Test
public void shouldIgnoreUnitPattern_whenValueIsFloat() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("MINUTES"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25.5), Wrapper.class);

assertEquals(Duration.parse("PT25.5S"), wrapper.value);
}

@Test
public void shouldIgnoreUnitPattern_whenValueIsString() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("MINUTES"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue("{\"value\":\"PT25S\"}", Wrapper.class);

assertEquals(Duration.parse("PT25S"), wrapper.value);
}

@Test
public void shouldIgnoreUnitPattern_whenUnitPatternDoesNotMatchExactly() throws Exception {
ObjectMapper mapper = newMapper();
mapper.configOverride(Duration.class)
.setFormat(JsonFormat.Value.forPattern("Nanos"));
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);

assertEquals(Duration.ofSeconds(25), wrapper.value);
}

private String wrapperPayload(Number number) {
return "{\"value\":" + number + "}";
}
}
Loading

0 comments on commit 1754c4b

Please sign in to comment.