Skip to content

Commit 9c5db48

Browse files
committed
Support for jsonformat in duration deserializer based on Duration::of(long,TemporalUnit). ref FasterXML#184
1 parent 0b6a711 commit 9c5db48

File tree

5 files changed

+350
-17
lines changed

5 files changed

+350
-17
lines changed

datetime/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ times but are supported with this module nonetheless.
4747
[`LocalDateTime`](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html), and
4848
[`OffsetTime`](https://docs.oracle.com/javase/8/docs/api/java/time/OffsetTime.html), which cannot portably be converted to
4949
timestamps and are instead represented as arrays when `WRITE_DATES_AS_TIMESTAMPS` is enabled.
50+
* [`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`.
51+
As the underlying implementation is based on `Duration::of` supported units are: `NANOS`, `MICROS`, `MILLIS`, `SECONDS`, `MINUTES`, `HOURS`, `HALF_DAYS` and `DAYS`.
52+
For instance:
53+
54+
```java
55+
@JsonFormat(pattern="MILLIS")
56+
long millis;
57+
58+
@JsonFormat(pattern="SECONDS")
59+
long seconds;
60+
61+
@JsonFormat(pattern="DAYS")
62+
long days;
63+
```
5064

5165
## Usage
5266

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java

+68-5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
import java.math.BigDecimal;
3535
import java.time.DateTimeException;
3636
import java.time.Duration;
37+
import java.time.temporal.ChronoUnit;
38+
import java.time.temporal.TemporalUnit;
39+
import java.util.Arrays;
40+
import java.util.Collections;
41+
import java.util.HashSet;
42+
import java.util.Optional;
43+
import java.util.Set;
3744

3845

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

5057
public static final DurationDeserializer INSTANCE = new DurationDeserializer();
5158

52-
private DurationDeserializer()
53-
{
59+
/**
60+
* Since 2.12
61+
* When set, integer values will be deserialized using the specified unit. Using this parser will tipically
62+
* override the value specified in {@link DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS} as it is
63+
* considered that the unit set in {@link JsonFormat#pattern()} has precedence since is more specific.
64+
*
65+
* @see [jackson-modules-java8#184] for more info
66+
*/
67+
private DurationUnitParser _durationUnitParser;
68+
69+
private DurationDeserializer() {
5470
super(Duration.class);
5571
}
5672

@@ -61,6 +77,11 @@ protected DurationDeserializer(DurationDeserializer base, Boolean leniency) {
6177
super(base, leniency);
6278
}
6379

80+
protected DurationDeserializer(DurationDeserializer base, DurationUnitParser durationUnitParser) {
81+
super(base, base._isLenient);
82+
_durationUnitParser = durationUnitParser;
83+
}
84+
6485
@Override
6586
protected DurationDeserializer withLeniency(Boolean leniency) {
6687
return new DurationDeserializer(this, leniency);
@@ -79,10 +100,19 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
79100
deser = deser.withLeniency(leniency);
80101
}
81102
}
103+
if (format.hasPattern()) {
104+
deser = DurationUnitParser.from(format.getPattern())
105+
.map(deser::withPattern)
106+
.orElse(deser);
107+
}
82108
}
83109
return deser;
84110
}
85111

