Skip to content

Commit c48e992

Browse files
committed
Fix #803
1 parent 9d0fae4 commit c48e992

File tree

3 files changed

+182
-29
lines changed

3 files changed

+182
-29
lines changed

release-notes/VERSION

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Project: jackson-databind
1313
#432: `StdValueInstantiator` unwraps exceptions, losing context
1414
(reported by Miles K)
1515
#497: Add new JsonInclude.Include feature to exclude maps after exclusion removes all elements
16+
#803: Allow use of `StdDateFormat.setLenient()`
17+
(suggested by raj-ghodke@github)
1618
#819: Add support for setting `FormatFeature` via `ObjectReader`, `ObjectWriter`
1719
#857: Add support for java.beans.Transient (requires Java 7)
1820
(suggested by Thomas M)

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

+138-29
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ public class StdDateFormat
109109
protected transient TimeZone _timezone;
110110

111111
protected final Locale _locale;
112+
113+
/**
114+
* Explicit override for leniency, if specified.
115+
*<p>
116+
* Can not be `final` because {@link #setLenient(boolean)} returns
117+
* `void`.
118+
*
119+
* @since 2.7
120+
*/
121+
protected Boolean _lenient;
112122

113123
protected transient DateFormat _formatRFC1123;
114124
protected transient DateFormat _formatISO8601;
@@ -125,11 +135,18 @@ public StdDateFormat() {
125135
_locale = DEFAULT_LOCALE;
126136
}
127137

138+
@Deprecated // since 2.7
128139
public StdDateFormat(TimeZone tz, Locale loc) {
129140
_timezone = tz;
130141
_locale = loc;
131142
}
132143

144+
protected StdDateFormat(TimeZone tz, Locale loc, Boolean lenient) {
145+
_timezone = tz;
146+
_locale = loc;
147+
_lenient = lenient;
148+
}
149+
133150
public static TimeZone getDefaultTimeZone() {
134151
return DEFAULT_TIMEZONE;
135152
}
@@ -145,22 +162,22 @@ public StdDateFormat withTimeZone(TimeZone tz) {
145162
if ((tz == _timezone) || tz.equals(_timezone)) {
146163
return this;
147164
}
148-
return new StdDateFormat(tz, _locale);
165+
return new StdDateFormat(tz, _locale, _lenient);
149166
}
150167

151168
public StdDateFormat withLocale(Locale loc) {
152169
if (loc.equals(_locale)) {
153170
return this;
154171
}
155-
return new StdDateFormat(_timezone, loc);
172+
return new StdDateFormat(_timezone, loc, _lenient);
156173
}
157174

158175
@Override
159176
public StdDateFormat clone() {
160177
/* Although there is that much state to share, we do need to
161178
* orchestrate a bit, mostly since timezones may be changed
162179
*/
163-
return new StdDateFormat(_timezone, _locale);
180+
return new StdDateFormat(_timezone, _locale, _lenient);
164181
}
165182

166183
/**
@@ -179,7 +196,7 @@ public static DateFormat getISO8601Format(TimeZone tz) {
179196
* @since 2.4
180197
*/
181198
public static DateFormat getISO8601Format(TimeZone tz, Locale loc) {
182-
return _cloneFormat(DATE_FORMAT_ISO8601, DATE_FORMAT_STR_ISO8601, tz, loc);
199+
return _cloneFormat(DATE_FORMAT_ISO8601, DATE_FORMAT_STR_ISO8601, tz, loc, null);
183200
}
184201

185202
/**
@@ -190,7 +207,8 @@ public static DateFormat getISO8601Format(TimeZone tz, Locale loc) {
190207
* @since 2.4
191208
*/
192209
public static DateFormat getRFC1123Format(TimeZone tz, Locale loc) {
193-
return _cloneFormat(DATE_FORMAT_RFC1123, DATE_FORMAT_STR_RFC1123, tz, loc);
210+
return _cloneFormat(DATE_FORMAT_RFC1123, DATE_FORMAT_STR_RFC1123,
211+
tz, loc, null);
194212
}
195213

