Skip to content

Commit 8cde8aa

Browse files
committed
Add release notes for FasterXML#184, rework a bit
1 parent 1754c4b commit 8cde8aa

File tree

8 files changed

+164
-169
lines changed

8 files changed

+164
-169
lines changed

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

+78-52
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,32 @@
1616

1717
package com.fasterxml.jackson.datatype.jsr310.deser;
1818

19+
import java.io.IOException;
20+
import java.math.BigDecimal;
21+
import java.time.DateTimeException;
22+
import java.time.Duration;
23+
import java.time.temporal.ChronoUnit;
24+
import java.time.temporal.TemporalUnit;
25+
import java.util.*;
26+
import java.util.stream.Collectors;
27+
1928
import com.fasterxml.jackson.annotation.JsonFormat;
29+
2030
import com.fasterxml.jackson.core.JsonParser;
2131
import com.fasterxml.jackson.core.JsonToken;
2232
import com.fasterxml.jackson.core.JsonTokenId;
2333
import com.fasterxml.jackson.core.StreamReadCapability;
2434
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;
35+
36+
import com.fasterxml.jackson.databind.*;
3037
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
3138
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
3239

33-
import java.io.IOException;
34-
import java.math.BigDecimal;
35-
import java.time.DateTimeException;
36-
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;
44-
45-
4640
/**
4741
* Deserializer for Java 8 temporal {@link Duration}s.
4842
*
4943
* @author Nick Williams
50-
* @since 2.2.0
44+
* @since 2.2
5145
*/
5246
public class DurationDeserializer extends JSR310DeserializerBase<Duration>
5347
implements ContextualDeserializer
@@ -57,39 +51,52 @@ public class DurationDeserializer extends JSR310DeserializerBase<Duration>
5751
public static final DurationDeserializer INSTANCE = new DurationDeserializer();
5852

5953
/**
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.
54+
* When defined (not {@code null}) integer values will be converted into duration
55+
* unit configured for the converter.
56+
* Using this converter will typically override the value specified in
57+
* {@link DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS} as it is
58+
* considered that the unit set in {@link JsonFormat#pattern()} has precedence
59+
* since it is more specific.
60+
*<p>
61+
* See [jackson-modules-java8#184] for more info.
6462
*
65-
* @see [jackson-modules-java8#184] for more info
63+
* @since 2.12
6664
*/
67-
private DurationUnitParser _durationUnitParser;
65+
protected final DurationUnitConverter _durationUnitConverter;
6866

69-
private DurationDeserializer() {
67+
public DurationDeserializer() {
7068
super(Duration.class);
69+
_durationUnitConverter = null;
7170
}
7271

7372
/**
74-
* Since 2.11
73+
* @since 2.11
7574
*/
7675
protected DurationDeserializer(DurationDeserializer base, Boolean leniency) {
7776
super(base, leniency);
77+
_durationUnitConverter = base._durationUnitConverter;
7878
}
7979

80-
protected DurationDeserializer(DurationDeserializer base, DurationUnitParser durationUnitParser) {
80+
/**
81+
* @since 2.12
82+
*/
83+
protected DurationDeserializer(DurationDeserializer base, DurationUnitConverter converter) {
8184
super(base, base._isLenient);
82-
_durationUnitParser = durationUnitParser;
85+
_durationUnitConverter = converter;
8386
}
8487

8588
@Override
8689
protected DurationDeserializer withLeniency(Boolean leniency) {
8790
return new DurationDeserializer(this, leniency);
8891
}
8992

93+
protected DurationDeserializer withConverter(DurationUnitConverter pattern) {
94+
return new DurationDeserializer(this, pattern);
95+
}
96+
9097
@Override
9198
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
92-
BeanProperty property) throws JsonMappingException
99+
BeanProperty property) throws JsonMappingException
93100
{
94101
JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
95102
DurationDeserializer deser = this;
@@ -101,18 +108,20 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
101108
}
102109
}
103110
if (format.hasPattern()) {
104-
deser = DurationUnitParser.from(format.getPattern())
105-
.map(deser::withPattern)
106-
.orElse(deser);
111+
final String pattern = format.getPattern();
112+
DurationUnitConverter p = DurationUnitConverter.from(pattern);
113+
if (p == null) {
114+
ctxt.reportBadDefinition(getValueType(ctxt),
115+
String.format(
116+
"Bad 'pattern' definition (\"%s\") for `Duration`: expected one of [%s]",
117+
pattern, DurationUnitConverter.descForAllowed()));
118+
}
119+
deser = deser.withConverter(p);
107120
}
108121
}
109122
return deser;
110123
}
111124

