Skip to content

Commit c8f9733

Browse files
committed
Support unit pattern in duration serializer. ref FasterXML#189
1 parent f2f7686 commit c8f9733

File tree

3 files changed

+270
-28
lines changed

3 files changed

+270
-28
lines changed

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerializer.java

+68-15
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@
2121
import com.fasterxml.jackson.core.JsonParser;
2222
import com.fasterxml.jackson.core.JsonToken;
2323

24+
import com.fasterxml.jackson.databind.BeanProperty;
2425
import com.fasterxml.jackson.databind.JavaType;
2526
import com.fasterxml.jackson.databind.JsonMappingException;
27+
import com.fasterxml.jackson.databind.JsonSerializer;
2628
import com.fasterxml.jackson.databind.SerializationFeature;
2729
import com.fasterxml.jackson.databind.SerializerProvider;
2830
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
2931
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor;
3032
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat;
3133
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
34+
import com.fasterxml.jackson.datatype.jsr310.util.DurationUnitConverter;
3235

3336
import java.io.IOException;
3437
import java.math.BigDecimal;
@@ -52,6 +55,16 @@ public class DurationSerializer extends JSR310FormattedSerializerBase<Duration>
5255

5356
public static final DurationSerializer INSTANCE = new DurationSerializer();
5457

58+
/**
59+
* When defined (not {@code null}) duration values will be converted into integers
60+
* with the unit configured for the converter.
61+
* Only available when {@link SerializationFeature#WRITE_DURATIONS_AS_TIMESTAMPS} is enabled
62+
* and {@link SerializationFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} is not enabled
63+
* since the duration converters do not support fractions
64+
* @since 2.12
65+
*/
66+
private DurationUnitConverter _durationUnitConverter;
67+
5568
private DurationSerializer() {
5669
super(Duration.class);
5770
}
@@ -66,45 +79,80 @@ protected DurationSerializer(DurationSerializer base,
6679
super(base, useTimestamp, useNanoseconds, dtf, null);
6780
}
6881

82+
public DurationSerializer(DurationSerializer base, DurationUnitConverter converter) {
83+
super(base, base._useTimestamp, base._useNanoseconds, base._formatter, base._shape);
84+
_durationUnitConverter = converter;
85+
}
86+
6987
@Override
7088
protected DurationSerializer withFormat(Boolean useTimestamp, DateTimeFormatter dtf, JsonFormat.Shape shape) {
7189
return new DurationSerializer(this, useTimestamp, dtf);
7290
}
7391

92+
protected DurationSerializer withConverter(DurationUnitConverter converter) {
93+
return new DurationSerializer(this, converter);
94+
}
95+
7496
// @since 2.10
7597
@Override
7698
protected SerializationFeature getTimestampsFeature() {
7799
return SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS;
78100
}
79101