196214
/**
@@ -200,41 +218,93 @@ public static DateFormat getRFC1123Format(TimeZone tz, Locale loc) {
200218
public static DateFormat getRFC1123Format(TimeZone tz) {
201219
return getRFC1123Format(tz, DEFAULT_LOCALE);
202220
}
203-
221+
204222
/*
205223
/**********************************************************
206-
/* Public API
224+
/* Public API, configuration
207225
/**********************************************************
208226
*/
209227

210228
@Override // since 2.6
211229
public TimeZone getTimeZone() {
212230
return _timezone;
213231
}
214-
232+
215233
@Override
216234
public void setTimeZone(TimeZone tz)
217235
{
218236
/* DateFormats are timezone-specific (via Calendar contained),
219237
* so need to reset instances if timezone changes:
220238
*/
221239
if (!tz.equals(_timezone)) {
222-
_formatRFC1123 = null;
223-
_formatISO8601 = null;
224-
_formatISO8601_z = null;
225-
_formatPlain = null;
240+
_clearFormats();
226241
_timezone = tz;
227242
}
228243
}
229-
244+
245+
/**
246+
* Need to override since we need to keep track of leniency locally,
247+
* and not via underlying {@link Calendar} instance like base class
248+
* does.
249+
*/
250+
@Override // since 2.7
251+
public void setLenient(boolean enabled) {
252+
Boolean newValue = enabled;
253+
if (_lenient != newValue) {
254+
_lenient = newValue;
255+
// and since leniency settings may have been used:
256+
_clearFormats();
257+
}
258+
}
259+
260+
@Override // since 2.7
261+
public boolean isLenient() {
262+
if (_lenient == null) {
263+
// default is, I believe, true
264+
return true;
265+
}
266+
return _lenient.booleanValue();
267+
}
268+
269+
/*
270+
/**********************************************************
271+
/* Public API, parsing
272+
/**********************************************************
273+
*/
274+
230275
@Override
231276
public Date parse(String dateStr) throws ParseException
232277
{
233278
dateStr = dateStr.trim();
234279
ParsePosition pos = new ParsePosition(0);
235-
Date result = parse(dateStr, pos);
236-
if (result != null) {
237-
return result;
280+
281+
Date dt;
282+
283+
if (looksLikeISO8601(dateStr)) { // also includes "plain"
284+
dt = parseAsISO8601(dateStr, pos, true);
285+
} else {
286+
// Also consider "stringified" simple time stamp
287+
int i = dateStr.length();
288+
while (--i >= 0) {
289+
char ch = dateStr.charAt(i);
290+
if (ch < '0' || ch > '9') {
291+
// 07-Aug-2013, tatu: And [databind#267] points out that negative numbers should also work
292+
if (i > 0 || ch != '-') {
293+
break;
294+
}
295+
}
296+
}
297+
if ((i < 0)
298+
// let's just assume negative numbers are fine (can't be RFC-1123 anyway); check length for positive
299+
&& (dateStr.charAt(0) == '-' || NumberInput.inLongRange(dateStr, false))) {
300+
dt = new Date(Long.parseLong(dateStr));
301+
} else {
302+
// Otherwise, fall back to using RFC 1123
303+
dt = parseAsRFC1123(dateStr, pos);
304+
}
305+
}
306+
if (dt != null) {
307+
return dt;
238308
}
239309

240310
StringBuilder sb = new StringBuilder();
@@ -256,7 +326,11 @@ public Date parse(String dateStr) throws ParseException
256326
public Date parse(String dateStr, ParsePosition pos)
257327
{
258328
if (looksLikeISO8601(dateStr)) { // also includes "plain"
259-
return parseAsISO8601(dateStr, pos);
329+
try {
330+
return parseAsISO8601(dateStr, pos, false);
331+
} catch (ParseException e) { // will NOT be thrown due to false but is declared...
332+
return null;
333+
}
260334
}
261335
// Also consider "stringified" simple time stamp
262336
int i = dateStr.length();
@@ -279,12 +353,19 @@ public Date parse(String dateStr, ParsePosition pos)
279353
return parseAsRFC1123(dateStr, pos);
280354
}
281355

356+
/*
357+
/**********************************************************
358+
/* Public API, writing
359+
/**********************************************************
360+
*/
361+
282362
@Override
283363
public StringBuffer format(Date date, StringBuffer toAppendTo,
284364
FieldPosition fieldPosition)
285365
{
286366
if (_formatISO8601 == null) {
287-
_formatISO8601 = _cloneFormat(DATE_FORMAT_ISO8601, DATE_FORMAT_STR_ISO8601, _timezone, _locale);
367+
_formatISO8601 = _cloneFormat(DATE_FORMAT_ISO8601, DATE_FORMAT_STR_ISO8601,
368+
_timezone, _locale, _lenient);
288369
}
289370
return _formatISO8601.format(date, toAppendTo, fieldPosition);
290371
}
@@ -328,7 +409,8 @@ protected boolean looksLikeISO8601(String dateStr)
328409
return false;
329410
}
330411

331-
protected Date parseAsISO8601(String dateStr, ParsePosition pos)
412+
protected Date parseAsISO8601(String dateStr, ParsePosition pos, boolean throwErrors)
413+
throws ParseException
332414
{
333415
/* 21-May-2009, tatu: DateFormat has very strict handling of
334416
* timezone modifiers for ISO-8601. So we need to do some scrubbing.
@@ -341,19 +423,24 @@ protected Date parseAsISO8601(String dateStr, ParsePosition pos)
341423
int len = dateStr.length();
342424
char c = dateStr.charAt(len-1);
343425
DateFormat df;
426+
String formatStr;
344427

345-
// [JACKSON-200]: need to support "plain" date...
428+
// Need to support "plain" date...
346429
if (len <= 10 && Character.isDigit(c)) {
347430
df = _formatPlain;
431+
formatStr = DATE_FORMAT_STR_PLAIN;
348432
if (df == null) {
349-
df = _formatPlain = _cloneFormat(DATE_FORMAT_PLAIN, DATE_FORMAT_STR_PLAIN, _timezone, _locale);
433+
df = _formatPlain = _cloneFormat(DATE_FORMAT_PLAIN, formatStr,
434+
_timezone, _locale, _lenient);
350435
}
351436
} else if (c == 'Z') {
352437
df = _formatISO8601_z;
438+
formatStr = DATE_FORMAT_STR_ISO8601_Z;
353439
if (df == null) {
354-
df = _formatISO8601_z = _cloneFormat(DATE_FORMAT_ISO8601_Z, DATE_FORMAT_STR_ISO8601_Z, _timezone, _locale);
440+
df = _formatISO8601_z = _cloneFormat(DATE_FORMAT_ISO8601_Z, formatStr,
441+
_timezone, _locale, _lenient);
355442
}
356-
// [JACKSON-334]: may be missing milliseconds... if so, add
443+
// may be missing milliseconds... if so, add
357444
if (dateStr.charAt(len-4) == ':') {
358445
StringBuilder sb = new StringBuilder(dateStr);
359446
sb.insert(len-1, ".000");
@@ -398,8 +485,10 @@ protected Date parseAsISO8601(String dateStr, ParsePosition pos)
398485
dateStr = sb.toString();
399486
}
400487
df = _formatISO8601;
488+
formatStr = DATE_FORMAT_STR_ISO8601;
401489
if (_formatISO8601 == null) {
402-
df = _formatISO8601 = _cloneFormat(DATE_FORMAT_ISO8601, DATE_FORMAT_STR_ISO8601, _timezone, _locale);
490+
df = _formatISO8601 = _cloneFormat(DATE_FORMAT_ISO8601, formatStr,
491+
_timezone, _locale, _lenient);
403492
}
404493
} else {
405494
// If not, plain date. Easiest to just patch 'Z' in the end?
@@ -419,19 +508,29 @@ protected Date parseAsISO8601(String dateStr, ParsePosition pos)
419508
sb.append('Z');
420509
dateStr = sb.toString();
421510
df = _formatISO8601_z;
511+
formatStr = DATE_FORMAT_STR_ISO8601_Z;
422512
if (df == null) {
423-
df = _formatISO8601_z = _cloneFormat(DATE_FORMAT_ISO8601_Z, DATE_FORMAT_STR_ISO8601_Z,
424-
_timezone, _locale);
513+
df = _formatISO8601_z = _cloneFormat(DATE_FORMAT_ISO8601_Z, formatStr,
514+
_timezone, _locale, _lenient);
425515
}
426516
}
427517
}
428-
return df.parse(dateStr, pos);
518+
Date dt = df.parse(dateStr, pos);
519+
// 22-Dec-2015, tatu: With non-lenient, may get null
520+
if (dt == null) {
521+
throw new ParseException
522+
(String.format("Can not parse date \"%s\": while it seems to fit format '%s', parsing fails (leniency? %s)",
523+
dateStr, formatStr, _lenient),
524+
pos.getErrorIndex());
525+
}
526+
return dt;
429527
}
430528

431529
protected Date parseAsRFC1123(String dateStr, ParsePosition pos)
432530
{
433531
if (_formatRFC1123 == null) {
434-
_formatRFC1123 = _cloneFormat(DATE_FORMAT_RFC1123, DATE_FORMAT_STR_RFC1123, _timezone, _locale);
532+
_formatRFC1123 = _cloneFormat(DATE_FORMAT_RFC1123, DATE_FORMAT_STR_RFC1123,
533+
_timezone, _locale, _lenient);
435534
}
436535
return _formatRFC1123.parse(dateStr, pos);
437536
}
@@ -452,7 +551,7 @@ private final static boolean hasTimeZone(String str)
452551
}
453552

454553
private final static DateFormat _cloneFormat(DateFormat df, String format,
455-
TimeZone tz, Locale loc)
554+
TimeZone tz, Locale loc, Boolean lenient)
456555
{
457556
if (!loc.equals(DEFAULT_LOCALE)) {
458557
df = new SimpleDateFormat(format, loc);
@@ -463,7 +562,17 @@ private final static DateFormat _cloneFormat(DateFormat df, String format,
463562
df.setTimeZone(tz);
464563
}
465564
}
565+
if (lenient != null) {
566+
df.setLenient(lenient.booleanValue());
567+
}
466568
return df;
467569
}
570+
571+
protected void _clearFormats() {
572+
_formatRFC1123 = null;
573+
_formatISO8601 = null;
574+
_formatISO8601_z = null;
575+
_formatPlain = null;
576+
}
468577
}
469578

src/test/java/com/fasterxml/jackson/databind/TestStdDateFormat.java

+42
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.fasterxml.jackson.databind;
22

3+
import java.text.ParseException;
34
import java.util.*;
45

56
import com.fasterxml.jackson.databind.util.StdDateFormat;
@@ -14,6 +15,47 @@ public void testFactories() {
1415
assertNotNull(StdDateFormat.getRFC1123Format(tz, loc));
1516
}
1617

18+
// [databind#803
19+
public void testLenient() throws Exception
20+
{
21+
StdDateFormat f = StdDateFormat.instance;
22+
23+
// default should be lenient
24+
assertTrue(f.isLenient());
25+
26+
StdDateFormat f2 = f.clone();
27+
assertTrue(f2.isLenient());
28+
29+
f2.setLenient(false);
30+
assertFalse(f2.isLenient());
31+
32+
f2.setLenient(true);
33+
assertTrue(f2.isLenient());
34+
35+
// and for testing, finally, leave as non-lenient
36+
f2.setLenient(false);
37+
assertFalse(f2.isLenient());
38+
StdDateFormat f3 = f2.clone();
39+
assertFalse(f3.isLenient());
40+
41+
// first, legal dates are... legal
42+
Date dt = f3.parse("2015-11-30");
43+
assertNotNull(dt);
44+
45+
// but as importantly, when not lenient, do not allow
46+
try {
47+
f3.parse("2015-11-32");
48+
fail("Should not pass");
49+
} catch (ParseException e) {
50+
verifyException(e, "can not parse date");
51+
}
52+
53+
// ... yet, with lenient, do allow
54+
f3.setLenient(true);
55+
dt = f3.parse("2015-11-32");
56+
assertNotNull(dt);
57+
}
58+
1759
public void testInvalid() {
1860
StdDateFormat std = new StdDateFormat();
1961
try {

0 commit comments

Comments
 (0)