112-
private DurationDeserializer withPattern(DurationUnitParser pattern) {
113-
return new DurationDeserializer(this, pattern);
114-
}
115-
116125
@Override
117126
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException
118127
{
@@ -122,11 +131,7 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
122131
BigDecimal value = parser.getDecimalValue();
123132
return DecimalUtils.extractSecondsAndNanos(value, Duration::ofSeconds);
124133
case JsonTokenId.ID_NUMBER_INT:
125-
long intValue = parser.getLongValue();
126-
if (_durationUnitParser != null) {
127-
return _durationUnitParser.parse(intValue);
128-
}
129-
return _fromTimestamp(context, intValue);
134+
return _fromTimestamp(context, parser.getLongValue());
130135
case JsonTokenId.ID_STRING:
131136
return _fromString(parser, context, parser.getText());
132137
// 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
@@ -170,14 +175,22 @@ && _isValidTimestampString(value)) {
170175
}
171176

172177
protected Duration _fromTimestamp(DeserializationContext ctxt, long ts) {
178+
if (_durationUnitConverter != null) {
179+
return _durationUnitConverter.convert(ts);
180+
}
181+
// 20-Oct-2020, tatu: This makes absolutely no sense but... somehow
182+
// became the default handling.
173183
if (ctxt.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)) {
174184
return Duration.ofSeconds(ts);
175185
}
176186
return Duration.ofMillis(ts);
177187
}
178188

179-
protected static class DurationUnitParser {
180-
final static Set<ChronoUnit> PARSEABLE_UNITS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
189+
protected static class DurationUnitConverter {
190+
private final static Map<String, ChronoUnit> PARSEABLE_UNITS;
191+
static {
192+
Map<String, ChronoUnit> units = new LinkedHashMap<>();
193+
for (ChronoUnit unit : new ChronoUnit[] {
181194
ChronoUnit.NANOS,
182195
ChronoUnit.MICROS,
183196
ChronoUnit.MILLIS,
@@ -186,22 +199,35 @@ protected static class DurationUnitParser {
186199
ChronoUnit.HOURS,
187200
ChronoUnit.HALF_DAYS,
188201
ChronoUnit.DAYS
189-
)));
202+
}) {
203+
units.put(unit.name(), unit);
204+
}
205+
PARSEABLE_UNITS = units;
206+
}
207+
190208
final TemporalUnit unit;
191209

