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 FasterXML#184
  • Loading branch information
obarcelonap committed Oct 14, 2020
1 parent 0b6a711 commit 7ecd6ce
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 6 deletions.
11 changes: 11 additions & 0 deletions datetime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ 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 [`ChronoUnit`](https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html) as `pattern`. 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,10 @@
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.Optional;
import java.util.stream.Stream;


/**
Expand All @@ -49,6 +53,15 @@ public class DurationDeserializer extends JSR310DeserializerBase<Duration>

public static final DurationDeserializer INSTANCE = new DurationDeserializer();

/**
* Since 2.12
* When set, 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 +74,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,20 +97,36 @@ 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
{
switch (parser.currentTokenId())
{
case JsonTokenId.ID_NUMBER_FLOAT:
BigDecimal value = parser.getDecimalValue();
return DecimalUtils.extractSecondsAndNanos(value, Duration::ofSeconds);
BigDecimal decValue = parser.getDecimalValue();
if (_durationUnitParser != null) {
return _durationUnitParser.parse(decValue.longValue());
}
return DecimalUtils.extractSecondsAndNanos(decValue, 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,7 +137,7 @@ 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);
}
Expand All @@ -121,6 +155,7 @@ protected Duration _fromString(JsonParser parser, DeserializationContext ctxt,
}
return null;
}

// 30-Sep-2020: Should allow use of "Timestamp as String" for
// some textual formats
if (ctxt.isEnabled(StreamReadCapability.UNTYPED_SCALARS)
Expand All @@ -129,9 +164,13 @@ && _isValidTimestampString(value)) {
}

try {
if (_durationUnitParser != null) {
return _durationUnitParser.parse(NumberInput.parseLong(value));
}

return Duration.parse(value);
} catch (DateTimeException e) {
return _handleDateTimeException(ctxt, e, value);
} catch (NumberFormatException | DateTimeException e) {
return _handleWeirdStringValue(ctxt, e, value);
}
}

Expand All @@ -141,4 +180,23 @@ protected Duration _fromTimestamp(DeserializationContext ctxt, long ts) {
}
return Duration.ofMillis(ts);
}

protected static class DurationUnitParser {
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 Stream.of(ChronoUnit.values())
.filter(u -> u.toString().equalsIgnoreCase(unit))
.map(DurationUnitParser::new)
.findFirst();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ protected <BOGUS> BOGUS _reportWrongToken(JsonParser parser, DeserializationCont
@SuppressWarnings("unchecked")
protected <R> R _handleDateTimeException(DeserializationContext context,
DateTimeException e0, String value) throws JsonMappingException
{
return _handleWeirdStringValue(context, e0, value);
}

@SuppressWarnings("unchecked")
protected <R, E extends RuntimeException> R _handleWeirdStringValue(DeserializationContext context,
E e0, String value) throws JsonMappingException
{
try {
return (R) context.handleWeirdStringValue(handledType(), value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
Expand All @@ -29,6 +30,15 @@ public class DurationDeserTest extends ModuleTestBase

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

final static class Wrapper {
@JsonFormat(pattern="HOURS")
public Duration value;

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


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

@Test
public void shouldDeserializeInHours_whenUnitAsPattern_andValueIsString() throws Exception {
ObjectMapper mapper = newMapper();
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

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

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

@Test(expected = InvalidFormatException.class)
public void shouldHandleException_whenUsingUnitAsPattern_andValueIsString() throws Exception {
ObjectMapper mapper = newMapper();
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

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

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

@Test
public void shouldDeserializeInHours_whenUnitAsPattern_andValueIsInteger() throws Exception {
ObjectMapper mapper = newMapper();
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

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

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

@Test
public void shouldDeserializeInHours_whenUnitAsPattern_andValueIsFloat() throws Exception {
ObjectMapper mapper = newMapper();
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);

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

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.fasterxml.jackson.datatype.jsr310.deser;

import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer.DurationUnitParser;
import org.junit.Test;

import static java.util.Optional.empty;
import static org.junit.Assert.assertEquals;

public class DurationUnitParserEmptyTest {

@Test
public void shouldReturnEmpty_whenNull() {
assertEquals(empty(), DurationUnitParser.from(null));
}

@Test
public void shouldReturnEmpty_whenEmptyString() {
assertEquals(empty(), DurationUnitParser.from(""));
}

@Test
public void shouldReturnEmpty_whenSpaces() {
assertEquals(empty(), DurationUnitParser.from(" "));
}

@Test
public void shouldReturnEmpty_whenDoesNotMatchAnyTemporalUnit() {
assertEquals(empty(), DurationUnitParser.from("DOESNOTMATCH"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.fasterxml.jackson.datatype.jsr310.deser;

import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer.DurationUnitParser;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static java.util.Arrays.asList;
import static java.util.Optional.of;
import static org.junit.Assert.assertEquals;

@RunWith(Parameterized.class)
public class DurationUnitParserTest {

private final String stringPattern;
private final TemporalUnit temporalUnit;

public DurationUnitParserTest(String stringPattern, TemporalUnit temporalUnit) {
this.stringPattern = stringPattern;
this.temporalUnit = temporalUnit;
}

@Test
public void shouldMapToTemporalUnit() {
Optional<DurationUnitParser> durationPattern = DurationUnitParser.from(stringPattern);

assertEquals(of(temporalUnit), durationPattern.map(dp -> dp.unit));
}

@Parameters
public static Collection<Object[]> testCases() {
List<Object[]> baseTestCases = asList(
asArray("Nanos", ChronoUnit.NANOS),
asArray("Micros", ChronoUnit.MICROS),
asArray("Millis", ChronoUnit.MILLIS),
asArray("Seconds", ChronoUnit.SECONDS),
asArray("Minutes", ChronoUnit.MINUTES),
asArray("Hours", ChronoUnit.HOURS),
asArray("HalfDays", ChronoUnit.HALF_DAYS),
asArray("Days", ChronoUnit.DAYS),
asArray("Weeks", ChronoUnit.WEEKS),
asArray("Months", ChronoUnit.MONTHS),
asArray("Years", ChronoUnit.YEARS),
asArray("Decades", ChronoUnit.DECADES),
asArray("Centuries", ChronoUnit.CENTURIES),
asArray("Millennia", ChronoUnit.MILLENNIA),
asArray("Eras", ChronoUnit.ERAS),
asArray("Forever", ChronoUnit.FOREVER)
);

List<Object[]> lowerCaseTestCases = baseTestCases.stream()
.map(testCase -> asArray(testCase[0].toString().toLowerCase(), testCase[1]))
.collect(Collectors.toList());

List<Object[]> upperCaseTestCases = baseTestCases.stream()
.map(testCase -> asArray(testCase[0].toString().toUpperCase(), testCase[1]))
.collect(Collectors.toList());

List<Object[]> testCases = new ArrayList<>(baseTestCases);
testCases.addAll(lowerCaseTestCases);
testCases.addAll(upperCaseTestCases);
return testCases;
}

private static Object[] asArray(Object... values) {
return values;
}
}

0 comments on commit 7ecd6ce

Please sign in to comment.