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 trueif 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