• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.fasterxml.jackson.databind.util;
2 
3 import java.text.ParseException;
4 import java.text.ParsePosition;
5 import java.util.*;
6 
7 /**
8  * Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC friendly than using SimpleDateFormat so
9  * highly suitable if you (un)serialize lots of date objects.
10  *
11  * Supported parse format: [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]
12  *
13  * @see <a href="http://www.w3.org/TR/NOTE-datetime">this specification</a>
14  */
15 @Deprecated // since 2.9
16 public class ISO8601Utils
17 {
18     protected final static int DEF_8601_LEN = "yyyy-MM-ddThh:mm:ss.SSS+00:00".length();
19 
20     /**
21      * Timezone we use for 'Z' in ISO-8601 date/time values: since 2.7
22      * {@link #TIMEZONE_UTC}; with earlier versions up to 2.7 was {@link #TIMEZONE_GMT}.
23      */
24     private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC");
25 
26     /*
27     /**********************************************************
28     /* Formatting
29     /**********************************************************
30      */
31 
32     /**
33      * Format a date into 'yyyy-MM-ddThh:mm:ssZ' (default timezone, no milliseconds precision)
34      *
35      * @param date the date to format
36      * @return the date formatted as 'yyyy-MM-ddThh:mm:ssZ'
37      */
format(Date date)38     public static String format(Date date) {
39         return format(date, false, TIMEZONE_Z);
40     }
41 
42     /**
43      * Format a date into 'yyyy-MM-ddThh:mm:ss[.sss]Z' (GMT timezone)
44      *
45      * @param date the date to format
46      * @param millis true to include millis precision otherwise false
47      * @return the date formatted as 'yyyy-MM-ddThh:mm:ss[.sss]Z'
48      */
format(Date date, boolean millis)49     public static String format(Date date, boolean millis) {
50         return format(date, millis, TIMEZONE_Z);
51     }
52 
53     @Deprecated // since 2.9
format(Date date, boolean millis, TimeZone tz)54     public static String format(Date date, boolean millis, TimeZone tz) {
55         return format(date, millis, tz, Locale.US);
56     }
57 
58     /**
59      * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
60      *
61      * @param date the date to format
62      * @param millis true to include millis precision otherwise false
63      * @param tz timezone to use for the formatting (UTC will produce 'Z')
64      * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
65      *
66      * @since 2.9
67      */
format(Date date, boolean millis, TimeZone tz, Locale loc)68     public static String format(Date date, boolean millis, TimeZone tz, Locale loc) {
69         Calendar calendar = new GregorianCalendar(tz, loc);
70         calendar.setTime(date);
71 
72         // estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
73         StringBuilder sb = new StringBuilder(30);
74         sb.append(String.format(
75                 "%04d-%02d-%02dT%02d:%02d:%02d",
76                 calendar.get(Calendar.YEAR),
77                 calendar.get(Calendar.MONTH) + 1,
78                 calendar.get(Calendar.DAY_OF_MONTH),
79                 calendar.get(Calendar.HOUR_OF_DAY),
80                 calendar.get(Calendar.MINUTE),
81                 calendar.get(Calendar.SECOND)
82                 ));
83         if (millis) {
84             sb.append(String.format(".%03d", calendar.get(Calendar.MILLISECOND)));
85         }
86 
87         int offset = tz.getOffset(calendar.getTimeInMillis());
88         if (offset != 0) {
89             int hours = Math.abs((offset / (60 * 1000)) / 60);
90             int minutes = Math.abs((offset / (60 * 1000)) % 60);
91             sb.append(String.format("%c%02d:%02d",
92                     (offset < 0 ? '-' : '+'),
93                     hours, minutes));
94         } else {
95             sb.append('Z');
96         }
97         return sb.toString();
98     }
99 
100     /*
101     /**********************************************************
102     /* Parsing
103     /**********************************************************
104      */
105 
106     /**
107      * Parse a date from ISO-8601 formatted string. It expects a format
108      * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]
109      *
110      * @param date ISO string to parse in the appropriate format.
111      * @param pos The position to start parsing from, updated to where parsing stopped.
112      * @return the parsed date
113      * @throws ParseException if the date is not in the appropriate format
114      */
parse(String date, ParsePosition pos)115     public static Date parse(String date, ParsePosition pos) throws ParseException {
116         Exception fail = null;
117         try {
118             int offset = pos.getIndex();
119 
120             // extract year
121             int year = parseInt(date, offset, offset += 4);
122             if (checkOffset(date, offset, '-')) {
123                 offset += 1;
124             }
125 
126             // extract month
127             int month = parseInt(date, offset, offset += 2);
128             if (checkOffset(date, offset, '-')) {
129                 offset += 1;
130             }
131 
132             // extract day
133             int day = parseInt(date, offset, offset += 2);
134             // default time value
135             int hour = 0;
136             int minutes = 0;
137             int seconds = 0;
138             int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time
139 
140             // if the value has no time component (and no time zone), we are done
141             boolean hasT = checkOffset(date, offset, 'T');
142 
143             if (!hasT && (date.length() <= offset)) {
144                 Calendar calendar = new GregorianCalendar(year, month - 1, day);
145 
146                 pos.setIndex(offset);
147                 return calendar.getTime();
148             }
149 
150             if (hasT) {
151 
152                 // extract hours, minutes, seconds and milliseconds
153                 hour = parseInt(date, offset += 1, offset += 2);
154                 if (checkOffset(date, offset, ':')) {
155                     offset += 1;
156                 }
157 
158                 minutes = parseInt(date, offset, offset += 2);
159                 if (checkOffset(date, offset, ':')) {
160                     offset += 1;
161                 }
162                 // second and milliseconds can be optional
163                 if (date.length() > offset) {
164                     char c = date.charAt(offset);
165                     if (c != 'Z' && c != '+' && c != '-') {
166                         seconds = parseInt(date, offset, offset += 2);
167                         if (seconds > 59 && seconds < 63) seconds = 59; // truncate up to 3 leap seconds
168                         // milliseconds can be optional in the format
169                         if (checkOffset(date, offset, '.')) {
170                             offset += 1;
171                             int endOffset = indexOfNonDigit(date, offset + 1); // assume at least one digit
172                             int parseEndOffset = Math.min(endOffset, offset + 3); // parse up to 3 digits
173                             int fraction = parseInt(date, offset, parseEndOffset);
174                             // compensate for "missing" digits
175                             switch (parseEndOffset - offset) { // number of digits parsed
176                             case 2:
177                                 milliseconds = fraction * 10;
178                                 break;
179                             case 1:
180                                 milliseconds = fraction * 100;
181                                 break;
182                             default:
183                                 milliseconds = fraction;
184                             }
185                             offset = endOffset;
186                         }
187                     }
188                 }
189             }
190 
191             // extract timezone
192             if (date.length() <= offset) {
193                 throw new IllegalArgumentException("No time zone indicator");
194             }
195 
196             TimeZone timezone = null;
197             char timezoneIndicator = date.charAt(offset);
198 
199             if (timezoneIndicator == 'Z') {
200                 timezone = TIMEZONE_Z;
201                 offset += 1;
202             } else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
203                 String timezoneOffset = date.substring(offset);
204                 offset += timezoneOffset.length();
205                 // 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
206                 if ("+0000".equals(timezoneOffset) || "+00:00".equals(timezoneOffset)) {
207                     timezone = TIMEZONE_Z;
208                 } else {
209                     // 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC...
210                     //    not sure why, but that's the way it looks. Further, Javadocs for
211                     //    `java.util.TimeZone` specifically instruct use of GMT as base for
212                     //    custom timezones... odd.
213                     String timezoneId = "GMT" + timezoneOffset;
214 //                    String timezoneId = "UTC" + timezoneOffset;
215 
216                     timezone = TimeZone.getTimeZone(timezoneId);
217 
218                     String act = timezone.getID();
219                     if (!act.equals(timezoneId)) {
220                         /* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given
221                          *    one without. If so, don't sweat.
222                          *   Yes, very inefficient. Hopefully not hit often.
223                          *   If it becomes a perf problem, add 'loose' comparison instead.
224                          */
225                         String cleaned = act.replace(":", "");
226                         if (!cleaned.equals(timezoneId)) {
227                             throw new IndexOutOfBoundsException("Mismatching time zone indicator: "+timezoneId+" given, resolves to "
228                                     +timezone.getID());
229                         }
230                     }
231                 }
232             } else {
233                 throw new IndexOutOfBoundsException("Invalid time zone indicator '" + timezoneIndicator+"'");
234             }
235 
236             Calendar calendar = new GregorianCalendar(timezone);
237             calendar.setLenient(false);
238             calendar.set(Calendar.YEAR, year);
239             calendar.set(Calendar.MONTH, month - 1);
240             calendar.set(Calendar.DAY_OF_MONTH, day);
241             calendar.set(Calendar.HOUR_OF_DAY, hour);
242             calendar.set(Calendar.MINUTE, minutes);
243             calendar.set(Calendar.SECOND, seconds);
244             calendar.set(Calendar.MILLISECOND, milliseconds);
245 
246             pos.setIndex(offset);
247             return calendar.getTime();
248             // If we get a ParseException it'll already have the right message/offset.
249             // Other exception types can convert here.
250         } catch (Exception e) {
251             fail = e;
252         }
253         String input = (date == null) ? null : ('"' + date + '"');
254         String msg = fail.getMessage();
255         if (msg == null || msg.isEmpty()) {
256             msg = "("+fail.getClass().getName()+")";
257         }
258         ParseException ex = new ParseException("Failed to parse date " + input + ": " + msg, pos.getIndex());
259         ex.initCause(fail);
260         throw ex;
261     }
262 
263     /**
264      * Check if the expected character exist at the given offset in the value.
265      *
266      * @param value the string to check at the specified offset
267      * @param offset the offset to look for the expected character
268      * @param expected the expected character
269      * @return true if the expected character exist at the given offset
270      */
checkOffset(String value, int offset, char expected)271     private static boolean checkOffset(String value, int offset, char expected) {
272         return (offset < value.length()) && (value.charAt(offset) == expected);
273     }
274 
275     /**
276      * Parse an integer located between 2 given offsets in a string
277      *
278      * @param value the string to parse
279      * @param beginIndex the start index for the integer in the string
280      * @param endIndex the end index for the integer in the string
281      * @return the int
282      * @throws NumberFormatException if the value is not a number
283      */
parseInt(String value, int beginIndex, int endIndex)284     private static int parseInt(String value, int beginIndex, int endIndex) throws NumberFormatException {
285         if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {
286             throw new NumberFormatException(value);
287         }
288         // use same logic as in Integer.parseInt() but less generic we're not supporting negative values
289         int i = beginIndex;
290         int result = 0;
291         int digit;
292         if (i < endIndex) {
293             digit = Character.digit(value.charAt(i++), 10);
294             if (digit < 0) {
295                 throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
296             }
297             result = -digit;
298         }
299         while (i < endIndex) {
300             digit = Character.digit(value.charAt(i++), 10);
301             if (digit < 0) {
302                 throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
303             }
304             result *= 10;
305             result -= digit;
306         }
307         return -result;
308     }
309 
310     /**
311      * Returns the index of the first character in the string that is not a digit, starting at offset.
312      */
indexOfNonDigit(String string, int offset)313     private static int indexOfNonDigit(String string, int offset) {
314         for (int i = offset; i < string.length(); i++) {
315             char c = string.charAt(i);
316             if (c < '0' || c > '9') return i;
317         }
318         return string.length();
319     }
320 }
321