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 12, 2020
1 parent 95780b6 commit b63edea
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 4 deletions.
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,11 @@ public class DurationDeserializer extends JSR310DeserializerBase<Duration>

public static final DurationDeserializer INSTANCE = new DurationDeserializer();

/**
* Since 2.12
*/
private DurationPattern pattern;

private DurationDeserializer()
{
super(Duration.class);
Expand Down Expand Up @@ -79,20 +88,38 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
deser = deser.withLeniency(leniency);
}
}
if (format.hasPattern()) {
this.pattern = DurationPattern.from(format.getPattern())
.orElse(null);
} else {
this.pattern = null;
}
}
return deser;
}

private DurationDeserializer withPattern(DurationPattern pattern) {
this.pattern = pattern;
return this;
}

@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 (pattern != null) {
return pattern.parse(decValue.longValue());
}
return DecimalUtils.extractSecondsAndNanos(decValue, Duration::ofSeconds);
case JsonTokenId.ID_NUMBER_INT:
return _fromTimestamp(context, parser.getLongValue());
long intValue = parser.getLongValue();
if (pattern != null) {
return pattern.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 +130,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,13 +148,18 @@ 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)
&& _isValidTimestampString(value)) {
return _fromTimestamp(ctxt, NumberInput.parseLong(value));
}

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

try {
return Duration.parse(value);
} catch (DateTimeException e) {
Expand All @@ -141,4 +173,23 @@ protected Duration _fromTimestamp(DeserializationContext ctxt, long ts) {
}
return Duration.ofMillis(ts);
}

protected static class DurationPattern {
final TemporalUnit unit;

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

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

static Optional<DurationPattern> from(String pattern) {
return Stream.of(ChronoUnit.values())
.filter(u -> u.toString().equalsIgnoreCase(pattern))
.map(DurationPattern::new)
.findFirst();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

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

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

Expand All @@ -29,6 +31,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 +431,35 @@ public void testStrictDeserializeFromEmptyString() throws Exception {
String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
objectReader.readValue(valueFromEmptyStr);
}

@Test
public void shouldDeserializeInHours_whenValueIsString() 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_whenValueIsInteger() 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_whenValueIsFloat() 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.DurationPattern;
import org.junit.Test;

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

public class DurationPatternEmptyTest {

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

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

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

@Test
public void shouldReturnEmpty_whenDoesNotMatchAnyTemporalUnit() {
assertEquals(empty(), DurationPattern.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.DurationPattern;
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 DurationPatternTest {

private final String stringPattern;
private final TemporalUnit temporalUnit;

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

@Test
public void shouldMapToTemporalUnit() {
Optional<DurationPattern> durationPattern = DurationPattern.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 b63edea

Please sign in to comment.