112+
private DurationDeserializer withPattern(DurationUnitParser pattern) {
113+
return new DurationDeserializer(this, pattern);
114+
}
115+
86116
@Override
87117
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException
88118
{
@@ -92,7 +122,11 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
92122
BigDecimal value = parser.getDecimalValue();
93123
return DecimalUtils.extractSecondsAndNanos(value, Duration::ofSeconds);
94124
case JsonTokenId.ID_NUMBER_INT:
95-
return _fromTimestamp(context, parser.getLongValue());
125+
long intValue = parser.getLongValue();
126+
if (_durationUnitParser != null) {
127+
return _durationUnitParser.parse(intValue);
128+
}
129+
return _fromTimestamp(context, intValue);
96130
case JsonTokenId.ID_STRING:
97131
return _fromString(parser, context, parser.getText());
98132
// 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
@@ -103,9 +137,9 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
103137
// 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
104138
// values quite easily
105139
return (Duration) parser.getEmbeddedObject();
106-
140+
107141
case JsonTokenId.ID_START_ARRAY:
108-
return _deserializeFromArray(parser, context);
142+
return _deserializeFromArray(parser, context);
109143
}
110144
return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING,
111145
JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT);
@@ -141,4 +175,33 @@ protected Duration _fromTimestamp(DeserializationContext ctxt, long ts) {
141175
}
142176
return Duration.ofMillis(ts);
143177
}
178+
179+
protected static class DurationUnitParser {
180+
final static Set<ChronoUnit> PARSEABLE_UNITS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
181+
ChronoUnit.NANOS,
182+
ChronoUnit.MICROS,
183+
ChronoUnit.MILLIS,
184+
ChronoUnit.SECONDS,
185+
ChronoUnit.MINUTES,
186+
ChronoUnit.HOURS,
187+
ChronoUnit.HALF_DAYS,
188+
ChronoUnit.DAYS
189+
)));
190+
final TemporalUnit unit;
191+
192+
DurationUnitParser(TemporalUnit unit) {
193+
this.unit = unit;
194+
}
195+
196+
Duration parse(long value) {
197+
return Duration.of(value, unit);
198+
}
199+
200+
static Optional<DurationUnitParser> from(String unit) {
201+
return PARSEABLE_UNITS.stream()
202+
.filter(u -> u.name().equals(unit))
203+
.map(DurationUnitParser::new)
204+
.findFirst();
205+
}
206+
}
144207
}

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserTest.java

+156-12
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
11
package com.fasterxml.jackson.datatype.jsr310.deser;
22

3+
import com.fasterxml.jackson.annotation.JsonFormat;
4+
import com.fasterxml.jackson.core.type.TypeReference;
5+
import com.fasterxml.jackson.databind.DeserializationFeature;
6+
import com.fasterxml.jackson.databind.JsonMappingException;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import com.fasterxml.jackson.databind.ObjectReader;
9+
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
10+
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
11+
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
12+
import org.junit.Test;
13+
314
import java.math.BigInteger;
415
import java.time.Duration;
16+
import java.time.temporal.ChronoUnit;
517
import java.time.temporal.TemporalAmount;
618
import java.util.Map;
719

8-
import com.fasterxml.jackson.annotation.JsonFormat;
9-
import com.fasterxml.jackson.core.type.TypeReference;
10-
import org.junit.Test;
11-
1220
import static org.junit.Assert.assertEquals;
1321
import static org.junit.Assert.assertNotNull;
1422
import static org.junit.Assert.assertNull;
1523
import static org.junit.Assert.assertTrue;
1624
import static org.junit.Assert.fail;
1725

