You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/08/08 17:56:56 UTC
[06/17] incubator-freemarker git commit: Renamed XxxUtil classes to
XxxUtils, as this convention is more widespread nowadays.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ebb39b84/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtils.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtils.java
new file mode 100644
index 0000000..5801f2e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtils.java
@@ -0,0 +1,220 @@
+/*
+ * 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 org.apache.freemarker.core.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * {@link Collection} and {@link Map}-related utilities.
+ */
+public class _CollectionUtils {
+
+ private _CollectionUtils() { }
+
+ public static final Object[] EMPTY_OBJECT_ARRAY = new Object[] { };
+ public static final Class[] EMPTY_CLASS_ARRAY = new Class[] { };
+ public static final String[] EMPTY_STRING_ARRAY = new String[] { };
+ public static final char[] EMPTY_CHAR_ARRAY = new char[] { };
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static <T> List<? extends T> safeCastList(
+ String argName, List list,
+ Class<T> itemClass, boolean allowNullItem) {
+ if (list == null) {
+ return null;
+ }
+ for (int i = 0; i < list.size(); i++) {
+ Object it = list.get(i);
+ if (!itemClass.isInstance(it)) {
+ if (it == null) {
+ if (!allowNullItem) {
+ throw new IllegalArgumentException(
+ (argName != null ? "Invalid value for argument \"" + argName + "\"" : "")
+ + "List item at index " + i + " is null");
+ }
+ } else {
+ throw new IllegalArgumentException(
+ (argName != null ? "Invalid value for argument \"" + argName + "\"" : "")
+ + "List item at index " + i + " is not instance of " + itemClass.getName() + "; "
+ + "its class is " + it.getClass().getName() + ".");
+ }
+ }
+ }
+
+ return list;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static <K, V> Map<? extends K, ? extends V> safeCastMap(
+ String argName, Map map,
+ Class<K> keyClass, boolean allowNullKey,
+ Class<V> valueClass, boolean allowNullValue) {
+ if (map == null) {
+ return null;
+ }
+ for (Map.Entry<?, ?> ent : ((Map<?, ?>) map).entrySet()) {
+ Object key = ent.getKey();
+ if (!keyClass.isInstance(key)) {
+ if (key == null) {
+ if (!allowNullKey) {
+ throw new IllegalArgumentException(
+ (argName != null ? "Invalid value for argument \"" + argName + "\": " : "")
+ + "The Map contains null key");
+ }
+ } else {
+ throw new IllegalArgumentException(
+ (argName != null ? "Invalid value for argument \"" + argName + "\": " : "")
+ + "The Map contains a key that's not instance of " + keyClass.getName() +
+ "; its class is " + key.getClass().getName() + ".");
+ }
+ }
+
+ Object value = ent.getValue();
+ if (!valueClass.isInstance(value)) {
+ if (value == null) {
+ if (!allowNullValue) {
+ throw new IllegalArgumentException(
+ (argName != null ? "Invalid value for argument \"" + argName + "\"" : "")
+ + "The Map contains null value");
+ }
+ } else {
+ throw new IllegalArgumentException(
+ (argName != null ? "Invalid value for argument \"" + argName + "\"" : "")
+ + "The Map contains a value that's not instance of " + valueClass.getName() +
+ "; its class is " + value.getClass().getName() + ".");
+ }
+ }
+ }
+
+ return map;
+ }
+
+ private static final Class<?> UNMODIFIABLE_MAP_CLASS_1 = Collections.emptyMap().getClass();
+ private static final Class<?> UNMODIFIABLE_MAP_CLASS_2 = Collections.unmodifiableMap(
+ new HashMap<Object, Object> (1)).getClass();
+ private static final Class<?> UNMODIFIABLE_LIST_CLASS_1 = Collections.emptyList().getClass();
+ private static final Class<?> UNMODIFIABLE_LIST_CLASS_2 = Collections.unmodifiableList(
+ new ArrayList<Object>(1)).getClass();
+
+ public static boolean isMapKnownToBeUnmodifiable(Map<?, ?> map) {
+ if (map == null) {
+ return true;
+ }
+ Class<? extends Map> mapClass = map.getClass();
+ return mapClass == UNMODIFIABLE_MAP_CLASS_1 || mapClass == UNMODIFIABLE_MAP_CLASS_2;
+ }
+
+ public static boolean isListKnownToBeUnmodifiable(List<?> list) {
+ if (list == null) {
+ return true;
+ }
+ Class<? extends List> listClass = list.getClass();
+ return listClass == UNMODIFIABLE_LIST_CLASS_1 || listClass == UNMODIFIABLE_LIST_CLASS_2;
+ }
+
+ /**
+ * Optimized version of {@link Collections#unmodifiableMap(Map)} (avoids needless wrapping).
+ *
+ * @param map The map to return or wrap if not already unmodifiable, or {@code null} which is silently bypassed.
+ */
+ public static <K, V> Map<K, V> unmodifiableMap(Map<K, V> map) {
+ return isMapKnownToBeUnmodifiable(map) ? map : Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * Adds two {@link Map}-s (keeping the iteration order); assuming the inputs are already unmodifiable and
+ * unchanging, it returns an unmodifiable and unchanging {@link Map} itself.
+ */
+ public static <K,V> Map<K,V> mergeImmutableMaps(Map<K,V> m1, Map<K,V> m2, boolean keepOriginalOrder) {
+ if (m1 == null) return m2;
+ if (m2 == null) return m1;
+ if (m1.isEmpty()) return m2;
+ if (m2.isEmpty()) return m1;
+
+ Map<K, V> mergedM = keepOriginalOrder
+ ? new LinkedHashMap<K, V>((m1.size() + m2.size()) * 4 / 3 + 1, 0.75f)
+ : new HashMap<K, V>((m1.size() + m2.size()) * 4 / 3 + 1, 0.75f);
+ mergedM.putAll(m1);
+ if (keepOriginalOrder) {
+ for (K m2Key : m2.keySet()) {
+ mergedM.remove(m2Key); // So that duplicate keys are moved after m1 keys
+ }
+ }
+ mergedM.putAll(m2);
+ return Collections.unmodifiableMap(mergedM);
+ }
+
+ /**
+ * Adds multiple {@link List}-s; assuming the inputs are already unmodifiable and unchanging, it returns an
+ * unmodifiable and unchanging {@link List} itself.
+ */
+ public static <T> List<T> mergeImmutableLists(boolean skipDuplicatesInList1, List<T> ... lists) {
+ if (lists == null || lists.length == 0) {
+ return null;
+ }
+
+ if (lists.length == 1) {
+ return mergeImmutableLists(lists[0], null, skipDuplicatesInList1);
+ } else if (lists.length == 2) {
+ return mergeImmutableLists(lists[0], lists[1], skipDuplicatesInList1);
+ } else {
+ List<T> [] reducedLists = new List[lists.length - 1];
+ reducedLists[0] = mergeImmutableLists(lists[0], lists[1], skipDuplicatesInList1);
+ System.arraycopy(lists, 2, reducedLists, 1, lists.length - 2);
+ return mergeImmutableLists(skipDuplicatesInList1, reducedLists);
+ }
+ }
+
+ /**
+ * Adds two {@link List}-s; assuming the inputs are already unmodifiable and unchanging, it returns an
+ * unmodifiable and unchanging {@link List} itself.
+ */
+ public static <T> List<T> mergeImmutableLists(List<T> list1, List<T> list2,
+ boolean skipDuplicatesInList1) {
+ if (list1 == null) return list2;
+ if (list2 == null) return list1;
+ if (list1.isEmpty()) return list2;
+ if (list2.isEmpty()) return list1;
+
+ ArrayList<T> mergedList = new ArrayList<>(list1.size() + list2.size());
+ if (skipDuplicatesInList1) {
+ Set<T> list2Set = new HashSet<>(list2);
+ for (T it : list1) {
+ if (!list2Set.contains(it)) {
+ mergedList.add(it);
+ }
+ }
+ } else {
+ mergedList.addAll(list1);
+ }
+ mergedList.addAll(list2);
+ return Collections.unmodifiableList(mergedList);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ebb39b84/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java
deleted file mode 100644
index 0cf2fea..0000000
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtil.java
+++ /dev/null
@@ -1,914 +0,0 @@
-/*
- * 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 org.apache.freemarker.core.util;
-
-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;
-
-/**
- * Don't use this; used internally by FreeMarker, might changes without notice.
- * 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 invoke 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;
-
- @Override
- 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;
-
- @Override
- 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);
- }
-
- }
-
-}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ebb39b84/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtils.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtils.java
new file mode 100644
index 0000000..60201b6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_DateUtils.java
@@ -0,0 +1,914 @@
+/*
+ * 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 org.apache.freemarker.core.util;
+
+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;
+
+/**
+ * Don't use this; used internally by FreeMarker, might changes without notice.
+ * Date and time related utilities.
+ */
+public class _DateUtils {
+
+ /**
+ * 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 _DateUtils() {
+ // 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 _DateUtils#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 invoke 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 _DateUtils.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 _DateUtils}; 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 _DateUtils}; 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;
+
+ @Override
+ 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;
+
+ @Override
+ 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);
+ }
+
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ebb39b84/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
index 96a9583..88efaca 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
@@ -37,7 +37,7 @@ public final class _JavaVersions {
private static final boolean IS_AT_LEAST_8;
static {
boolean result = false;
- String vStr = _SecurityUtil.getSystemProperty("java.version", null);
+ String vStr = _SecurityUtils.getSystemProperty("java.version", null);
if (vStr != null) {
try {
Version v = new Version(vStr);
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ebb39b84/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java
deleted file mode 100644
index 2f09c88..0000000
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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 org.apache.freemarker.core.util;
-
-import java.util.Locale;
-
-/**
- * For internal use only; don't depend on this, there's no backward compatibility guarantee at all!
- */
-public class _LocaleUtil {
-
- /**
- * Returns a locale that's one less specific, or {@code null} if there's no less specific locale.
- */
- public static Locale getLessSpecificLocale(Locale locale) {
- String country = locale.getCountry();
- if (locale.getVariant().length() != 0) {
- String language = locale.getLanguage();
- return country != null ? new Locale(language, country) : new Locale(language);
- }
- if (country.length() != 0) {
- return new Locale(locale.getLanguage());
- }
- return null;
- }
-
-}