102+
@Override
103+
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
104+
DurationSerializer ser = (DurationSerializer) super.createContextual(prov, property);
105+
JsonFormat.Value format = findFormatOverrides(prov, property, handledType());
106+
if (format != null && format.hasPattern()) {
107+
final String pattern = format.getPattern();
108+
DurationUnitConverter p = DurationUnitConverter.from(pattern);
109+
if (p == null) {
110+
prov.reportBadDefinition(handledType(),
111+
String.format(
112+
"Bad 'pattern' definition (\"%s\") for `Duration`: expected one of [%s]",
113+
pattern, DurationUnitConverter.descForAllowed()));
114+
}
115+
116+
ser = ser.withConverter(p);
117+
}
118+
return ser;
119+
}
120+
80121
@Override
81122
public void serialize(Duration duration, JsonGenerator generator, SerializerProvider provider) throws IOException
82123
{
83124
if (useTimestamp(provider)) {
84125
if (useNanoseconds(provider)) {
85-
// 20-Oct-2020, tatu: [modules-java8#165] Need to take care of
86-
// negative values too, and without work-around values
87-
// returned are wonky wrt conversions
88-
BigDecimal bd;
89-
if (duration.isNegative()) {
90-
duration = duration.abs();
91-
bd = DecimalUtils.toBigDecimal(duration.getSeconds(),
92-
duration.getNano())
93-
.negate();
126+
generator.writeNumber(_toNanos(duration));
127+
} else {
128+
if (_durationUnitConverter != null) {
129+
generator.writeNumber(_durationUnitConverter.convert(duration));
94130
} else {
95-
bd = DecimalUtils.toBigDecimal(duration.getSeconds(),
96-
duration.getNano());
131+
generator.writeNumber(duration.toMillis());
97132
}
98-
generator.writeNumber(bd);
99-
} else {
100-
generator.writeNumber(duration.toMillis());
101133
}
102134
} else {
103-
// Does not look like we can make any use of DateTimeFormatter here?
104135
generator.writeString(duration.toString());
105136
}
106137
}
107138

139+
// 20-Oct-2020, tatu: [modules-java8#165] Need to take care of
140+
// negative values too, and without work-around values
141+
// returned are wonky wrt conversions
142+
private BigDecimal _toNanos(Duration duration) {
143+
BigDecimal bd;
144+
if (duration.isNegative()) {
145+
duration = duration.abs();
146+
bd = DecimalUtils.toBigDecimal(duration.getSeconds(),
147+
duration.getNano())
148+
.negate();
149+
} else {
150+
bd = DecimalUtils.toBigDecimal(duration.getSeconds(),
151+
duration.getNano());
152+
}
153+
return bd;
154+
}
155+
108156
@Override
109157
protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException
110158
{
@@ -135,4 +183,9 @@ protected JsonToken serializationShape(SerializerProvider provider) {
135183
protected JSR310FormattedSerializerBase<?> withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) {
136184
return new DurationSerializer(this, _useTimestamp, writeNanoseconds, _formatter);
137185
}
186+
187+
@Override
188+
protected DateTimeFormatter _useDateTimeFormatter(SerializerProvider prov, JsonFormat.Value format) {
189+
return null;
190+
}
138191
}

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/JSR310FormattedSerializerBase.java

+20-13
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,7 @@ public JsonSerializer<?> createContextual(SerializerProvider prov,
144144

145145
// If not, do we have a pattern?
146146
if (format.hasPattern()) {
147-
final String pattern = format.getPattern();
148-
final Locale locale = format.hasLocale() ? format.getLocale() : prov.getLocale();
149-
if (locale == null) {
150-
dtf = DateTimeFormatter.ofPattern(pattern);
151-
} else {
152-
dtf = DateTimeFormatter.ofPattern(pattern, locale);
153-
}
154-
//Issue #69: For instant serializers/deserializers we need to configure the formatter with
155-
//a time zone picked up from JsonFormat annotation, otherwise serialization might not work
156-
if (format.hasTimeZone()) {
157-
dtf = dtf.withZone(format.getTimeZone().toZoneId());
158-
}
147+
dtf = _useDateTimeFormatter(prov, format);
159148
}
160149
JSR310FormattedSerializerBase<?> ser = this;
161150
if ((shape != _shape) || (useTimestamp != _useTimestamp) || (dtf != _formatter)) {
@@ -211,7 +200,7 @@ protected JavaType _integerListType(SerializerProvider prov) {
211200
}
212201
return t;
213202
}
214-
203+
215204
/**
216205
* Overridable method that determines {@link SerializationFeature} that is used as
217206
* the global default in determining if date/time value serialized should use numeric
@@ -265,4 +254,22 @@ protected boolean useNanoseconds(SerializerProvider provider) {
265254
return (provider != null)
266255
&& provider.isEnabled(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
267256
}
257+
258+
// modules-java8#189: to be overridden by other formatters using this as base class
259+
protected DateTimeFormatter _useDateTimeFormatter(SerializerProvider prov, JsonFormat.Value format) {
260+
DateTimeFormatter dtf;
261+
final String pattern = format.getPattern();
262+
final Locale locale = format.hasLocale() ? format.getLocale() : prov.getLocale();
263+
if (locale == null) {
264+
dtf = DateTimeFormatter.ofPattern(pattern);
265+
} else {
266+
dtf = DateTimeFormatter.ofPattern(pattern, locale);
267+
}
268+
//Issue #69: For instant serializers/deserializers we need to configure the formatter with
269+
//a time zone picked up from JsonFormat annotation, otherwise serialization might not work
270+
if (format.hasTimeZone()) {
271+
dtf = dtf.withZone(format.getTimeZone().toZoneId());
272+
}
273+
return dtf;
274+
}
268275
}

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/DurationSerTest.java

+182
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.fasterxml.jackson.datatype.jsr310.ser;
22

3+
import com.fasterxml.jackson.annotation.JsonFormat;
34
import com.fasterxml.jackson.databind.ObjectMapper;
45
import com.fasterxml.jackson.databind.ObjectWriter;
56
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -161,4 +162,185 @@ public void testSerializationWithTypeInfo03() throws Exception
161162
assertEquals("The value is not correct.",
162163
"[\"" + Duration.class.getName() + "\",\"" + duration.toString() + "\"]", value);
163164
}
165+
166+
/*
167+
/**********************************************************
168+
/* Tests for custom patterns (modules-java8#189)
169+
/**********************************************************
170+
*/
171+
172+
@Test
173+
public void shouldSerializeInNanos_whenSetAsPattern() throws Exception
174+
{
175+
ObjectMapper mapper = _mapperForPatternOverride("NANOS");
176+
177+
Duration duration = Duration.ofHours(1);
178+
String value = mapper.writeValueAsString(duration);
179+
180+
assertEquals("The value is not correct.", "3600000000000", value);
181+
}
182+
183+
@Test
184+
public void shouldSerializeInMicros_whenSetAsPattern() throws Exception
185+
{
186+
ObjectMapper mapper = _mapperForPatternOverride("MICROS");
187+
188+
Duration duration = Duration.ofMillis(1);
189+
String value = mapper.writeValueAsString(duration);
190+
191+
assertEquals("The value is not correct.", "1000", value);
192+
}
193+
194+
@Test
195+
public void shouldSerializeInMicrosDiscardingFractions_whenSetAsPattern() throws Exception
196+
{
197+
ObjectMapper mapper = _mapperForPatternOverride("MICROS");
198+
199+
Duration duration = Duration.ofNanos(1500);
200+
String value = mapper.writeValueAsString(duration);
201+
202+
assertEquals("The value is not correct.", "1", value);
203+
}
204+
205+
@Test
206+
public void shouldSerializeInMillis_whenSetAsPattern() throws Exception
207+
{
208+
ObjectMapper mapper = _mapperForPatternOverride("MILLIS");
209+
210+
Duration duration = Duration.ofSeconds(1);
211+
String value = mapper.writeValueAsString(duration);
212+
213+
assertEquals("The value is not correct.", "1000", value);
214+
}
215+
216+
@Test
217+
public void shouldSerializeInMillisDiscardingFractions_whenSetAsPattern() throws Exception
218+
{
219+
ObjectMapper mapper = _mapperForPatternOverride("MILLIS");
220+
221+
Duration duration = Duration.ofNanos(1500000);
222+
String value = mapper.writeValueAsString(duration);
223+
224+
assertEquals("The value is not correct.", "1", value);
225+
}
226+
227+
@Test
228+
public void shouldSerializeInSeconds_whenSetAsPattern() throws Exception
229+
{
230+
ObjectMapper mapper = _mapperForPatternOverride("SECONDS");
231+
232+
Duration duration = Duration.ofMinutes(1);
233+
String value = mapper.writeValueAsString(duration);
234+
235+
assertEquals("The value is not correct.", "60", value);
236+
}
237+
238+
@Test
239+
public void shouldSerializeInSecondsDiscardingFractions_whenSetAsPattern() throws Exception
240+
{
241+
ObjectMapper mapper = _mapperForPatternOverride("SECONDS");
242+
243+
Duration duration = Duration.ofMillis(1500);
244+
String value = mapper.writeValueAsString(duration);
245+
246+
assertEquals("The value is not correct.", "1", value);
247+
}
248+
249+
@Test
250+
public void shouldSerializeInMinutes_whenSetAsPattern() throws Exception
251+
{
252+
ObjectMapper mapper = _mapperForPatternOverride("MINUTES");
253+
254+
Duration duration = Duration.ofHours(1);
255+
String value = mapper.writeValueAsString(duration);
256+
257+
assertEquals("The value is not correct.", "60", value);
258+
}
259+
260+
@Test
261+
public void shouldSerializeInMinutesDiscardingFractions_whenSetAsPattern() throws Exception
262+
{
263+
ObjectMapper mapper = _mapperForPatternOverride("MINUTES");
264+
265+
Duration duration = Duration.ofSeconds(90);
266+
String value = mapper.writeValueAsString(duration);
267+
268+
assertEquals("The value is not correct.", "1", value);
269+
}
270+
271+
@Test
272+
public void shouldSerializeInHours_whenSetAsPattern() throws Exception
273+
{
274+
ObjectMapper mapper = _mapperForPatternOverride("HOURS");
275+
276+
Duration duration = Duration.ofDays(1);
277+
String value = mapper.writeValueAsString(duration);
278+
279+
assertEquals("The value is not correct.", "24", value);
280+
}
281+
282+
@Test
283+
public void shouldSerializeInHoursDiscardingFractions_whenSetAsPattern() throws Exception
284+
{
285+
ObjectMapper mapper = _mapperForPatternOverride("HOURS");
286+
287+
Duration duration = Duration.ofMinutes(90);
288+
String value = mapper.writeValueAsString(duration);
289+
290+
assertEquals("The value is not correct.", "1", value);
291+
}
292+
293+
@Test
294+
public void shouldSerializeInHalfDays_whenSetAsPattern() throws Exception
295+
{
296+
ObjectMapper mapper = _mapperForPatternOverride("HALF_DAYS");
297+
298+
Duration duration = Duration.ofDays(1);
299+
String value = mapper.writeValueAsString(duration);
300+
301+
assertEquals("The value is not correct.", "2", value);
302+
}
303+
304+
@Test
305+
public void shouldSerializeInHalfDaysDiscardingFractions_whenSetAsPattern() throws Exception
306+
{
307+
ObjectMapper mapper = _mapperForPatternOverride("DAYS");
308+
309+
Duration duration = Duration.ofHours(30);
310+
String value = mapper.writeValueAsString(duration);
311+
312+
assertEquals("The value is not correct.", "1", value);
313+
}
314+
315+
@Test
316+
public void shouldSerializeInDays_whenSetAsPattern() throws Exception
317+
{
318+
ObjectMapper mapper = _mapperForPatternOverride("DAYS");
319+
320+
Duration duration = Duration.ofDays(1);
321+
String value = mapper.writeValueAsString(duration);
322+
323+
assertEquals("The value is not correct.", "1", value);
324+
}
325+
326+
@Test
327+
public void shouldSerializeInDaysDiscardingFractions_whenSetAsPattern() throws Exception
328+
{
329+
ObjectMapper mapper = _mapperForPatternOverride("DAYS");
330+
331+
Duration duration = Duration.ofHours(36);
332+
String value = mapper.writeValueAsString(duration);
333+
334+
assertEquals("The value is not correct.", "1", value);
335+
}
336+
337+
protected ObjectMapper _mapperForPatternOverride(String pattern) {
338+
ObjectMapper mapper = mapperBuilder()
339+
.enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
340+
.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
341+
.build();
342+
mapper.configOverride(Duration.class)
343+
.setFormat(JsonFormat.Value.forPattern(pattern));
344+
return mapper;
345+
}
164346
}

0 commit comments

Comments
 (0)