18-
import com.fasterxml.jackson.databind.DeserializationFeature;
19-
import com.fasterxml.jackson.databind.JsonMappingException;
20-
import com.fasterxml.jackson.databind.ObjectMapper;
21-
import com.fasterxml.jackson.databind.ObjectReader;
22-
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
23-
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
24-
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
25-
2626
public class DurationDeserTest extends ModuleTestBase
2727
{
2828
private final ObjectReader READER = newMapper().readerFor(Duration.class);
2929

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

32+
final static class Wrapper {
33+
public Duration value;
34+
35+
public Wrapper() { }
36+
public Wrapper(Duration v) { value = v; }
37+
}
38+
39+
3240
@Test
3341
public void testDeserializationAsFloat01() throws Exception
3442
{
@@ -420,4 +428,140 @@ public void testStrictDeserializeFromEmptyString() throws Exception {
420428
String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
421429
objectReader.readValue(valueFromEmptyStr);
422430
}
431+
432+
@Test
433+
public void shouldDeserializeInNanos_whenNanosUnitAsPattern_andValueIsInteger() throws Exception {
434+
ObjectMapper mapper = newMapper();
435+
mapper.configOverride(Duration.class)
436+
.setFormat(JsonFormat.Value.forPattern("NANOS"));
437+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
438+
439+
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
440+
441+
assertEquals(Duration.ofNanos(25), wrapper.value);
442+
}
443+
444+
@Test
445+
public void shouldDeserializeInMicros_whenMicrosUnitAsPattern_andValueIsInteger() throws Exception {
446+
ObjectMapper mapper = newMapper();
447+
mapper.configOverride(Duration.class)
448+
.setFormat(JsonFormat.Value.forPattern("MICROS"));
449+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
450+
451+
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
452+
453+
assertEquals(Duration.of(25, ChronoUnit.MICROS), wrapper.value);
454+
}
455+
456+
@Test
457+
public void shouldDeserializeInMillis_whenMillisUnitAsPattern_andValueIsInteger() throws Exception {
458+
ObjectMapper mapper = newMapper();
459+
mapper.configOverride(Duration.class)
460+
.setFormat(JsonFormat.Value.forPattern("MILLIS"));
461+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
462+
463+
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
464+
465+
assertEquals(Duration.ofMillis(25), wrapper.value);
466+
}
467+
468+
@Test
469+
public void shouldDeserializeInSeconds_whenSecondsUnitAsPattern_andValueIsInteger() throws Exception {
470+
ObjectMapper mapper = newMapper();
471+
mapper.configOverride(Duration.class)
472+
.setFormat(JsonFormat.Value.forPattern("SECONDS"));
473+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
474+
475+
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
476+
477+
assertEquals(Duration.ofSeconds(25), wrapper.value);
478+
}
479+
480+
@Test
481+
public void shouldDeserializeInMinutes_whenMinutesUnitAsPattern_andValueIsInteger() throws Exception {
482+
ObjectMapper mapper = newMapper();
483+
mapper.configOverride(Duration.class)
484+
.setFormat(JsonFormat.Value.forPattern("MINUTES"));
485+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
486+
487+
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
488+
489+
assertEquals(Duration.ofMinutes(25), wrapper.value);
490+
}
491+
492+
@Test
493+
public void shouldDeserializeInHours_whenHoursUnitAsPattern_andValueIsInteger() throws Exception {
494+
ObjectMapper mapper = newMapper();
495+
mapper.configOverride(Duration.class)
496+
.setFormat(JsonFormat.Value.forPattern("HOURS"));
497+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
498+
499+
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
500+
501+
assertEquals(Duration.ofHours(25), wrapper.value);
502+
}
503+
504+
@Test
505+
public void shouldDeserializeInHalfDays_whenHalfDaysUnitAsPattern_andValueIsInteger() throws Exception {
506+
ObjectMapper mapper = newMapper();
507+
mapper.configOverride(Duration.class)
508+
.setFormat(JsonFormat.Value.forPattern("HALF_DAYS"));
509+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
510+
511+
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
512+
513+
assertEquals(Duration.of(25, ChronoUnit.HALF_DAYS), wrapper.value);
514+
}
515+
516+
@Test
517+
public void shouldDeserializeInDays_whenDaysUnitAsPattern_andValueIsInteger() throws Exception {
518+
ObjectMapper mapper = newMapper();
519+
mapper.configOverride(Duration.class)
520+
.setFormat(JsonFormat.Value.forPattern("DAYS"));
521+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
522+
523+
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
524+
525+
assertEquals(Duration.ofDays(25), wrapper.value);
526+
}
527+
528+
@Test
529+
public void shouldIgnoreUnitPattern_whenValueIsFloat() throws Exception {
530+
ObjectMapper mapper = newMapper();
531+
mapper.configOverride(Duration.class)
532+
.setFormat(JsonFormat.Value.forPattern("MINUTES"));
533+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
534+
535+
Wrapper wrapper = reader.readValue(wrapperPayload(25.5), Wrapper.class);
536+
537+
assertEquals(Duration.parse("PT25.5S"), wrapper.value);
538+
}
539+
540+
@Test
541+
public void shouldIgnoreUnitPattern_whenValueIsString() throws Exception {
542+
ObjectMapper mapper = newMapper();
543+
mapper.configOverride(Duration.class)
544+
.setFormat(JsonFormat.Value.forPattern("MINUTES"));
545+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
546+
547+
Wrapper wrapper = reader.readValue("{\"value\":\"PT25S\"}", Wrapper.class);
548+
549+
assertEquals(Duration.parse("PT25S"), wrapper.value);
550+
}
551+
552+
@Test
553+
public void shouldIgnoreUnitPattern_whenUnitPatternDoesNotMatchExactly() throws Exception {
554+
ObjectMapper mapper = newMapper();
555+
mapper.configOverride(Duration.class)
556+
.setFormat(JsonFormat.Value.forPattern("Nanos"));
557+
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
558+
559+
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
560+
561+
assertEquals(Duration.ofSeconds(25), wrapper.value);
562+
}
563+
564+
private String wrapperPayload(Number number) {
565+
return "{\"value\":" + number + "}";
566+
}
423567
}

0 commit comments

Comments
 (0)