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;
-    }
-    
-}


Reply via email to