Skip to content

Commit 9836c01

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

File tree

5 files changed

+344
-23
lines changed

5 files changed

+344
-23
lines changed

datetime/README.md

Lines changed: 14 additions & 0 deletions
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

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,17 @@
2222
import com.fasterxml.jackson.core.JsonTokenId;
2323
import com.fasterxml.jackson.core.StreamReadCapability;
2424
import com.fasterxml.jackson.core.io.NumberInput;
25-
import com.fasterxml.jackson.databind.BeanProperty;
26-
import com.fasterxml.jackson.databind.DeserializationContext;
27-
import com.fasterxml.jackson.databind.DeserializationFeature;
28-
import com.fasterxml.jackson.databind.JsonDeserializer;
29-
import com.fasterxml.jackson.databind.JsonMappingException;
25+
import com.fasterxml.jackson.databind.*;
3026
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
3127
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
3228

3329
import java.io.IOException;
3430
import java.math.BigDecimal;
3531
import java.time.DateTimeException;
3632
import java.time.Duration;
33+
import java.time.temporal.ChronoUnit;
34+
import java.time.temporal.TemporalUnit;
35+
import java.util.*;
3736

3837

3938
/**
@@ -49,8 +48,17 @@ public class DurationDeserializer extends JSR310DeserializerBase<Duration>
4948

5049
public static final DurationDeserializer INSTANCE = new DurationDeserializer();
5150

52-
private DurationDeserializer()
53-
{
51+
/**
52+
* Since 2.12
53+
* When set, integer values will be deserialized using the specified unit. Using this parser will tipically
54+
* override the value specified in {@link DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS} as it is
55+
* considered that the unit set in {@link JsonFormat#pattern()} has precedence since is more specific.
56+
*
57+
* @see [jackson-modules-java8#184] for more info
58+
*/
59+
private DurationUnitParser _durationUnitParser;
60+
61+
private DurationDeserializer() {
5462
super(Duration.class);
5563
}
5664

@@ -61,6 +69,11 @@ protected DurationDeserializer(DurationDeserializer base, Boolean leniency) {
6169
super(base, leniency);
6270
}
6371

72+
protected DurationDeserializer(DurationDeserializer base, DurationUnitParser durationUnitParser) {
73+
super(base, base._isLenient);
74+
_durationUnitParser = durationUnitParser;
75+
}
76+
6477
@Override
6578
protected DurationDeserializer withLeniency(Boolean leniency) {
6679
return new DurationDeserializer(this, leniency);
@@ -79,10 +92,19 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
7992
deser = deser.withLeniency(leniency);
8093
}
8194
}
95+
if (format.hasPattern()) {
96+
deser = DurationUnitParser.from(format.getPattern())
97+
.map(deser::withPattern)
98+
.orElse(deser);
99+
}
82100
}
83101
return deser;
84102
}
85103

104+
private DurationDeserializer withPattern(DurationUnitParser pattern) {
105+
return new DurationDeserializer(this, pattern);
106+
}
107+
86108
@Override
87109
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException
88110
{
@@ -92,7 +114,11 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
92114
BigDecimal value = parser.getDecimalValue();
93115
return DecimalUtils.extractSecondsAndNanos(value, Duration::ofSeconds);
94116
case JsonTokenId.ID_NUMBER_INT:
95-
return _fromTimestamp(context, parser.getLongValue());
117+
long intValue = parser.getLongValue();
118+
if (_durationUnitParser != null) {
119+
return _durationUnitParser.parse(intValue);
120+
}
121+
return _fromTimestamp(context, intValue);
96122
case JsonTokenId.ID_STRING:
97123
return _fromString(parser, context, parser.getText());
98124
// 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
@@ -103,9 +129,9 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
103129
// 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
104130
// values quite easily
105131
return (Duration) parser.getEmbeddedObject();
106-
132+
107133
case JsonTokenId.ID_START_ARRAY:
108-
return _deserializeFromArray(parser, context);
134+
return _deserializeFromArray(parser, context);
109135
}
110136
return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING,
111137
JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT);
@@ -141,4 +167,33 @@ protected Duration _fromTimestamp(DeserializationContext ctxt, long ts) {
141167
}
142168
return Duration.ofMillis(ts);
143169
}
170+
171+
protected static class DurationUnitParser {
172+
final static Set<ChronoUnit> PARSEABLE_UNITS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
173+
ChronoUnit.NANOS,
174+
ChronoUnit.MICROS,
175+
ChronoUnit.MILLIS,
176+
ChronoUnit.SECONDS,
177+
ChronoUnit.MINUTES,
178+
ChronoUnit.HOURS,
179+
ChronoUnit.HALF_DAYS,
180+
ChronoUnit.DAYS
181+
)));
182+
final TemporalUnit unit;
183+
184+
DurationUnitParser(TemporalUnit unit) {
185+
this.unit = unit;
186+
}
187+
188+
Duration parse(long value) {
189+
return Duration.of(value, unit);
190+
}
191+
192+
static Optional<DurationUnitParser> from(String unit) {
193+
return PARSEABLE_UNITS.stream()
194+
.filter(u -> u.name().equals(unit))
195+
.map(DurationUnitParser::new)
196+
.findFirst();
197+
}
198+
}
144199
}

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

Lines changed: 153 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
11
package com.fasterxml.jackson.datatype.jsr310.deser;
22

3-
import java.math.BigInteger;
4-
import java.time.Duration;
5-
import java.time.temporal.TemporalAmount;
6-
import java.util.Map;
7-
83
import com.fasterxml.jackson.annotation.JsonFormat;
94
import com.fasterxml.jackson.core.type.TypeReference;
10-
import org.junit.Test;
11-
12-
import static org.junit.Assert.assertEquals;
13-
import static org.junit.Assert.assertNotNull;
14-
import static org.junit.Assert.assertNull;
15-
import static org.junit.Assert.assertTrue;
16-
import static org.junit.Assert.fail;
17-
185
import com.fasterxml.jackson.databind.DeserializationFeature;
196
import com.fasterxml.jackson.databind.JsonMappingException;
207
import com.fasterxml.jackson.databind.ObjectMapper;
218
import com.fasterxml.jackson.databind.ObjectReader;
229
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
2310
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
2411
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
12+
import org.junit.Test;
13+
14+
import java.math.BigInteger;
15+
import java.time.Duration;
16+
import java.time.temporal.ChronoUnit;
17+
import java.time.temporal.TemporalAmount;
18+
import java.util.Map;
19+
20+
import static org.junit.Assert.*;
2521

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

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

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

0 commit comments

Comments
 (0)