| package cn.emay.sdk.util.json.gson.internal.bind.util; | 
|   | 
| import java.text.ParseException; | 
| import java.text.ParsePosition; | 
| import java.util.Calendar; | 
| import java.util.Date; | 
| import java.util.GregorianCalendar; | 
| import java.util.Locale; | 
| import java.util.TimeZone; | 
|   | 
| /** | 
|  * Utilities methods for manipulating dates in iso8601 format. This is much much | 
|  * faster and GC friendly than using SimpleDateFormat so highly suitable if you | 
|  * (un)serialize lots of date objects. | 
|  *  | 
|  * Supported parse format: | 
|  * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]] | 
|  *  | 
|  * @see <a href="http://www.w3.org/TR/NOTE-datetime">this specification</a> | 
|  */ | 
| // Date parsing code from Jackson databind ISO8601Utils.java | 
| // https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java | 
| public class ISO8601Utils { | 
|     /** | 
|      * ID to represent the 'UTC' string, default timezone since Jackson 2.7 | 
|      *  | 
|      * @since 2.7 | 
|      */ | 
|     private static final String UTC_ID = "UTC"; | 
|     /** | 
|      * The UTC timezone, prefetched to avoid more lookups. | 
|      *  | 
|      * @since 2.7 | 
|      */ | 
|     private static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone(UTC_ID); | 
|   | 
|     /* | 
|      * /********************************************************** /* Formatting | 
|      * /********************************************************** | 
|      */ | 
|   | 
|     /** | 
|      * Format a date into 'yyyy-MM-ddThh:mm:ssZ' (default timezone, no milliseconds | 
|      * precision) | 
|      *  | 
|      * @param date | 
|      *            the date to format | 
|      * @return the date formatted as 'yyyy-MM-ddThh:mm:ssZ' | 
|      */ | 
|     public static String format(Date date) { | 
|         return format(date, false, TIMEZONE_UTC); | 
|     } | 
|   | 
|     /** | 
|      * Format a date into 'yyyy-MM-ddThh:mm:ss[.sss]Z' (GMT timezone) | 
|      *  | 
|      * @param date | 
|      *            the date to format | 
|      * @param millis | 
|      *            true to include millis precision otherwise false | 
|      * @return the date formatted as 'yyyy-MM-ddThh:mm:ss[.sss]Z' | 
|      */ | 
|     public static String format(Date date, boolean millis) { | 
|         return format(date, millis, TIMEZONE_UTC); | 
|     } | 
|   | 
|     /** | 
|      * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] | 
|      *  | 
|      * @param date | 
|      *            the date to format | 
|      * @param millis | 
|      *            true to include millis precision otherwise false | 
|      * @param tz | 
|      *            timezone to use for the formatting (UTC will produce 'Z') | 
|      * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] | 
|      */ | 
|     public static String format(Date date, boolean millis, TimeZone tz) { | 
|         Calendar calendar = new GregorianCalendar(tz, Locale.US); | 
|         calendar.setTime(date); | 
|   | 
|         // estimate capacity of buffer as close as we can (yeah, that's pedantic ;) | 
|         int capacity = "yyyy-MM-ddThh:mm:ss".length(); | 
|         capacity += millis ? ".sss".length() : 0; | 
|         capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length(); | 
|         StringBuilder formatted = new StringBuilder(capacity); | 
|   | 
|         padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length()); | 
|         formatted.append('-'); | 
|         padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length()); | 
|         formatted.append('-'); | 
|         padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length()); | 
|         formatted.append('T'); | 
|         padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length()); | 
|         formatted.append(':'); | 
|         padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length()); | 
|         formatted.append(':'); | 
|         padInt(formatted, calendar.get(Calendar.SECOND), "ss".length()); | 
|         if (millis) { | 
|             formatted.append('.'); | 
|             padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length()); | 
|         } | 
|   | 
|         int offset = tz.getOffset(calendar.getTimeInMillis()); | 
|         if (offset != 0) { | 
|             int hours = Math.abs((offset / (60 * 1000)) / 60); | 
|             int minutes = Math.abs((offset / (60 * 1000)) % 60); | 
|             formatted.append(offset < 0 ? '-' : '+'); | 
|             padInt(formatted, hours, "hh".length()); | 
|             formatted.append(':'); | 
|             padInt(formatted, minutes, "mm".length()); | 
|         } else { | 
|             formatted.append('Z'); | 
|         } | 
|   | 
|         return formatted.toString(); | 
|     } | 
|   | 
|     /* | 
|      * /********************************************************** /* Parsing | 
|      * /********************************************************** | 
|      */ | 
|   | 
|     /** | 
|      * Parse a date from ISO-8601 formatted string. It expects a format | 
|      * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:mm]]] | 
|      *  | 
|      * @param date | 
|      *            ISO string to parse in the appropriate format. | 
|      * @param pos | 
|      *            The position to start parsing from, updated to where parsing | 
|      *            stopped. | 
|      * @return the parsed date | 
|      * @throws ParseException | 
|      *             if the date is not in the appropriate format | 
|      */ | 
|     public static Date parse(String date, ParsePosition pos) throws ParseException { | 
|         Exception fail = null; | 
|         try { | 
|             int offset = pos.getIndex(); | 
|   | 
|             // extract year | 
|             int year = parseInt(date, offset, offset += 4); | 
|             if (checkOffset(date, offset, '-')) { | 
|                 offset += 1; | 
|             } | 
|   | 
|             // extract month | 
|             int month = parseInt(date, offset, offset += 2); | 
|             if (checkOffset(date, offset, '-')) { | 
|                 offset += 1; | 
|             } | 
|   | 
|             // extract day | 
|             int day = parseInt(date, offset, offset += 2); | 
|             // default time value | 
|             int hour = 0; | 
|             int minutes = 0; | 
|             int seconds = 0; | 
|             int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time | 
|   | 
|             // if the value has no time component (and no time zone), we are done | 
|             boolean hasT = checkOffset(date, offset, 'T'); | 
|   | 
|             if (!hasT && (date.length() <= offset)) { | 
|                 Calendar calendar = new GregorianCalendar(year, month - 1, day); | 
|   | 
|                 pos.setIndex(offset); | 
|                 return calendar.getTime(); | 
|             } | 
|   | 
|             if (hasT) { | 
|   | 
|                 // extract hours, minutes, seconds and milliseconds | 
|                 hour = parseInt(date, offset += 1, offset += 2); | 
|                 if (checkOffset(date, offset, ':')) { | 
|                     offset += 1; | 
|                 } | 
|   | 
|                 minutes = parseInt(date, offset, offset += 2); | 
|                 if (checkOffset(date, offset, ':')) { | 
|                     offset += 1; | 
|                 } | 
|                 // second and milliseconds can be optional | 
|                 if (date.length() > offset) { | 
|                     char c = date.charAt(offset); | 
|                     if (c != 'Z' && c != '+' && c != '-') { | 
|                         seconds = parseInt(date, offset, offset += 2); | 
|                         if (seconds > 59 && seconds < 63) | 
|                             seconds = 59; // truncate up to 3 leap seconds | 
|                         // milliseconds can be optional in the format | 
|                         if (checkOffset(date, offset, '.')) { | 
|                             offset += 1; | 
|                             int endOffset = indexOfNonDigit(date, offset + 1); // assume at least one digit | 
|                             int parseEndOffset = Math.min(endOffset, offset + 3); // parse up to 3 digits | 
|                             int fraction = parseInt(date, offset, parseEndOffset); | 
|                             // compensate for "missing" digits | 
|                             switch (parseEndOffset - offset) { // number of digits parsed | 
|                             case 2: | 
|                                 milliseconds = fraction * 10; | 
|                                 break; | 
|                             case 1: | 
|                                 milliseconds = fraction * 100; | 
|                                 break; | 
|                             default: | 
|                                 milliseconds = fraction; | 
|                             } | 
|                             offset = endOffset; | 
|                         } | 
|                     } | 
|                 } | 
|             } | 
|   | 
|             // extract timezone | 
|             if (date.length() <= offset) { | 
|                 throw new IllegalArgumentException("No time zone indicator"); | 
|             } | 
|   | 
|             TimeZone timezone = null; | 
|             char timezoneIndicator = date.charAt(offset); | 
|   | 
|             if (timezoneIndicator == 'Z') { | 
|                 timezone = TIMEZONE_UTC; | 
|                 offset += 1; | 
|             } else if (timezoneIndicator == '+' || timezoneIndicator == '-') { | 
|                 String timezoneOffset = date.substring(offset); | 
|   | 
|                 // When timezone has no minutes, we should append it, valid timezones are, for | 
|                 // example: +00:00, +0000 and +00 | 
|                 timezoneOffset = timezoneOffset.length() >= 5 ? timezoneOffset : timezoneOffset + "00"; | 
|   | 
|                 offset += timezoneOffset.length(); | 
|                 // 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00" | 
|                 if ("+0000".equals(timezoneOffset) || "+00:00".equals(timezoneOffset)) { | 
|                     timezone = TIMEZONE_UTC; | 
|                 } else { | 
|                     // 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC... | 
|                     // not sure why, but that's the way it looks. Further, Javadocs for | 
|                     // `java.util.TimeZone` specifically instruct use of GMT as base for | 
|                     // custom timezones... odd. | 
|                     String timezoneId = "GMT" + timezoneOffset; | 
|                     // String timezoneId = "UTC" + timezoneOffset; | 
|   | 
|                     timezone = TimeZone.getTimeZone(timezoneId); | 
|   | 
|                     String act = timezone.getID(); | 
|                     if (!act.equals(timezoneId)) { | 
|                         /* | 
|                          * 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be | 
|                          * given one without. If so, don't sweat. Yes, very inefficient. Hopefully not | 
|                          * hit often. If it becomes a perf problem, add 'loose' comparison instead. | 
|                          */ | 
|                         String cleaned = act.replace(":", ""); | 
|                         if (!cleaned.equals(timezoneId)) { | 
|                             throw new IndexOutOfBoundsException("Mismatching time zone indicator: " + timezoneId + " given, resolves to " + timezone.getID()); | 
|                         } | 
|                     } | 
|                 } | 
|             } else { | 
|                 throw new IndexOutOfBoundsException("Invalid time zone indicator '" + timezoneIndicator + "'"); | 
|             } | 
|   | 
|             Calendar calendar = new GregorianCalendar(timezone); | 
|             calendar.setLenient(false); | 
|             calendar.set(Calendar.YEAR, year); | 
|             calendar.set(Calendar.MONTH, month - 1); | 
|             calendar.set(Calendar.DAY_OF_MONTH, day); | 
|             calendar.set(Calendar.HOUR_OF_DAY, hour); | 
|             calendar.set(Calendar.MINUTE, minutes); | 
|             calendar.set(Calendar.SECOND, seconds); | 
|             calendar.set(Calendar.MILLISECOND, milliseconds); | 
|   | 
|             pos.setIndex(offset); | 
|             return calendar.getTime(); | 
|             // If we get a ParseException it'll already have the right message/offset. | 
|             // Other exception types can convert here. | 
|         } catch (IndexOutOfBoundsException e) { | 
|             fail = e; | 
|         } catch (NumberFormatException e) { | 
|             fail = e; | 
|         } catch (IllegalArgumentException e) { | 
|             fail = e; | 
|         } | 
|         String input = (date == null) ? null : ('"' + date + "'"); | 
|         String msg = fail.getMessage(); | 
|         if (msg == null || msg.isEmpty()) { | 
|             msg = "(" + fail.getClass().getName() + ")"; | 
|         } | 
|         ParseException ex = new ParseException("Failed to parse date [" + input + "]: " + msg, pos.getIndex()); | 
|         ex.initCause(fail); | 
|         throw ex; | 
|     } | 
|   | 
|     /** | 
|      * Check if the expected character exist at the given offset in the value. | 
|      *  | 
|      * @param value | 
|      *            the string to check at the specified offset | 
|      * @param offset | 
|      *            the offset to look for the expected character | 
|      * @param expected | 
|      *            the expected character | 
|      * @return true if the expected character exist at the given offset | 
|      */ | 
|     private static boolean checkOffset(String value, int offset, char expected) { | 
|         return (offset < value.length()) && (value.charAt(offset) == expected); | 
|     } | 
|   | 
|     /** | 
|      * Parse an integer located between 2 given offsets in a string | 
|      *  | 
|      * @param value | 
|      *            the string to parse | 
|      * @param beginIndex | 
|      *            the start index for the integer in the string | 
|      * @param endIndex | 
|      *            the end index for the integer in the string | 
|      * @return the int | 
|      * @throws NumberFormatException | 
|      *             if the value is not a number | 
|      */ | 
|     private static int parseInt(String value, int beginIndex, int endIndex) throws NumberFormatException { | 
|         if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) { | 
|             throw new NumberFormatException(value); | 
|         } | 
|         // use same logic as in Integer.parseInt() but less generic we're not supporting | 
|         // negative values | 
|         int i = beginIndex; | 
|         int result = 0; | 
|         int digit; | 
|         if (i < endIndex) { | 
|             digit = Character.digit(value.charAt(i++), 10); | 
|             if (digit < 0) { | 
|                 throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex)); | 
|             } | 
|             result = -digit; | 
|         } | 
|         while (i < endIndex) { | 
|             digit = Character.digit(value.charAt(i++), 10); | 
|             if (digit < 0) { | 
|                 throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex)); | 
|             } | 
|             result *= 10; | 
|             result -= digit; | 
|         } | 
|         return -result; | 
|     } | 
|   | 
|     /** | 
|      * Zero pad a number to a specified length | 
|      *  | 
|      * @param buffer | 
|      *            buffer to use for padding | 
|      * @param value | 
|      *            the integer value to pad if necessary. | 
|      * @param length | 
|      *            the length of the string we should zero pad | 
|      */ | 
|     private static void padInt(StringBuilder buffer, int value, int length) { | 
|         String strValue = Integer.toString(value); | 
|         for (int i = length - strValue.length(); i > 0; i--) { | 
|             buffer.append('0'); | 
|         } | 
|         buffer.append(strValue); | 
|     } | 
|   | 
|     /** | 
|      * Returns the index of the first character in the string that is not a digit, | 
|      * starting at offset. | 
|      */ | 
|     private static int indexOfNonDigit(String string, int offset) { | 
|         for (int i = offset; i < string.length(); i++) { | 
|             char c = string.charAt(i); | 
|             if (c < '0' || c > '9') | 
|                 return i; | 
|         } | 
|         return string.length(); | 
|     } | 
|   | 
| } |