192-
DurationUnitParser(TemporalUnit unit) {
210+
DurationUnitConverter(TemporalUnit unit) {
193211
this.unit = unit;
194212
}
195213

196-
Duration parse(long value) {
214+
public Duration convert(long value) {
197215
return Duration.of(value, unit);
198216
}
199217

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();
218+
/**
219+
* @return Description of all allowed valued as a sequence of
220+
* double-quoted values separated by comma
221+
*/
222+
public static String descForAllowed() {
223+
return "\"" + PARSEABLE_UNITS.keySet().stream()
224+
.collect(Collectors.joining("\", \""))
225+
+"\"";
226+
}
227+
228+
static DurationUnitConverter from(String unit) {
229+
ChronoUnit chr = PARSEABLE_UNITS.get(unit);
230+
return (chr == null) ? null : new DurationUnitConverter(chr);
205231
}
206232
}
207233
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ protected <R> R _handleUnexpectedToken(DeserializationContext context,
183183

184184
@SuppressWarnings("unchecked")
185185
protected T _failForNotLenient(JsonParser p, DeserializationContext ctxt,
186-
JsonToken expToken) throws IOException
186+
JsonToken expToken) throws IOException
187187
{
188188
return (T) ctxt.handleUnexpectedToken(handledType(), expToken, p,
189189
"Cannot deserialize instance of %s out of %s token: not allowed because 'strict' mode set for property or type (enable 'lenient' handling to allow)",

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

+15-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.fasterxml.jackson.databind.JsonMappingException;
77
import com.fasterxml.jackson.databind.ObjectMapper;
88
import com.fasterxml.jackson.databind.ObjectReader;
9+
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
910
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
1011
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
1112
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
@@ -429,6 +430,12 @@ public void testStrictDeserializeFromEmptyString() throws Exception {
429430
objectReader.readValue(valueFromEmptyStr);
430431
}
431432

433+
/*
434+
/**********************************************************
435+
/* Tests for custom patterns (modules-java8#184)
436+
/**********************************************************
437+
*/
438+
432439
@Test
433440
public void shouldDeserializeInNanos_whenNanosUnitAsPattern_andValueIsInteger() throws Exception {
434441
ObjectMapper mapper = newMapper();
@@ -550,15 +557,19 @@ public void shouldIgnoreUnitPattern_whenValueIsString() throws Exception {
550557
}
551558

552559
@Test
553-
public void shouldIgnoreUnitPattern_whenUnitPatternDoesNotMatchExactly() throws Exception {
560+
public void shouldFailForInvalidPattern() throws Exception {
554561
ObjectMapper mapper = newMapper();
555562
mapper.configOverride(Duration.class)
556563
.setFormat(JsonFormat.Value.forPattern("Nanos"));
557564
ObjectReader reader = mapper.readerFor(MAP_TYPE_REF);
558565

559-
Wrapper wrapper = reader.readValue(wrapperPayload(25), Wrapper.class);
560-
561-
assertEquals(Duration.ofSeconds(25), wrapper.value);
566+
try {
567+
/*Wrapper wrapper =*/ reader.readValue(wrapperPayload(25), Wrapper.class);
568+
fail("Should not allow invalid 'pattern'");
569+
} catch (InvalidDefinitionException e) {
570+
verifyException(e, "Bad 'pattern' definition (\"Nanos\")");
571+
verifyException(e, "expected one of [");
572+
}
562573
}
563574

564575
private String wrapperPayload(Number number) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.fasterxml.jackson.datatype.jsr310.deser;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotNull;
5+
import static org.junit.Assert.assertNull;
6+
7+
import java.time.temporal.ChronoUnit;
8+
9+
import org.junit.Test;
10+
11+
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
12+
import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer.DurationUnitConverter;
13+
14+
public class DurationUnitConverterTest
15+
extends ModuleTestBase
16+
{
17+
@Test
18+
public void shouldMapToTemporalUnit() {
19+
for (ChronoUnit inputUnit : new ChronoUnit[] {
20+
ChronoUnit.NANOS,
21+
ChronoUnit.MICROS,
22+
ChronoUnit.MILLIS,
23+
ChronoUnit.SECONDS,
24+
ChronoUnit.MINUTES,
25+
ChronoUnit.HOURS,
26+
ChronoUnit.HALF_DAYS,
27+
ChronoUnit.DAYS,
28+
}) {
29+
DurationUnitConverter conv = DurationUnitConverter.from(inputUnit.name());
30+
assertNotNull(conv);
31+
assertEquals(inputUnit, conv.unit);
32+
// is case-sensitive:
33+
assertNull(DurationUnitConverter.from(inputUnit.name().toLowerCase()));
34+
}
35+
}
36+
37+
@Test
38+
public void shouldNotMapToTemporalUnit() {
39+
for (String invalid : new String[] {
40+
// Inaccurate units not (yet?) supported
41+
"WEEKS",
42+
"MONTHS",
43+
"YEARS",
44+
"DECADES",
45+
"CENTURIES",
46+
"MILLENNIA",
47+
"ERAS",
48+
"FOREVER",
49+
50+
// Not matching at all
51+
"DOESNOTMATCH", "", " "
52+
}) {
53+
assertNull("Should not map pattern '"+invalid+"'",
54+
DurationUnitConverter.from(invalid));
55+
}
56+
}
57+
}

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

-59
This file was deleted.

0 commit comments

Comments
 (0)