| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package freemarker.template.utility; |
| |
| import java.text.ParseException; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.GregorianCalendar; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Date and time related utilities. |
| */ |
| public class DateUtil { |
| |
| /** |
| * Show hours (24h); always 2 digits, like {@code 00}, {@code 05}, etc. |
| */ |
| public static final int ACCURACY_HOURS = 4; |
| |
| /** |
| * Show hours and minutes (even if minutes is 00). |
| */ |
| public static final int ACCURACY_MINUTES = 5; |
| |
| /** |
| * Show hours, minutes and seconds (even if seconds is 00). |
| */ |
| public static final int ACCURACY_SECONDS = 6; |
| |
| /** |
| * Show hours, minutes and seconds and up to 3 fraction second digits, without trailing 0-s in the fraction part. |
| */ |
| public static final int ACCURACY_MILLISECONDS = 7; |
| |
| /** |
| * Show hours, minutes and seconds and exactly 3 fraction second digits (even if it's 000) |
| */ |
| public static final int ACCURACY_MILLISECONDS_FORCED = 8; |
| |
| public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); |
| |
| private static final String REGEX_XS_TIME_ZONE |
| = "Z|(?:[-+][0-9]{2}:[0-9]{2})"; |
| private static final String REGEX_ISO8601_BASIC_TIME_ZONE |
| = "Z|(?:[-+][0-9]{2}(?:[0-9]{2})?)"; |
| private static final String REGEX_ISO8601_EXTENDED_TIME_ZONE |
| = "Z|(?:[-+][0-9]{2}(?::[0-9]{2})?)"; |
| |
| private static final String REGEX_XS_OPTIONAL_TIME_ZONE |
| = "(" + REGEX_XS_TIME_ZONE + ")?"; |
| private static final String REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE |
| = "(" + REGEX_ISO8601_BASIC_TIME_ZONE + ")?"; |
| private static final String REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE |
| = "(" + REGEX_ISO8601_EXTENDED_TIME_ZONE + ")?"; |
| |
| private static final String REGEX_XS_DATE_BASE |
| = "(-?[0-9]+)-([0-9]{2})-([0-9]{2})"; |
| private static final String REGEX_ISO8601_BASIC_DATE_BASE |
| = "(-?[0-9]{4,}?)([0-9]{2})([0-9]{2})"; |
| private static final String REGEX_ISO8601_EXTENDED_DATE_BASE |
| = "(-?[0-9]{4,})-([0-9]{2})-([0-9]{2})"; |
| |
| private static final String REGEX_XS_TIME_BASE |
| = "([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\\.([0-9]+))?"; |
| private static final String REGEX_ISO8601_BASIC_TIME_BASE |
| = "([0-9]{2})(?:([0-9]{2})(?:([0-9]{2})(?:[\\.,]([0-9]+))?)?)?"; |
| private static final String REGEX_ISO8601_EXTENDED_TIME_BASE |
| = "([0-9]{2})(?::([0-9]{2})(?::([0-9]{2})(?:[\\.,]([0-9]+))?)?)?"; |
| |
| private static final Pattern PATTERN_XS_DATE = Pattern.compile( |
| REGEX_XS_DATE_BASE + REGEX_XS_OPTIONAL_TIME_ZONE); |
| private static final Pattern PATTERN_ISO8601_BASIC_DATE = Pattern.compile( |
| REGEX_ISO8601_BASIC_DATE_BASE); // No time zone allowed here |
| private static final Pattern PATTERN_ISO8601_EXTENDED_DATE = Pattern.compile( |
| REGEX_ISO8601_EXTENDED_DATE_BASE); // No time zone allowed here |
| |
| private static final Pattern PATTERN_XS_TIME = Pattern.compile( |
| REGEX_XS_TIME_BASE + REGEX_XS_OPTIONAL_TIME_ZONE); |
| private static final Pattern PATTERN_ISO8601_BASIC_TIME = Pattern.compile( |
| REGEX_ISO8601_BASIC_TIME_BASE + REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE); |
| private static final Pattern PATTERN_ISO8601_EXTENDED_TIME = Pattern.compile( |
| REGEX_ISO8601_EXTENDED_TIME_BASE + REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE); |
| |
| private static final Pattern PATTERN_XS_DATE_TIME = Pattern.compile( |
| REGEX_XS_DATE_BASE |
| + "T" + REGEX_XS_TIME_BASE |
| + REGEX_XS_OPTIONAL_TIME_ZONE); |
| private static final Pattern PATTERN_ISO8601_BASIC_DATE_TIME = Pattern.compile( |
| REGEX_ISO8601_BASIC_DATE_BASE |
| + "T" + REGEX_ISO8601_BASIC_TIME_BASE |
| + REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE); |
| private static final Pattern PATTERN_ISO8601_EXTENDED_DATE_TIME = Pattern.compile( |
| REGEX_ISO8601_EXTENDED_DATE_BASE |
| + "T" + REGEX_ISO8601_EXTENDED_TIME_BASE |
| + REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE); |
| |
| private static final Pattern PATTERN_XS_TIME_ZONE = Pattern.compile( |
| REGEX_XS_TIME_ZONE); |
| |
| private static final String MSG_YEAR_0_NOT_ALLOWED |
| = "Year 0 is not allowed in XML schema dates. BC 1 is -1, AD 1 is 1."; |
| |
| private DateUtil() { |
| // can't be instantiated |
| } |
| |
| /** |
| * Returns the time zone object for the name (or ID). This differs from |
| * {@link TimeZone#getTimeZone(String)} in that the latest returns GMT |
| * if it doesn't recognize the name, while this throws an |
| * {@link UnrecognizedTimeZoneException}. |
| * |
| * @throws UnrecognizedTimeZoneException If the time zone name wasn't understood |
| */ |
| public static TimeZone getTimeZone(String name) |
| throws UnrecognizedTimeZoneException { |
| if (isGMTish(name)) { |
| if (name.equalsIgnoreCase("UTC")) { |
| return UTC; |
| } |
| return TimeZone.getTimeZone(name); |
| } |
| TimeZone tz = TimeZone.getTimeZone(name); |
| if (isGMTish(tz.getID())) { |
| throw new UnrecognizedTimeZoneException(name); |
| } |
| return tz; |
| } |
| |
| /** |
| * Tells if a offset or time zone is GMT. GMT is a fuzzy term, it used to |
| * referred both to UTC and UT1. |
| */ |
| private static boolean isGMTish(String name) { |
| if (name.length() < 3) { |
| return false; |
| } |
| char c1 = name.charAt(0); |
| char c2 = name.charAt(1); |
| char c3 = name.charAt(2); |
| if ( |
| !( |
| (c1 == 'G' || c1 == 'g') |
| && (c2 == 'M' || c2 == 'm') |
| && (c3 == 'T' || c3 == 't') |
| ) |
| && |
| !( |
| (c1 == 'U' || c1 == 'u') |
| && (c2 == 'T' || c2 == 't') |
| && (c3 == 'C' || c3 == 'c') |
| ) |
| && |
| !( |
| (c1 == 'U' || c1 == 'u') |
| && (c2 == 'T' || c2 == 't') |
| && (c3 == '1') |
| ) |
| ) { |
| return false; |
| } |
| |
| if (name.length() == 3) { |
| return true; |
| } |
| |
| String offset = name.substring(3); |
| if (offset.startsWith("+")) { |
| return offset.equals("+0") || offset.equals("+00") |
| || offset.equals("+00:00"); |
| } else { |
| return offset.equals("-0") || offset.equals("-00") |
| || offset.equals("-00:00"); |
| } |
| } |
| |
| /** |
| * Format a date, time or dateTime with one of the ISO 8601 extended |
| * formats that is also compatible with the XML Schema format (as far as you |
| * don't have dates in the BC era). Examples of possible outputs: |
| * {@code "2005-11-27T15:30:00+02:00"}, {@code "2005-11-27"}, |
| * {@code "15:30:00Z"}. Note the {@code ":00"} in the time zone offset; |
| * this is not required by ISO 8601, but included for compatibility with |
| * the XML Schema format. Regarding the B.C. issue, those dates will be |
| * one year off when read back according the XML Schema format, because of a |
| * mismatch between that format and ISO 8601:2000 Second Edition. |
| * |
| * <p>This method is thread-safe. |
| * |
| * @param date the date to convert to ISO 8601 string |
| * @param datePart whether the date part (year, month, day) will be included |
| * or not |
| * @param timePart whether the time part (hours, minutes, seconds, |
| * milliseconds) will be included or not |
| * @param offsetPart whether the time zone offset part will be included or |
| * not. This will be shown as an offset to UTC (examples: |
| * {@code "+01"}, {@code "-02"}, {@code "+04:30"}) or as {@code "Z"} |
| * for UTC (and for UT1 and for GMT+00, since the Java platform |
| * doesn't really care about the difference). |
| * Note that this can't be {@code true} when {@code timePart} is |
| * {@code false}, because ISO 8601 (2004) doesn't mention such |
| * patterns. |
| * @param accuracy tells which parts of the date/time to drop. The |
| * {@code datePart} and {@code timePart} parameters are stronger than |
| * this. Note that when {@link #ACCURACY_MILLISECONDS} is specified, |
| * the milliseconds part will be displayed as fraction seconds |
| * (like {@code "15:30.00.25"}) with the minimum number of |
| * digits needed to show the milliseconds without precision lose. |
| * Thus, if the milliseconds happen to be exactly 0, no fraction |
| * seconds will be shown at all. |
| * @param timeZone the time zone in which the date/time will be shown. (You |
| * may find {@link DateUtil#UTC} handy here.) Note |
| * that although date-only formats has no time zone offset part, |
| * the result still depends on the time zone, as days start and end |
| * at different points on the time line in different zones. |
| * @param calendarFactory the factory that will create the calendar used |
| * internally for calculations. The point of this parameter is that |
| * creating a new calendar is relatively expensive, so it's desirable |
| * to reuse calendars and only set their time and zone. (This was |
| * tested on Sun JDK 1.6 x86 Win, where it gave 2x-3x speedup.) |
| */ |
| public static String dateToISO8601String( |
| Date date, |
| boolean datePart, boolean timePart, boolean offsetPart, |
| int accuracy, |
| TimeZone timeZone, |
| DateToISO8601CalendarFactory calendarFactory) { |
| return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, false, calendarFactory); |
| } |
| |
| /** |
| * Same as {@link #dateToISO8601String}, but gives XML Schema compliant format. |
| */ |
| public static String dateToXSString( |
| Date date, |
| boolean datePart, boolean timePart, boolean offsetPart, |
| int accuracy, |
| TimeZone timeZone, |
| DateToISO8601CalendarFactory calendarFactory) { |
| return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, true, calendarFactory); |
| } |
| |
| private static String dateToString( |
| Date date, |
| boolean datePart, boolean timePart, boolean offsetPart, |
| int accuracy, |
| TimeZone timeZone, boolean xsMode, |
| DateToISO8601CalendarFactory calendarFactory) { |
| if (!xsMode && !timePart && offsetPart) { |
| throw new IllegalArgumentException( |
| "ISO 8601:2004 doesn't specify any formats where the " |
| + "offset is shown but the time isn't."); |
| } |
| |
| if (timeZone == null) { |
| timeZone = UTC; |
| } |
| |
| GregorianCalendar cal = calendarFactory.get(timeZone, date); |
| |
| int maxLength; |
| if (!timePart) { |
| maxLength = 10 + (xsMode ? 6 : 0); // YYYY-MM-DD+00:00 |
| } else { |
| if (!datePart) { |
| maxLength = 12 + 6; // HH:MM:SS.mmm+00:00 |
| } else { |
| maxLength = 10 + 1 + 12 + 6; |
| } |
| } |
| char[] res = new char[maxLength]; |
| int dstIdx = 0; |
| |
| if (datePart) { |
| int x = cal.get(Calendar.YEAR); |
| if (x > 0 && cal.get(Calendar.ERA) == GregorianCalendar.BC) { |
| x = -x + (xsMode ? 0 : 1); |
| } |
| if (x >= 0 && x < 9999) { |
| res[dstIdx++] = (char) ('0' + x / 1000); |
| res[dstIdx++] = (char) ('0' + x % 1000 / 100); |
| res[dstIdx++] = (char) ('0' + x % 100 / 10); |
| res[dstIdx++] = (char) ('0' + x % 10); |
| } else { |
| String yearString = String.valueOf(x); |
| |
| // Re-allocate buffer: |
| maxLength = maxLength - 4 + yearString.length(); |
| res = new char[maxLength]; |
| |
| for (int i = 0; i < yearString.length(); i++) { |
| res[dstIdx++] = yearString.charAt(i); |
| } |
| } |
| |
| res[dstIdx++] = '-'; |
| |
| x = cal.get(Calendar.MONTH) + 1; |
| dstIdx = append00(res, dstIdx, x); |
| |
| res[dstIdx++] = '-'; |
| |
| x = cal.get(Calendar.DAY_OF_MONTH); |
| dstIdx = append00(res, dstIdx, x); |
| |
| if (timePart) { |
| res[dstIdx++] = 'T'; |
| } |
| } |
| |
| if (timePart) { |
| int x = cal.get(Calendar.HOUR_OF_DAY); |
| dstIdx = append00(res, dstIdx, x); |
| |
| if (accuracy >= ACCURACY_MINUTES) { |
| res[dstIdx++] = ':'; |
| |
| x = cal.get(Calendar.MINUTE); |
| dstIdx = append00(res, dstIdx, x); |
| |
| if (accuracy >= ACCURACY_SECONDS) { |
| res[dstIdx++] = ':'; |
| |
| x = cal.get(Calendar.SECOND); |
| dstIdx = append00(res, dstIdx, x); |
| |
| if (accuracy >= ACCURACY_MILLISECONDS) { |
| x = cal.get(Calendar.MILLISECOND); |
| int forcedDigits = accuracy == ACCURACY_MILLISECONDS_FORCED ? 3 : 0; |
| if (x != 0 || forcedDigits != 0) { |
| if (x > 999) { |
| // Shouldn't ever happen... |
| throw new RuntimeException( |
| "Calendar.MILLISECOND > 999"); |
| } |
| res[dstIdx++] = '.'; |
| do { |
| res[dstIdx++] = (char) ('0' + (x / 100)); |
| forcedDigits--; |
| x = x % 100 * 10; |
| } while (x != 0 || forcedDigits > 0); |
| } |
| } |
| } |
| } |
| } |
| |
| if (offsetPart) { |
| if (timeZone == UTC) { |
| res[dstIdx++] = 'Z'; |
| } else { |
| int dt = timeZone.getOffset(date.getTime()); |
| boolean positive; |
| if (dt < 0) { |
| positive = false; |
| dt = -dt; |
| } else { |
| positive = true; |
| } |
| |
| dt /= 1000; |
| int offS = dt % 60; |
| dt /= 60; |
| int offM = dt % 60; |
| dt /= 60; |
| int offH = dt; |
| |
| if (offS == 0 && offM == 0 && offH == 0) { |
| res[dstIdx++] = 'Z'; |
| } else { |
| res[dstIdx++] = positive ? '+' : '-'; |
| dstIdx = append00(res, dstIdx, offH); |
| res[dstIdx++] = ':'; |
| dstIdx = append00(res, dstIdx, offM); |
| if (offS != 0) { |
| res[dstIdx++] = ':'; |
| dstIdx = append00(res, dstIdx, offS); |
| } |
| } |
| } |
| } |
| |
| return new String(res, 0, dstIdx); |
| } |
| |
| /** |
| * Appends a number between 0 and 99 padded to 2 digits. |
| */ |
| private static int append00(char[] res, int dstIdx, int x) { |
| res[dstIdx++] = (char) ('0' + x / 10); |
| res[dstIdx++] = (char) ('0' + x % 10); |
| return dstIdx; |
| } |
| |
| /** |
| * Parses an W3C XML Schema date string (not time or date-time). |
| * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. |
| * |
| * @param dateStr the string to parse. |
| * @param defaultTimeZone used if the date doesn't specify the |
| * time zone offset explicitly. Can't be {@code null}. |
| * @param calToDateConverter Used internally to calculate the result from the calendar field values. |
| * If you don't have a such object around, you can just use |
| * {@code new }{@link TrivialCalendarFieldsToDateConverter}{@code ()}. |
| * |
| * @throws DateParseException if the date is malformed, or if the time |
| * zone offset is unspecified and the {@code defaultTimeZone} is |
| * {@code null}. |
| */ |
| public static Date parseXSDate( |
| String dateStr, TimeZone defaultTimeZone, |
| CalendarFieldsToDateConverter calToDateConverter) |
| throws DateParseException { |
| Matcher m = PATTERN_XS_DATE.matcher(dateStr); |
| if (!m.matches()) { |
| throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_DATE); |
| } |
| return parseDate_parseMatcher( |
| m, defaultTimeZone, true, calToDateConverter); |
| } |
| |
| /** |
| * Same as {@link #parseXSDate(String, TimeZone, CalendarFieldsToDateConverter)}, but for ISO 8601 dates. |
| */ |
| public static Date parseISO8601Date( |
| String dateStr, TimeZone defaultTimeZone, |
| CalendarFieldsToDateConverter calToDateConverter) |
| throws DateParseException { |
| Matcher m = PATTERN_ISO8601_EXTENDED_DATE.matcher(dateStr); |
| if (!m.matches()) { |
| m = PATTERN_ISO8601_BASIC_DATE.matcher(dateStr); |
| if (!m.matches()) { |
| throw new DateParseException("The value didn't match the expected pattern: " |
| + PATTERN_ISO8601_EXTENDED_DATE + " or " |
| + PATTERN_ISO8601_BASIC_DATE); |
| } |
| } |
| return parseDate_parseMatcher( |
| m, defaultTimeZone, false, calToDateConverter); |
| } |
| |
| private static Date parseDate_parseMatcher( |
| Matcher m, TimeZone defaultTZ, |
| boolean xsMode, |
| CalendarFieldsToDateConverter calToDateConverter) |
| throws DateParseException { |
| NullArgumentException.check("defaultTZ", defaultTZ); |
| try { |
| int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE); |
| |
| int era; |
| // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2. |
| // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based |
| // on the earlier version where 0000 didn't exist, and year -1 is BC 1. |
| if (year <= 0) { |
| era = GregorianCalendar.BC; |
| year = -year + (xsMode ? 0 : 1); |
| if (year == 0) { |
| throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED); |
| } |
| } else { |
| era = GregorianCalendar.AD; |
| } |
| |
| int month = groupToInt(m.group(2), "month", 1, 12) - 1; |
| int day = groupToInt(m.group(3), "day-of-month", 1, 31); |
| |
| TimeZone tz = xsMode ? parseMatchingTimeZone(m.group(4), defaultTZ) : defaultTZ; |
| |
| return calToDateConverter.calculate(era, year, month, day, 0, 0, 0, 0, false, tz); |
| } catch (IllegalArgumentException e) { |
| // Calendar methods used to throw this for illegal dates. |
| throw new DateParseException( |
| "Date calculation faliure. " |
| + "Probably the date is formally correct, but refers " |
| + "to an unexistent date (like February 30)."); |
| } |
| } |
| |
| /** |
| * Parses an W3C XML Schema time string (not date or date-time). |
| * If the time string doesn't specify the time zone offset explicitly, |
| * the value of the {@code defaultTZ} paramter will be used. |
| */ |
| public static Date parseXSTime( |
| String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) |
| throws DateParseException { |
| Matcher m = PATTERN_XS_TIME.matcher(timeStr); |
| if (!m.matches()) { |
| throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_TIME); |
| } |
| return parseTime_parseMatcher(m, defaultTZ, calToDateConverter); |
| } |
| |
| /** |
| * Same as {@link #parseXSTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 times. |
| */ |
| public static Date parseISO8601Time( |
| String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) |
| throws DateParseException { |
| Matcher m = PATTERN_ISO8601_EXTENDED_TIME.matcher(timeStr); |
| if (!m.matches()) { |
| m = PATTERN_ISO8601_BASIC_TIME.matcher(timeStr); |
| if (!m.matches()) { |
| throw new DateParseException("The value didn't match the expected pattern: " |
| + PATTERN_ISO8601_EXTENDED_TIME + " or " |
| + PATTERN_ISO8601_BASIC_TIME); |
| } |
| } |
| return parseTime_parseMatcher(m, defaultTZ, calToDateConverter); |
| } |
| |
| private static Date parseTime_parseMatcher( |
| Matcher m, TimeZone defaultTZ, |
| CalendarFieldsToDateConverter calToDateConverter) |
| throws DateParseException { |
| NullArgumentException.check("defaultTZ", defaultTZ); |
| try { |
| // ISO 8601 allows both 00:00 and 24:00, |
| // but Calendar.set(...) doesn't if the Calendar is not lenient. |
| int hours = groupToInt(m.group(1), "hour-of-day", 0, 24); |
| boolean hourWas24; |
| if (hours == 24) { |
| hours = 0; |
| hourWas24 = true; |
| // And a day will be added later... |
| } else { |
| hourWas24 = false; |
| } |
| |
| final String minutesStr = m.group(2); |
| int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0; |
| |
| final String secsStr = m.group(3); |
| // Allow 60 because of leap seconds |
| int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0; |
| |
| int millisecs = groupToMillisecond(m.group(4)); |
| |
| // As a time is just the distance from the beginning of the day, |
| // the time-zone offest should be 0 usually. |
| TimeZone tz = parseMatchingTimeZone(m.group(5), defaultTZ); |
| |
| // Continue handling the 24:00 special case |
| int day; |
| if (hourWas24) { |
| if (minutes == 0 && secs == 0 && millisecs == 0) { |
| day = 2; |
| } else { |
| throw new DateParseException( |
| "Hour 24 is only allowed in the case of " |
| + "midnight."); |
| } |
| } else { |
| day = 1; |
| } |
| |
| return calToDateConverter.calculate( |
| GregorianCalendar.AD, 1970, 0, day, hours, minutes, secs, millisecs, false, tz); |
| } catch (IllegalArgumentException e) { |
| // Calendar methods used to throw this for illegal dates. |
| throw new DateParseException( |
| "Unexpected time calculation faliure."); |
| } |
| } |
| |
| /** |
| * Parses an W3C XML Schema date-time string (not date or time). |
| * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. |
| * |
| * @param dateTimeStr the string to parse. |
| * @param defaultTZ used if the dateTime doesn't specify the |
| * time zone offset explicitly. Can't be {@code null}. |
| * |
| * @throws DateParseException if the dateTime is malformed. |
| */ |
| public static Date parseXSDateTime( |
| String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) |
| throws DateParseException { |
| Matcher m = PATTERN_XS_DATE_TIME.matcher(dateTimeStr); |
| if (!m.matches()) { |
| throw new DateParseException( |
| "The value didn't match the expected pattern: " + PATTERN_XS_DATE_TIME); |
| } |
| return parseDateTime_parseMatcher( |
| m, defaultTZ, true, calToDateConverter); |
| } |
| |
| /** |
| * Same as {@link #parseXSDateTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 format. |
| */ |
| public static Date parseISO8601DateTime( |
| String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) |
| throws DateParseException { |
| Matcher m = PATTERN_ISO8601_EXTENDED_DATE_TIME.matcher(dateTimeStr); |
| if (!m.matches()) { |
| m = PATTERN_ISO8601_BASIC_DATE_TIME.matcher(dateTimeStr); |
| if (!m.matches()) { |
| throw new DateParseException("The value (" + dateTimeStr + ") didn't match the expected pattern: " |
| + PATTERN_ISO8601_EXTENDED_DATE_TIME + " or " |
| + PATTERN_ISO8601_BASIC_DATE_TIME); |
| } |
| } |
| return parseDateTime_parseMatcher( |
| m, defaultTZ, false, calToDateConverter); |
| } |
| |
| private static Date parseDateTime_parseMatcher( |
| Matcher m, TimeZone defaultTZ, |
| boolean xsMode, |
| CalendarFieldsToDateConverter calToDateConverter) |
| throws DateParseException { |
| NullArgumentException.check("defaultTZ", defaultTZ); |
| try { |
| int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE); |
| |
| int era; |
| // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2. |
| // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based |
| // on the earlier version where 0000 didn't exist, and year -1 is BC 1. |
| if (year <= 0) { |
| era = GregorianCalendar.BC; |
| year = -year + (xsMode ? 0 : 1); |
| if (year == 0) { |
| throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED); |
| } |
| } else { |
| era = GregorianCalendar.AD; |
| } |
| |
| int month = groupToInt(m.group(2), "month", 1, 12) - 1; |
| int day = groupToInt(m.group(3), "day-of-month", 1, 31); |
| |
| // ISO 8601 allows both 00:00 and 24:00, |
| // but cal.set(...) doesn't if the Calendar is not lenient. |
| int hours = groupToInt(m.group(4), "hour-of-day", 0, 24); |
| boolean hourWas24; |
| if (hours == 24) { |
| hours = 0; |
| hourWas24 = true; |
| // And a day will be added later... |
| } else { |
| hourWas24 = false; |
| } |
| |
| final String minutesStr = m.group(5); |
| int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0; |
| |
| final String secsStr = m.group(6); |
| // Allow 60 because of leap seconds |
| int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0; |
| |
| int millisecs = groupToMillisecond(m.group(7)); |
| |
| // As a time is just the distance from the beginning of the day, |
| // the time-zone offest should be 0 usually. |
| TimeZone tz = parseMatchingTimeZone(m.group(8), defaultTZ); |
| |
| // Continue handling the 24:00 specail case |
| if (hourWas24) { |
| if (minutes != 0 || secs != 0 || millisecs != 0) { |
| throw new DateParseException( |
| "Hour 24 is only allowed in the case of " |
| + "midnight."); |
| } |
| } |
| |
| return calToDateConverter.calculate( |
| era, year, month, day, hours, minutes, secs, millisecs, hourWas24, tz); |
| } catch (IllegalArgumentException e) { |
| // Calendar methods used to throw this for illegal dates. |
| throw new DateParseException( |
| "Date-time calculation faliure. " |
| + "Probably the date-time is formally correct, but " |
| + "refers to an unexistent date-time " |
| + "(like February 30)."); |
| } |
| } |
| |
| /** |
| * Parses the time zone part from a W3C XML Schema date/time/dateTime. |
| * @throws DateParseException if the zone is malformed. |
| */ |
| public static TimeZone parseXSTimeZone(String timeZoneStr) |
| throws DateParseException { |
| Matcher m = PATTERN_XS_TIME_ZONE.matcher(timeZoneStr); |
| if (!m.matches()) { |
| throw new DateParseException( |
| "The time zone offset didn't match the expected pattern: " + PATTERN_XS_TIME_ZONE); |
| } |
| return parseMatchingTimeZone(timeZoneStr, null); |
| } |
| |
| private static int groupToInt(String g, String gName, |
| int min, int max) |
| throws DateParseException { |
| if (g == null) { |
| throw new DateParseException("The " + gName + " part " |
| + "is missing."); |
| } |
| |
| int start; |
| |
| // Remove minus sign, so we can remove the 0-s later: |
| boolean negative; |
| if (g.startsWith("-")) { |
| negative = true; |
| start = 1; |
| } else { |
| negative = false; |
| start = 0; |
| } |
| |
| // Remove leading 0-s: |
| while (start < g.length() - 1 && g.charAt(start) == '0') { |
| start++; |
| } |
| if (start != 0) { |
| g = g.substring(start); |
| } |
| |
| try { |
| int r = Integer.parseInt(g); |
| if (negative) { |
| r = -r; |
| } |
| if (r < min) { |
| throw new DateParseException("The " + gName + " part " |
| + "must be at least " + min + "."); |
| } |
| if (r > max) { |
| throw new DateParseException("The " + gName + " part " |
| + "can't be more than " + max + "."); |
| } |
| return r; |
| } catch (NumberFormatException e) { |
| throw new DateParseException("The " + gName + " part " |
| + "is a malformed integer."); |
| } |
| } |
| |
| private static TimeZone parseMatchingTimeZone( |
| String s, TimeZone defaultZone) |
| throws DateParseException { |
| if (s == null) { |
| return defaultZone; |
| } |
| if (s.equals("Z")) { |
| return DateUtil.UTC; |
| } |
| |
| StringBuilder sb = new StringBuilder(9); |
| sb.append("GMT"); |
| sb.append(s.charAt(0)); |
| |
| String h = s.substring(1, 3); |
| groupToInt(h, "offset-hours", 0, 23); |
| sb.append(h); |
| |
| String m; |
| int ln = s.length(); |
| if (ln > 3) { |
| int startIdx = s.charAt(3) == ':' ? 4 : 3; |
| m = s.substring(startIdx, startIdx + 2); |
| groupToInt(m, "offset-minutes", 0, 59); |
| sb.append(':'); |
| sb.append(m); |
| } |
| |
| return TimeZone.getTimeZone(sb.toString()); |
| } |
| |
| private static int groupToMillisecond(String g) |
| throws DateParseException { |
| if (g == null) { |
| return 0; |
| } |
| |
| if (g.length() > 3) { |
| g = g.substring(0, 3); |
| } |
| int i = groupToInt(g, "partial-seconds", 0, Integer.MAX_VALUE); |
| return g.length() == 1 ? i * 100 : (g.length() == 2 ? i * 10 : i); |
| } |
| |
| /** |
| * Used internally by {@link DateUtil}; don't use its implementations for |
| * anything else. |
| */ |
| public interface DateToISO8601CalendarFactory { |
| |
| /** |
| * Returns a {@link GregorianCalendar} with the desired time zone and |
| * time and US locale. The returned calendar is used as read-only. |
| * It must be guaranteed that within a thread the instance returned last time |
| * is not in use anymore when this method is called again. |
| */ |
| GregorianCalendar get(TimeZone tz, Date date); |
| |
| } |
| |
| /** |
| * Used internally by {@link DateUtil}; don't use its implementations for anything else. |
| */ |
| public interface CalendarFieldsToDateConverter { |
| |
| /** |
| * Calculates the {@link Date} from the specified calendar fields. |
| */ |
| Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs, |
| boolean addOneDay, |
| TimeZone tz); |
| |
| } |
| |
| /** |
| * Non-thread-safe factory that hard-references a calendar internally. |
| */ |
| public static final class TrivialDateToISO8601CalendarFactory |
| implements DateToISO8601CalendarFactory { |
| |
| private GregorianCalendar calendar; |
| private TimeZone lastlySetTimeZone; |
| |
| public GregorianCalendar get(TimeZone tz, Date date) { |
| if (calendar == null) { |
| calendar = new GregorianCalendar(tz, Locale.US); |
| calendar.setGregorianChange(new Date(Long.MIN_VALUE)); // never use Julian calendar |
| } else { |
| // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone. |
| if (lastlySetTimeZone != tz) { // Deliberately `!=` instead of `!<...>.equals()` |
| calendar.setTimeZone(tz); |
| lastlySetTimeZone = tz; |
| } |
| } |
| calendar.setTime(date); |
| return calendar; |
| } |
| |
| } |
| |
| /** |
| * Non-thread-safe implementation that hard-references a calendar internally. |
| */ |
| public static final class TrivialCalendarFieldsToDateConverter |
| implements CalendarFieldsToDateConverter { |
| |
| private GregorianCalendar calendar; |
| private TimeZone lastlySetTimeZone; |
| |
| public Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs, |
| boolean addOneDay, TimeZone tz) { |
| if (calendar == null) { |
| calendar = new GregorianCalendar(tz, Locale.US); |
| calendar.setLenient(false); |
| calendar.setGregorianChange(new Date(Long.MIN_VALUE)); // never use Julian calendar |
| } else { |
| // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone. |
| if (lastlySetTimeZone != tz) { // Deliberately `!=` instead of `!<...>.equals()` |
| calendar.setTimeZone(tz); |
| lastlySetTimeZone = tz; |
| } |
| } |
| |
| calendar.set(Calendar.ERA, era); |
| calendar.set(Calendar.YEAR, year); |
| calendar.set(Calendar.MONTH, month); |
| calendar.set(Calendar.DAY_OF_MONTH, day); |
| calendar.set(Calendar.HOUR_OF_DAY, hours); |
| calendar.set(Calendar.MINUTE, minutes); |
| calendar.set(Calendar.SECOND, secs); |
| calendar.set(Calendar.MILLISECOND, millisecs); |
| if (addOneDay) { |
| calendar.add(Calendar.DAY_OF_MONTH, 1); |
| } |
| |
| return calendar.getTime(); |
| } |
| |
| } |
| |
| public static final class DateParseException extends ParseException { |
| |
| public DateParseException(String message) { |
| super(message, 0); |
| } |
| |
| } |
| |
| } |