Skip to content

Commit b308e29

Browse files
committed
Merge branch 'master' of github.com:FasterXML/jackson-databind
2 parents 6655a7a + 2eef7a5 commit b308e29

File tree

4 files changed

+160
-26
lines changed

4 files changed

+160
-26
lines changed

release-notes/VERSION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Project: jackson-databind
1111
(reported by ctytgat@github)
1212
#1730: InvalidFormatException` for `JsonToken.VALUE_EMBEDDED_OBJECT`
1313
(reported by zigzago@github)
14+
#1744: StdDateFormat: add option to serialize timezone offset with a colon
15+
(contributed by Bertrand R)
1416
#1745: StdDateFormat: accept and truncate millis larger than 3 digits
1517
(suggested by Bertrand R)
1618
#1749: StdDateFormat: performance improvement of '_format(..)' method

src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@ public ObjectMapper registerModules(Module... modules)
945945
*
946946
* @since 2.2
947947
*/
948-
public ObjectMapper registerModules(Iterable<Module> modules)
948+
public ObjectMapper registerModules(Iterable<? extends Module> modules)
949949
{
950950
for (Module module : modules) {
951951
registerModule(module);

src/main/java/com/fasterxml/jackson/databind/util/StdDateFormat.java

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ public class StdDateFormat
143143

144144
private transient DateFormat _formatRFC1123;
145145

146+
/**
147+
* Whether the TZ offset must be formatted with a colon between hours and minutes ({@code HH:mm} format)
148+
*
149+
* @since 2.9.1
150+
*/
151+
private boolean _tzSerializedWithColon = false;
152+
146153
/*
147154
/**********************************************************
148155
/* Life cycle, accessing singleton "standard" formats
@@ -160,9 +167,18 @@ public StdDateFormat(TimeZone tz, Locale loc) {
160167
}
161168

162169
protected StdDateFormat(TimeZone tz, Locale loc, Boolean lenient) {
170+
this(tz, loc, lenient, false);
171+
}
172+
173+
/**
174+
* @since 2.9.1
175+
*/
176+
protected StdDateFormat(TimeZone tz, Locale loc, Boolean lenient,
177+
boolean formatTzOffsetWithColon) {
163178
_timezone = tz;
164179
_locale = loc;
165180
_lenient = lenient;
181+
_tzSerializedWithColon = formatTzOffsetWithColon;
166182
}
167183

168184
public static TimeZone getDefaultTimeZone() {
@@ -180,31 +196,61 @@ public StdDateFormat withTimeZone(TimeZone tz) {
180196
if ((tz == _timezone) || tz.equals(_timezone)) {
181197
return this;
182198
}
183-
return new StdDateFormat(tz, _locale, _lenient);
199+
return new StdDateFormat(tz, _locale, _lenient, _tzSerializedWithColon);
184200
}
185201

202+
/**
203+
* "Mutant factory" method that will return an instance that uses specified
204+
* {@code Locale}:
205+
* either {@code this} instance (if setting would not change), or newly
206+
* constructed instance with different {@code Locale} to use.
207+
*/
186208
public StdDateFormat withLocale(Locale loc) {
187209
if (loc.equals(_locale)) {
188210
return this;
189211
}
190-
return new StdDateFormat(_timezone, loc, _lenient);
212+
return new StdDateFormat(_timezone, loc, _lenient, _tzSerializedWithColon);
191213
}
192214

193215
/**
216+
* "Mutant factory" method that will return an instance that has specified leniency
217+
* setting: either {@code this} instance (if setting would not change), or newly
218+
* constructed instance.
219+
*
194220
* @since 2.9
195221
*/
196222
public StdDateFormat withLenient(Boolean b) {
197223
if (_equals(b, _lenient)) {
198224
return this;
199225
}
200-
return new StdDateFormat(_timezone, _locale, b);
226+
return new StdDateFormat(_timezone, _locale, b, _tzSerializedWithColon);
201227
}
202228

229+
/**
230+
* "Mutant factory" method that will return an instance that has specified
231+
* handling of colon when serializing timezone (timezone either written
232+
* like {@code +0500} or {@code +05:00}):
233+
* either {@code this} instance (if setting would not change), or newly
234+
* constructed instance with desired setting for colon inclusion.
235+
*<p>
236+
* NOTE: does NOT affect deserialization as colon is optional accepted
237+
* but not required -- put another way, either serialization is accepted
238+
* by this class.
239+
*
240+
* @since 2.9.1
241+
*/
242+
public StdDateFormat withColonInTimeZone(boolean b) {
243+
if (_tzSerializedWithColon == b) {
244+
return this;
245+
}
246+
return new StdDateFormat(_timezone, _locale, _lenient, b);
247+
}
248+
203249
@Override
204250
public StdDateFormat clone() {
205251
// Although there is that much state to share, we do need to
206252
// orchestrate a bit, mostly since timezones may be changed
207-
return new StdDateFormat(_timezone, _locale, _lenient);
253+
return new StdDateFormat(_timezone, _locale, _lenient, _tzSerializedWithColon);
208254
}
209255

210256
/**
@@ -280,6 +326,24 @@ public boolean isLenient() {
280326
return (_lenient == null) || _lenient.booleanValue();
281327
}
282328

329+
/**
330+
* Accessor for checking whether this instance would include colon
331+
* within timezone serialization or not: if {code true}, timezone offset
332+
* is serialized like {@code -06:00}; if {code false} as {@code -0600}.
333+
*<p>
334+
* NOTE: only relevant for serialization (formatting), as deserialization
335+
* (parsing) always accepts optional colon but does not require it, regardless
336+
* of this setting.
337+
*
338+
* @return {@code true} if a colon is to be inserted between the hours and minutes
339+
* of the TZ offset when serializing as String; otherwise {@code false}
340+
*
341+
* @since 2.9.1
342+
*/
343+
public boolean isColonIncludedInTimeZone() {
344+
return _tzSerializedWithColon;
345+
}
346+
283347
/*
284348
/**********************************************************
285349
/* Public API, parsing
@@ -398,15 +462,20 @@ protected void _format(TimeZone tz, Locale loc, Date date,
398462
int minutes = Math.abs((offset / (60 * 1000)) % 60);
399463
buffer.append(offset < 0 ? '-' : '+');
400464
pad2(buffer, hours);
401-
// 24-Jun-2017, tatu: To add colon or not to add colon? Both are legal...
402-
// tests appear to expect no colon so let's go with that.
403-
// formatted.append(':');
465+
if( _tzSerializedWithColon ) {
466+
buffer.append(':');
467+
}
404468
pad2(buffer, minutes);
405469
} else {
406470
// 24-Jun-2017, tatu: While `Z` would be conveniently short, older specs
407471
// mandate use of full `+0000`
408472
// formatted.append('Z');
409-
buffer.append("+0000");
473+
if( _tzSerializedWithColon ) {
474+
buffer.append("+00:00");
475+
}
476+
else {
477+
buffer.append("+0000");
478+
}
410479
}
411480
}
412481

@@ -527,7 +596,7 @@ protected Date parseAsISO8601(String dateStr, ParsePosition pos)
527596
}
528597
}
529598

530-
protected Date _parseAsISO8601(String dateStr, ParsePosition pos)
599+
protected Date _parseAsISO8601(String dateStr, ParsePosition bogus)
531600
throws IllegalArgumentException, ParseException
532601
{
533602
final int totalLen = dateStr.length();
@@ -611,8 +680,7 @@ protected Date _parseAsISO8601(String dateStr, ParsePosition pos)
611680
throw new ParseException(String.format(
612681
"Cannot parse date \"%s\": invalid fractional seconds '%s'; can use at most 9 digits",
613682
dateStr, m.group(1).substring(1)
614-
),
615-
pos.getErrorIndex());
683+
), start);
616684
}
617685
// fall through
618686
case 3:
@@ -635,7 +703,9 @@ protected Date _parseAsISO8601(String dateStr, ParsePosition pos)
635703
throw new ParseException
636704
(String.format("Cannot parse date \"%s\": while it seems to fit format '%s', parsing fails (leniency? %s)",
637705
dateStr, formatStr, _lenient),
638-
pos.getErrorIndex());
706+
// [databind#1742]: Might be able to give actual location, some day, but for now
707+
// we can't give anything more indicative
708+
0);
639709
}
640710

641711
private static int _parse4D(String str, int index) {

src/test/java/com/fasterxml/jackson/databind/ser/jdk/DateSerializationTest.java

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
package com.fasterxml.jackson.databind.ser.jdk;
22

3-
import java.io.*;
4-
import java.text.*;
5-
import java.util.*;
3+
import java.io.IOException;
4+
import java.text.DateFormat;
5+
import java.text.SimpleDateFormat;
6+
import java.util.Calendar;
7+
import java.util.Date;
8+
import java.util.GregorianCalendar;
9+
import java.util.HashMap;
10+
import java.util.Locale;
11+
import java.util.Map;
12+
import java.util.TimeZone;
13+
14+
import org.junit.Assert;
615

716
import com.fasterxml.jackson.annotation.JsonFormat;
8-
import com.fasterxml.jackson.databind.*;
17+
import com.fasterxml.jackson.databind.BaseMapTest;
18+
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import com.fasterxml.jackson.databind.ObjectWriter;
20+
import com.fasterxml.jackson.databind.SerializationFeature;
21+
import com.fasterxml.jackson.databind.util.StdDateFormat;
922

1023
public class DateSerializationTest
1124
extends BaseMapTest
@@ -103,9 +116,42 @@ public void testDateISO8601() throws IOException
103116
{
104117
ObjectMapper mapper = new ObjectMapper();
105118
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
106-
// let's hit epoch start
107-
String json = mapper.writeValueAsString(new Date(0L));
108-
assertEquals("\"1970-01-01T00:00:00.000+0000\"", json);
119+
120+
serialize( mapper, judate(1970, 1, 1, 02, 00, 00, 0, "GMT+2"), "1970-01-01T00:00:00.000+0000");
121+
serialize( mapper, judate(1970, 1, 1, 00, 00, 00, 0, "UTC"), "1970-01-01T00:00:00.000+0000");
122+
}
123+
124+
/**
125+
* Use a default TZ other than UTC. Dates must be serialized using that TZ.
126+
*/
127+
public void testDateISO8601_customTZ() throws IOException
128+
{
129+
ObjectMapper mapper = new ObjectMapper();
130+
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
131+
mapper.setTimeZone(TimeZone.getTimeZone("GMT+2"));
132+
133+
serialize( mapper, judate(1970, 1, 1, 00, 00, 00, 0, "GMT+2"), "1970-01-01T00:00:00.000+0200");
134+
serialize( mapper, judate(1970, 1, 1, 00, 00, 00, 0, "UTC"), "1970-01-01T02:00:00.000+0200");
135+
}
136+
137+
/**
138+
* Configure the StdDateFormat to serialize TZ offset with a colon between hours and minutes
139+
*
140+
* See [databind#1744]
141+
*/
142+
public void testDateISO8601_colonInTZ() throws IOException
143+
{
144+
StdDateFormat dateFormat = new StdDateFormat();
145+
assertFalse(dateFormat.isColonIncludedInTimeZone());
146+
dateFormat = dateFormat.withColonInTimeZone(true);
147+
assertTrue(dateFormat.isColonIncludedInTimeZone());
148+
149+
ObjectMapper mapper = new ObjectMapper();
150+
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
151+
mapper.setDateFormat(dateFormat);
152+
153+
serialize( mapper, judate(1970, 1, 1, 02, 00, 00, 0, "GMT+2"), "1970-01-01T00:00:00.000+00:00");
154+
serialize( mapper, judate(1970, 1, 1, 00, 00, 00, 0, "UTC"), "1970-01-01T00:00:00.000+00:00");
109155
}
110156

111157
public void testDateOther() throws IOException
@@ -114,8 +160,9 @@ public void testDateOther() throws IOException
114160
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'X'HH:mm:ss");
115161
mapper.setDateFormat(df);
116162
mapper.setTimeZone(TimeZone.getTimeZone("PST"));
163+
117164
// let's hit epoch start, offset by a bit
118-
assertEquals(quote("1969-12-31X16:00:00"), mapper.writeValueAsString(new Date(0L)));
165+
serialize( mapper, judate(1970, 1, 1, 00, 00, 00, 0, "UTC"), "1969-12-31X16:00:00");
119166
}
120167

121168
public void testTimeZone() throws IOException
@@ -200,19 +247,18 @@ public void testWithTimeZoneOverride() throws Exception
200247
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd/HH:mm z"));
201248
mapper.setTimeZone(TimeZone.getTimeZone("PST"));
202249
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
203-
String json = mapper.writeValueAsString(new Date(0));
250+
204251
// pacific time is GMT-8; so midnight becomes 16:00 previous day:
205-
assertEquals(quote("1969-12-31/16:00 PST"), json);
252+
serialize( mapper, judate(1969, 12, 31, 16, 00, 00, 00, "PST"), "1969-12-31/16:00 PST");
206253

207254
// Let's also verify that Locale won't matter too much...
208255
mapper.setLocale(Locale.FRANCE);
209-
json = mapper.writeValueAsString(new Date(0));
210-
assertEquals(quote("1969-12-31/16:00 PST"), json);
256+
serialize( mapper, judate(1969, 12, 31, 16, 00, 00, 00, "PST"), "1969-12-31/16:00 PST");
211257

212258
// Also: should be able to dynamically change timezone:
213259
ObjectWriter w = mapper.writer();
214260
w = w.with(TimeZone.getTimeZone("EST"));
215-
json = w.writeValueAsString(new Date(0));
261+
String json = w.writeValueAsString(new Date(0));
216262
assertEquals(quote("1969-12-31/19:00 EST"), json);
217263
}
218264

@@ -272,4 +318,20 @@ public void testFormatWithoutPattern() throws Exception
272318
String json = mapper.writeValueAsString(new DateAsDefaultBeanWithTimezone(0L));
273319
assertEquals(aposToQuotes("{'date':'1970-01-01X01:00:00'}"), json);
274320
}
321+
322+
323+
324+
private static Date judate(int year, int month, int day, int hour, int minutes, int seconds, int millis, String tz) {
325+
Calendar cal = Calendar.getInstance();
326+
cal.set(year, month-1, day, hour, minutes, seconds);
327+
cal.set(Calendar.MILLISECOND, millis);
328+
cal.setTimeZone(TimeZone.getTimeZone(tz));
329+
330+
return cal.getTime();
331+
}
332+
333+
private void serialize(ObjectMapper mapper, Object date, String expected) throws IOException {
334+
String actual = mapper.writeValueAsString(date);
335+
Assert.assertEquals(quote(expected), actual);
336+
}
275337
}

0 commit comments

Comments
 (0)