1 package com.fasterxml.jackson.databind.util;
2
3 import java.text.DateFormat;
4 import java.text.FieldPosition;
5 import java.text.ParseException;
6 import java.text.ParsePosition;
7 import java.text.SimpleDateFormat;
8 import java.util.*;
9 import java.util.regex.Matcher;
10 import java.util.regex.Pattern;
11
12 import com.fasterxml.jackson.core.io.NumberInput;
13
14 /**
15 * Default {@link DateFormat} implementation used by standard Date
16 * serializers and deserializers. For serialization defaults to using
17 * an ISO-8601 compliant format (format String "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
18 * and for deserialization, both ISO-8601 and RFC-1123.
19 *<br>
20 * Note that `Z` in format String refers to ISO-8601 time offset notation which produces
21 * values like "-08:00" -- that is, full minute/hour combo without colon, and not using `Z`
22 * as alias for "+00:00".
23 * Inclusion of colon as separator, as default setting, started in Jackson 2.11:
24 * prior versions omitted it.
25 * Note that it is possible to enable/disable use of colon in time offset by using method
26 * {@link #withColonInTimeZone} for creating new differently configured format instance,
27 * and configuring {@code ObjectMapper} with it.
28 */
29 @SuppressWarnings("serial")
30 public class StdDateFormat
31 extends DateFormat
32 {
33 /* 24-Jun-2017, tatu: Finally rewrote deserialization to use basic Regex
34 * instead of SimpleDateFormat; partly for better concurrency, partly
35 * for easier enforcing of specific rules. Heavy lifting done by Calendar,
36 * anyway.
37 */
38 protected final static String PATTERN_PLAIN_STR = "\\d\\d\\d\\d[-]\\d\\d[-]\\d\\d";
39
40 protected final static Pattern PATTERN_PLAIN = Pattern.compile(PATTERN_PLAIN_STR);
41
42 protected final static Pattern PATTERN_ISO8601;
43 static {
44 Pattern p = null;
45 try {
46 p = Pattern.compile(PATTERN_PLAIN_STR
47 +"[T]\\d\\d[:]\\d\\d(?:[:]\\d\\d)?" // hours, minutes, optional seconds
48 +"(\\.\\d+)?" // optional second fractions
49 +"(Z|[+-]\\d\\d(?:[:]?\\d\\d)?)?" // optional timeoffset/Z
50 );
51 } catch (Throwable t) {
52 throw new RuntimeException(t);
53 }
54 PATTERN_ISO8601 = p;
55 }
56
57 /**
58 * Defines a commonly used date format that conforms
59 * to ISO-8601 date formatting standard, when it includes basic undecorated
60 * timezone definition.
61 */
62 public final static String DATE_FORMAT_STR_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
63
64 /**
65 * ISO-8601 with just the Date part, no time: needed for error messages
66 */
67 protected final static String DATE_FORMAT_STR_PLAIN = "yyyy-MM-dd";
68
69 /**
70 * This constant defines the date format specified by
71 * RFC 1123 / RFC 822. Used for parsing via `SimpleDateFormat` as well as
72 * error messages.
73 */
74 protected final static String DATE_FORMAT_STR_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
75
76 /**
77 * For error messages we'll also need a list of all formats.
78 */
79 protected final static String[] ALL_FORMATS = new String[] {
80 DATE_FORMAT_STR_ISO8601,
81 "yyyy-MM-dd'T'HH:mm:ss.SSS", // ISO-8601 but no timezone
82 DATE_FORMAT_STR_RFC1123,
83 DATE_FORMAT_STR_PLAIN
84 };
85
86 /**
87 * By default we use UTC for everything, with Jackson 2.7 and later
88 * (2.6 and earlier relied on GMT)
89 */
90 protected final static TimeZone DEFAULT_TIMEZONE;
91 static {
92 DEFAULT_TIMEZONE = TimeZone.getTimeZone("UTC"); // since 2.7
93 }
94
95 protected final static Locale DEFAULT_LOCALE = Locale.US;
96
97 protected final static DateFormat DATE_FORMAT_RFC1123;
98
99 /* Let's construct "blueprint" date format instances: cannot be used
100 * as is, due to thread-safety issues, but can be used for constructing
101 * actual instances more cheaply (avoids re-parsing).
102 */
103 static {
104 // Another important thing: let's force use of default timezone for
105 // baseline DataFormat objects
106 DATE_FORMAT_RFC1123 = new SimpleDateFormat(DATE_FORMAT_STR_RFC1123, DEFAULT_LOCALE);
107 DATE_FORMAT_RFC1123.setTimeZone(DEFAULT_TIMEZONE);
108 }
109
110 /**
111 * A singleton instance can be used for cloning purposes, as a blueprint of sorts.
112 */
113 public final static StdDateFormat instance = new StdDateFormat();
114
115 /**
116 * Blueprint "Calendar" instance for use during formatting. Cannot be used as is,
117 * due to thread-safety issues, but can be used for constructing actual instances
118 * more cheaply by cloning.
119 *
120 * @since 2.9.1
121 */
122 protected static final Calendar CALENDAR = new GregorianCalendar(DEFAULT_TIMEZONE, DEFAULT_LOCALE);
123
124 /**
125 * Caller may want to explicitly override timezone to use; if so,
126 * we will have non-null value here.
127 */
128 protected transient TimeZone _timezone;
129
130 protected final Locale _locale;
131
132 /**
133 * Explicit override for leniency, if specified.
134 *<p>
135 * Cannot be `final` because {@link #setLenient(boolean)} returns
136 * `void`.
137 *
138 * @since 2.7
139 */
140 protected Boolean _lenient;
141
142 /**
143 * Lazily instantiated calendar used by this instance for serialization ({@link #format(Date)}).
144 *
145 * @since 2.9.1
146 */
147 private transient Calendar _calendar;
148
149 private transient DateFormat _formatRFC1123;
150
151 /**
152 * Whether the TZ offset must be formatted with a colon between hours and minutes ({@code HH:mm} format)
153 *<p>
154 * Defaults to {@code true} since 2.11: earlier versions defaulted to {@code false}
155 * for backwards compatibility reasons
156 *
157 * @since 2.9.1
158 */
159 private boolean _tzSerializedWithColon = true;
160
161 /*
162 /**********************************************************
163 /* Life cycle, accessing singleton "standard" formats
164 /**********************************************************
165 */
166
167 public StdDateFormat() {
168 _locale = DEFAULT_LOCALE;
169 }
170
171 @Deprecated // since 2.7
172 public StdDateFormat(TimeZone tz, Locale loc) {
173 _timezone = tz;
174 _locale = loc;
175 }
176
177 protected StdDateFormat(TimeZone tz, Locale loc, Boolean lenient) {
178 this(tz, loc, lenient, false);
179 }
180
181 /**
182 * @since 2.9.1
183 */
184 protected StdDateFormat(TimeZone tz, Locale loc, Boolean lenient,
185 boolean formatTzOffsetWithColon) {
186 _timezone = tz;
187 _locale = loc;
188 _lenient = lenient;
189 _tzSerializedWithColon = formatTzOffsetWithColon;
190 }
191
192 public static TimeZone getDefaultTimeZone() {
193 return DEFAULT_TIMEZONE;
194 }
195
196 /**
197 * Method used for creating a new instance with specified timezone;
198 * if no timezone specified, defaults to the default timezone (UTC).
199 */
200 public StdDateFormat withTimeZone(TimeZone tz) {
201 if (tz == null) {
202 tz = DEFAULT_TIMEZONE;
203 }
204 if ((tz == _timezone) || tz.equals(_timezone)) {
205 return this;
206 }
207 return new StdDateFormat(tz, _locale, _lenient, _tzSerializedWithColon);
208 }
209
210 /**
211 * "Mutant factory" method that will return an instance that uses specified
212 * {@code Locale}:
213 * either {@code this} instance (if setting would not change), or newly
214 * constructed instance with different {@code Locale} to use.
215 */
216 public StdDateFormat withLocale(Locale loc) {
217 if (loc.equals(_locale)) {
218 return this;
219 }
220 return new StdDateFormat(_timezone, loc, _lenient, _tzSerializedWithColon);
221 }
222
223 /**
224 * "Mutant factory" method that will return an instance that has specified leniency
225 * setting: either {@code this} instance (if setting would not change), or newly
226 * constructed instance.
227 *
228 * @since 2.9
229 */
230 public StdDateFormat withLenient(Boolean b) {
231 if (_equals(b, _lenient)) {
232 return this;
233 }
234 return new StdDateFormat(_timezone, _locale, b, _tzSerializedWithColon);
235 }
236
237 /**
238 * "Mutant factory" method that will return an instance that has specified
239 * handling of colon when serializing timezone (timezone either written
240 * like {@code +0500} or {@code +05:00}):
241 * either {@code this} instance (if setting would not change), or newly
242 * constructed instance with desired setting for colon inclusion.
243 *<p>
244 * NOTE: does NOT affect deserialization as colon is optional accepted
245 * but not required -- put another way, either serialization is accepted
246 * by this class.
247 *
248 * @since 2.9.1
249 */
250 public StdDateFormat withColonInTimeZone(boolean b) {
251 if (_tzSerializedWithColon == b) {
252 return this;
253 }
254 return new StdDateFormat(_timezone, _locale, _lenient, b);
255 }
256
257 @Override
258 public StdDateFormat clone() {
259 // Although there isn't that much state to share, we do need to
260 // orchestrate a bit, mostly since timezones may be changed
261 return new StdDateFormat(_timezone, _locale, _lenient, _tzSerializedWithColon);
262 }
263
264 /**
265 * Method for getting a non-shared DateFormat instance
266 * that uses specified timezone and can handle simple ISO-8601
267 * compliant date format.
268 *
269 * @since 2.4
270 *
271 * @deprecated Since 2.9
272 */
273 @Deprecated // since 2.9
274 public static DateFormat getISO8601Format(TimeZone tz, Locale loc) {
275 DateFormat df = new SimpleDateFormat(DATE_FORMAT_STR_ISO8601, loc);
276 df.setTimeZone(DEFAULT_TIMEZONE);
277 return df;
278 }
279
280 /**
281 * Method for getting a non-shared DateFormat instance
282 * that uses specific timezone and can handle RFC-1123
283 * compliant date format.
284 *
285 * @since 2.4
286 *
287 * @deprecated Since 2.9
288 */
289 @Deprecated // since 2.9
290 public static DateFormat getRFC1123Format(TimeZone tz, Locale loc) {
291 return _cloneFormat(DATE_FORMAT_RFC1123, DATE_FORMAT_STR_RFC1123,
292 tz, loc, null);
293 }
294
295 /*
296 /**********************************************************
297 /* Public API, configuration
298 /**********************************************************
299 */
300
301 @Override // since 2.6
302 public TimeZone getTimeZone() {
303 return _timezone;
304 }
305
306 @Override
307 public void setTimeZone(TimeZone tz)
308 {
309 /* DateFormats are timezone-specific (via Calendar contained),
310 * so need to reset instances if timezone changes:
311 */
312 if (!tz.equals(_timezone)) {
313 _clearFormats();
314 _timezone = tz;
315 }
316 }
317
318 /**
319 * Need to override since we need to keep track of leniency locally,
320 * and not via underlying {@link Calendar} instance like base class
321 * does.
322 */
323 @Override // since 2.7
324 public void setLenient(boolean enabled) {
325 Boolean newValue = Boolean.valueOf(enabled);
326 if (!_equals(newValue, _lenient)) {
327 _lenient = newValue;
328 // and since leniency settings may have been used:
329 _clearFormats();
330 }
331 }
332
333 @Override // since 2.7
334 public boolean isLenient() {
335 // default is, I believe, true
336 return (_lenient == null) || _lenient.booleanValue();
337 }
338
339 /**
340 * Accessor for checking whether this instance would include colon
341 * within timezone serialization or not: if {code true}, timezone offset
342 * is serialized like {@code -06:00}; if {code false} as {@code -0600}.
343 *<p>
344 * NOTE: only relevant for serialization (formatting), as deserialization
345 * (parsing) always accepts optional colon but does not require it, regardless
346 * of this setting.
347 *
348 * @return {@code true} if a colon is to be inserted between the hours and minutes
349 * of the TZ offset when serializing as String; otherwise {@code false}
350 *
351 * @since 2.9.1
352 */
353 public boolean isColonIncludedInTimeZone() {
354 return _tzSerializedWithColon;
355 }
356
357 /*
358 /**********************************************************
359 /* Public API, parsing
360 /**********************************************************
361 */
362
363 @Override
364 public Date parse(String dateStr) throws ParseException
365 {
366 dateStr = dateStr.trim();
367 ParsePosition pos = new ParsePosition(0);
368 Date dt = _parseDate(dateStr, pos);
369 if (dt != null) {
370 return dt;
371 }
372 StringBuilder sb = new StringBuilder();
373 for (String f : ALL_FORMATS) {
374 if (sb.length() > 0) {
375 sb.append("\", \"");
376 } else {
377 sb.append('"');
378 }
379 sb.append(f);
380 }
381 sb.append('"');
382 throw new ParseException
383 (String.format("Cannot parse date \"%s\": not compatible with any of standard forms (%s)",
384 dateStr, sb.toString()), pos.getErrorIndex());
385 }
386
387 // 24-Jun-2017, tatu: I don't think this ever gets called. So could... just not implement?
388 @Override
389 public Date parse(String dateStr, ParsePosition pos)
390 {
391 try {
392 return _parseDate(dateStr, pos);
393 } catch (ParseException e) {
394 // may look weird but this is what `DateFormat` suggest to do...
395 }
396 return null;
397 }
398
399 protected Date _parseDate(String dateStr, ParsePosition pos) throws ParseException
400 {
401 if (looksLikeISO8601(dateStr)) { // also includes "plain"
402 return parseAsISO8601(dateStr, pos);
403 }
404 // Also consider "stringified" simple time stamp
405 int i = dateStr.length();
406 while (--i >= 0) {
407 char ch = dateStr.charAt(i);
408 if (ch < '0' || ch > '9') {
409 // 07-Aug-2013, tatu: And [databind#267] points out that negative numbers should also work
410 if (i > 0 || ch != '-') {
411 break;
412 }
413 }
414 }
415 if ((i < 0)
416 // let's just assume negative numbers are fine (can't be RFC-1123 anyway); check length for positive
417 && (dateStr.charAt(0) == '-' || NumberInput.inLongRange(dateStr, false))) {
418 return _parseDateFromLong(dateStr, pos);
419 }
420 // Otherwise, fall back to using RFC 1123. NOTE: call will NOT throw, just returns `null`
421 return parseAsRFC1123(dateStr, pos);
422 }
423
424 /*
425 /**********************************************************
426 /* Public API, writing
427 /**********************************************************
428 */
429
430 @Override
431 public StringBuffer format(Date date, StringBuffer toAppendTo,
432 FieldPosition fieldPosition)
433 {
434 TimeZone tz = _timezone;
435 if (tz == null) {
436 tz = DEFAULT_TIMEZONE;
437 }
438 _format(tz, _locale, date, toAppendTo);
439 return toAppendTo;
440 }
441
442 protected void _format(TimeZone tz, Locale loc, Date date,
443 StringBuffer buffer)
444 {
445 Calendar cal = _getCalendar(tz);
446 cal.setTime(date);
447 // [databind#2167]: handle range beyond [1, 9999]
448 final int year = cal.get(Calendar.YEAR);
449
450 // Assuming GregorianCalendar, special handling needed for BCE (aka BC)
451 if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
452 _formatBCEYear(buffer, year);
453 } else {
454 if (year > 9999) {
455 // 22-Nov-2018, tatu: Handling beyond 4-digits is not well specified wrt ISO-8601, but
456 // it seems that plus prefix IS mandated. Padding is an open question, but since agreeement
457 // for max length would be needed, we ewould need to limit to arbitrary length
458 // like five digits (erroring out if beyond or padding to that as minimum).
459 // Instead, let's just print number out as is and let decoder try to make sense of it.
460 buffer.append('+');
461 }
462 pad4(buffer, year);
463 }
464 buffer.append('-');
465 pad2(buffer, cal.get(Calendar.MONTH) + 1);
466 buffer.append('-');
467 pad2(buffer, cal.get(Calendar.DAY_OF_MONTH));
468 buffer.append('T');
469 pad2(buffer, cal.get(Calendar.HOUR_OF_DAY));
470 buffer.append(':');
471 pad2(buffer, cal.get(Calendar.MINUTE));
472 buffer.append(':');
473 pad2(buffer, cal.get(Calendar.SECOND));
474 buffer.append('.');
475 pad3(buffer, cal.get(Calendar.MILLISECOND));
476
477 int offset = tz.getOffset(cal.getTimeInMillis());
478 if (offset != 0) {
479 int hours = Math.abs((offset / (60 * 1000)) / 60);
480 int minutes = Math.abs((offset / (60 * 1000)) % 60);
481 buffer.append(offset < 0 ? '-' : '+');
482 pad2(buffer, hours);
483 if( _tzSerializedWithColon ) {
484 buffer.append(':');
485 }
486 pad2(buffer, minutes);
487 } else {
488 // 24-Jun-2017, tatu: While `Z` would be conveniently short, older specs
489 // mandate use of full `+0000`
490 // 06-Mar-2020, tatu: Actually statement should read "for compatibility reasons"
491 // and not standards (unless it is wrt RFC-1123). This will change in 3.0 at latest
492 // formatted.append('Z');
493 if( _tzSerializedWithColon ) {
494 buffer.append("+00:00");
495 } else {
496 buffer.append("+0000");
497 }
498 }
499 }
500
501 protected void _formatBCEYear(StringBuffer buffer, int bceYearNoSign) {
502 // Ok. First of all, BCE 1 output (given as value `1` in era BCE) needs to become
503 // "+0000", but rest (from `2` up, in that era) need minus sign.
504 if (bceYearNoSign == 1) {
505 buffer.append("+0000");
506 return;
507 }
508 final int isoYear = bceYearNoSign - 1;
509 buffer.append('-');
510 // as with CE, 4 digit variant needs padding; beyond that not (although that part is
511 // open to debate, needs agreement with receiver)
512 // But `pad4()` deals with "big" numbers now so:
513 pad4(buffer, isoYear);
514 }
515
516 private static void pad2(StringBuffer buffer, int value) {
517 int tens = value / 10;
518 if (tens == 0) {
519 buffer.append('0');
520 } else {
521 buffer.append((char) ('0' + tens));
522 value -= 10 * tens;
523 }
524 buffer.append((char) ('0' + value));
525 }
526
527 private static void pad3(StringBuffer buffer, int value) {
528 int h = value / 100;
529 if (h == 0) {
530 buffer.append('0');
531 } else {
532 buffer.append((char) ('0' + h));
533 value -= (h * 100);
534 }
535 pad2(buffer, value);
536 }
537
538 private static void pad4(StringBuffer buffer, int value) {
539 int h = value / 100;
540 if (h == 0) {
541 buffer.append('0').append('0');
542 } else {
543 if (h > 99) { // [databind#2167]: handle above 9999 correctly
544 buffer.append(h);
545 } else {
546 pad2(buffer, h);
547 }
548 value -= (100 * h);
549 }
550 pad2(buffer, value);
551 }
552
553 /*
554 /**********************************************************
555 /* Std overrides
556 /**********************************************************
557 */
558
559 @Override
560 public String toString() {
561 return String.format("DateFormat %s: (timezone: %s, locale: %s, lenient: %s)",
562 getClass().getName(), _timezone, _locale, _lenient);
563 }
564
565 public String toPattern() { // same as SimpleDateFormat
566 StringBuilder sb = new StringBuilder(100);
567 sb.append("[one of: '")
568 .append(DATE_FORMAT_STR_ISO8601)
569 .append("', '")
570 .append(DATE_FORMAT_STR_RFC1123)
571 .append("' (")
572 ;
573 sb.append(Boolean.FALSE.equals(_lenient) ?
574 "strict" : "lenient")
575 .append(")]");
576 return sb.toString();
577 }
578
579 @Override // since 2.7[.2], as per [databind#1130]
580 public boolean equals(Object o) {
581 return (o == this);
582 }
583
584 @Override // since 2.7[.2], as per [databind#1130]
585 public int hashCode() {
586 return System.identityHashCode(this);
587 }
588
589 /*
590 /**********************************************************
591 /* Helper methods, parsing
592 /**********************************************************
593 */
594
595 /**
596 * Helper method used to figure out if input looks like valid
597 * ISO-8601 string.
598 */
599 protected boolean looksLikeISO8601(String dateStr)
600 {
601 if (dateStr.length() >= 7 // really need 10, but...
602 && Character.isDigit(dateStr.charAt(0))
603 && Character.isDigit(dateStr.charAt(3))
604 && dateStr.charAt(4) == '-'
605 && Character.isDigit(dateStr.charAt(5))
606 ) {
607 return true;
608 }
609 return false;
610 }
611
612 private Date _parseDateFromLong(String longStr, ParsePosition pos) throws ParseException
613 {
614 long ts;
615 try {
616 ts = NumberInput.parseLong(longStr);
617 } catch (NumberFormatException e) {
618 throw new ParseException(String.format(
619 "Timestamp value %s out of 64-bit value range", longStr),
620 pos.getErrorIndex());
621 }
622 return new Date(ts);
623 }
624
625 protected Date parseAsISO8601(String dateStr, ParsePosition pos)
626 throws ParseException
627 {
628 try {
629 return _parseAsISO8601(dateStr, pos);
630 } catch (IllegalArgumentException e) {
631 throw new ParseException(String.format("Cannot parse date \"%s\", problem: %s",
632 dateStr, e.getMessage()),
633 pos.getErrorIndex());
634 }
635 }
636
637 protected Date _parseAsISO8601(String dateStr, ParsePosition bogus)
638 throws IllegalArgumentException, ParseException
639 {
640 final int totalLen = dateStr.length();
641 // actually, one short-cut: if we end with "Z", must be UTC
642 TimeZone tz = DEFAULT_TIMEZONE;
643 if ((_timezone != null) && ('Z' != dateStr.charAt(totalLen-1))) {
644 tz = _timezone;
645 }
646 Calendar cal = _getCalendar(tz);
647 cal.clear();
648 String formatStr;
649 if (totalLen <= 10) {
650 Matcher m = PATTERN_PLAIN.matcher(dateStr);
651 if (m.matches()) {
652 int year = _parse4D(dateStr, 0);
653 int month = _parse2D(dateStr, 5)-1;
654 int day = _parse2D(dateStr, 8);
655
656 cal.set(year, month, day, 0, 0, 0);
657 cal.set(Calendar.MILLISECOND, 0);
658 return cal.getTime();
659 }
660 formatStr = DATE_FORMAT_STR_PLAIN;
661 } else {
662 Matcher m = PATTERN_ISO8601.matcher(dateStr);
663 if (m.matches()) {
664 // Important! START with optional time zone; otherwise Calendar will explode
665
666 int start = m.start(2);
667 int end = m.end(2);
668 int len = end-start;
669 if (len > 1) { // 0 -> none, 1 -> 'Z'
670 // NOTE: first char is sign; then 2 digits, then optional colon, optional 2 digits
671 int offsetSecs = _parse2D(dateStr, start+1) * 3600; // hours
672 if (len >= 5) {
673 offsetSecs += _parse2D(dateStr, end-2) * 60; // minutes
674 }
675 if (dateStr.charAt(start) == '-') {
676 offsetSecs *= -1000;
677 } else {
678 offsetSecs *= 1000;
679 }
680 cal.set(Calendar.ZONE_OFFSET, offsetSecs);
681 // 23-Jun-2017, tatu: Not sure why, but this appears to be needed too:
682 cal.set(Calendar.DST_OFFSET, 0);
683 }
684
685 int year = _parse4D(dateStr, 0);
686 int month = _parse2D(dateStr, 5)-1;
687 int day = _parse2D(dateStr, 8);
688
689 // So: 10 chars for date, then `T`, so starts at 11
690 int hour = _parse2D(dateStr, 11);
691 int minute = _parse2D(dateStr, 14);
692
693 // Seconds are actually optional... so
694 int seconds;
695 if ((totalLen > 16) && dateStr.charAt(16) == ':') {
696 seconds = _parse2D(dateStr, 17);
697 } else {
698 seconds = 0;
699 }
700 cal.set(year, month, day, hour, minute, seconds);
701
702 // Optional milliseconds
703 start = m.start(1) + 1;
704 end = m.end(1);
705 int msecs = 0;
706 if (start >= end) { // no fractional
707 cal.set(Calendar.MILLISECOND, 0);
708 } else {
709 // first char is '.', but rest....
710 msecs = 0;
711 final int fractLen = end-start;
712 switch (fractLen) {
713 default: // [databind#1745] Allow longer fractions... for now, cap at nanoseconds tho
714
715 if (fractLen > 9) { // only allow up to nanos
716 throw new ParseException(String.format(
717 "Cannot parse date \"%s\": invalid fractional seconds '%s'; can use at most 9 digits",
718 dateStr, m.group(1).substring(1)
719 ), start);
720 }
721 // fall through
722 case 3:
723 msecs += (dateStr.charAt(start+2) - '0');
724 case 2:
725 msecs += 10 * (dateStr.charAt(start+1) - '0');
726 case 1:
727 msecs += 100 * (dateStr.charAt(start) - '0');
728 break;
729 case 0:
730 break;
731 }
732 cal.set(Calendar.MILLISECOND, msecs);
733 }
734 return cal.getTime();
735 }
736 formatStr = DATE_FORMAT_STR_ISO8601;
737 }
738
739 throw new ParseException
740 (String.format("Cannot parse date \"%s\": while it seems to fit format '%s', parsing fails (leniency? %s)",
741 dateStr, formatStr, _lenient),
742 // [databind#1742]: Might be able to give actual location, some day, but for now
743 // we can't give anything more indicative
744 0);
745 }
746
747 private static int _parse4D(String str, int index) {
748 return (1000 * (str.charAt(index) - '0'))
749 + (100 * (str.charAt(index+1) - '0'))
750 + (10 * (str.charAt(index+2) - '0'))
751 + (str.charAt(index+3) - '0');
752 }
753
754 private static int _parse2D(String str, int index) {
755 return (10 * (str.charAt(index) - '0'))
756 + (str.charAt(index+1) - '0');
757 }
758
759 protected Date parseAsRFC1123(String dateStr, ParsePosition pos)
760 {
761 if (_formatRFC1123 == null) {
762 _formatRFC1123 = _cloneFormat(DATE_FORMAT_RFC1123, DATE_FORMAT_STR_RFC1123,
763 _timezone, _locale, _lenient);
764 }
765 return _formatRFC1123.parse(dateStr, pos);
766 }
767
768 /*
769 /**********************************************************
770 /* Helper methods, other
771 /**********************************************************
772 */
773
774 private final static DateFormat _cloneFormat(DateFormat df, String format,
775 TimeZone tz, Locale loc, Boolean lenient)
776 {
777 if (!loc.equals(DEFAULT_LOCALE)) {
778 df = new SimpleDateFormat(format, loc);
779 df.setTimeZone((tz == null) ? DEFAULT_TIMEZONE : tz);
780 } else {
781 df = (DateFormat) df.clone();
782 if (tz != null) {
783 df.setTimeZone(tz);
784 }
785 }
786 if (lenient != null) {
787 df.setLenient(lenient.booleanValue());
788 }
789 return df;
790 }
791
792 protected void _clearFormats() {
793 _formatRFC1123 = null;
794 }
795
796 protected Calendar _getCalendar(TimeZone tz) {
797 Calendar cal = _calendar;
798 if (cal == null ) {
799 _calendar = cal = (Calendar)CALENDAR.clone();
800 }
801 if (!cal.getTimeZone().equals(tz) ) {
802 cal.setTimeZone(tz);
803 }
804 cal.setLenient(isLenient());
805 return cal;
806 }
807
808 protected static <T> boolean _equals(T value1, T value2) {
809 if (value1 == value2) {
810 return true;
811 }
812 return (value1 != null) && value1.equals(value2);
813 }
814 }
815