freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [05/11] incubator-freemarker git commit: Marked most static utility classes as internal, and renamed them to start with "_" (for example StringUtils was renamed to _StringUtil, thus people won't accidentally use it when they wanted to autocomplete to Apa
Date Fri, 17 Feb 2017 14:06:06 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java b/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java
new file mode 100644
index 0000000..7617407
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_ArrayIterator.java
@@ -0,0 +1,51 @@
+/*
+ * 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.Iterator;
+import java.util.NoSuchElementException;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _ArrayIterator implements Iterator {
+
+    private final Object[] array;
+    private int nextIndex;
+
+    public _ArrayIterator(Object[] array) {
+        this.array = array;
+        this.nextIndex = 0;
+    }
+
+    public boolean hasNext() {
+        return nextIndex < array.length;
+    }
+
+    public Object next() {
+        if (nextIndex >= array.length) {
+            throw new NoSuchElementException();
+        }
+        return array[nextIndex++];
+    }
+
+    public void remove() {
+        throw new UnsupportedOperationException();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java b/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java
new file mode 100644
index 0000000..48ae23f
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_ClassUtil.java
@@ -0,0 +1,385 @@
+/*
+ * 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.HashSet;
+import java.util.Set;
+
+import org.apache.freemarker.core.ast.Environment;
+import org.apache.freemarker.core.ast.Macro;
+import org.apache.freemarker.core.ast.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.AdapterTemplateModel;
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateMethodModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNodeModelEx;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.model.WrapperTemplateModel;
+import org.apache.freemarker.core.model.impl.beans.BeanModel;
+import org.apache.freemarker.core.model.impl.beans.BooleanModel;
+import org.apache.freemarker.core.model.impl.beans.CollectionModel;
+import org.apache.freemarker.core.model.impl.beans.DateModel;
+import org.apache.freemarker.core.model.impl.beans.EnumerationModel;
+import org.apache.freemarker.core.model.impl.beans.IteratorModel;
+import org.apache.freemarker.core.model.impl.beans.MapModel;
+import org.apache.freemarker.core.model.impl.beans.NumberModel;
+import org.apache.freemarker.core.model.impl.beans.OverloadedMethodsModel;
+import org.apache.freemarker.core.model.impl.beans.SimpleMethodModel;
+import org.apache.freemarker.core.model.impl.beans.StringModel;
+
+public class _ClassUtil {
+    
+    private static final String ORG_APACHE_FREEMARKER = "org.apache.freemarker.";
+    private static final String ORG_APACHE_FREEMARKER_CORE = "org.apache.freemarker.core.";
+    private static final String ORG_APACHE_FREEMARKER_CORE_TEMPLATERESOLVER
+            = "org.apache.freemarker.core.templateresolver.";
+    private static final String ORG_APACHE_FREEMARKER_CORE_MODEL = "org.apache.freemarker.core.model.";
+
+    private _ClassUtil() {
+    }
+    
+    /**
+     * Similar to {@link Class#forName(java.lang.String)}, but attempts to load
+     * through the thread context class loader. Only if thread context class
+     * loader is inaccessible, or it can't find the class will it attempt to
+     * fall back to the class loader that loads the FreeMarker classes.
+     */
+    public static Class forName(String className)
+    throws ClassNotFoundException {
+        try {
+            ClassLoader ctcl = Thread.currentThread().getContextClassLoader();
+            if (ctcl != null) {  // not null: we don't want to fall back to the bootstrap class loader
+                return Class.forName(className, true, ctcl);
+            }
+        } catch (ClassNotFoundException e) {
+            ;// Intentionally ignored
+        } catch (SecurityException e) {
+            ;// Intentionally ignored
+        }
+        // Fall back to the defining class loader of the FreeMarker classes 
+        return Class.forName(className);
+    }
+    
+    /**
+     * Same as {@link #getShortClassName(Class, boolean) getShortClassName(pClass, false)}.
+     * 
+     * @since 2.3.20
+     */
+    public static String getShortClassName(Class pClass) {
+        return getShortClassName(pClass, false);
+    }
+    
+    /**
+     * Returns a class name without "java.lang." and "java.util." prefix, also shows array types in a format like
+     * {@code int[]}; useful for printing class names in error messages.
+     * 
+     * @param pClass can be {@code null}, in which case the method returns {@code null}.
+     * @param shortenFreeMarkerClasses if {@code true}, it will also shorten FreeMarker class names. The exact rules
+     *     aren't specified and might change over time, but right now, {@link NumberModel} for
+     *     example becomes to {@code f.e.b.NumberModel}. 
+     * 
+     * @since 2.3.20
+     */
+    public static String getShortClassName(Class pClass, boolean shortenFreeMarkerClasses) {
+        if (pClass == null) {
+            return null;
+        } else if (pClass.isArray()) {
+            return getShortClassName(pClass.getComponentType()) + "[]";
+        } else {
+            String cn = pClass.getName();
+            if (cn.startsWith("java.lang.") || cn.startsWith("java.util.")) {
+                return cn.substring(10);
+            } else {
+                if (shortenFreeMarkerClasses) {
+                    if (cn.startsWith(ORG_APACHE_FREEMARKER_CORE_MODEL)) {
+                        return "o.a.f.c.m." + cn.substring(ORG_APACHE_FREEMARKER_CORE_MODEL.length());
+                    } else if (cn.startsWith(ORG_APACHE_FREEMARKER_CORE_TEMPLATERESOLVER)) {
+                        return "o.a.f.c.t." + cn.substring(ORG_APACHE_FREEMARKER_CORE_TEMPLATERESOLVER.length());
+                    } else if (cn.startsWith(ORG_APACHE_FREEMARKER_CORE)) {
+                        return "o.a.f.c." + cn.substring(ORG_APACHE_FREEMARKER_CORE.length());
+                    } else if (cn.startsWith(ORG_APACHE_FREEMARKER)) {
+                        return "o.a.f." + cn.substring(ORG_APACHE_FREEMARKER.length());
+                    }
+                    // Falls through
+                }
+                return cn;
+            }
+        }
+    }
+
+    /**
+     * Same as {@link #getShortClassNameOfObject(Object, boolean) getShortClassNameOfObject(pClass, false)}.
+     * 
+     * @since 2.3.20
+     */
+    public static String getShortClassNameOfObject(Object obj) {
+        return getShortClassNameOfObject(obj, false);
+    }
+    
+    /**
+     * {@link #getShortClassName(Class, boolean)} called with {@code object.getClass()}, but returns the fictional
+     * class name {@code Null} for a {@code null} value.
+     * 
+     * @since 2.3.20
+     */
+    public static String getShortClassNameOfObject(Object obj, boolean shortenFreeMarkerClasses) {
+        if (obj == null) {
+            return "Null";
+        } else {
+            return _ClassUtil.getShortClassName(obj.getClass(), shortenFreeMarkerClasses);
+        }
+    }
+
+    /**
+     * Returns the {@link TemplateModel} interface that is the most characteristic of the object, or {@code null}.
+     */
+    private static Class getPrimaryTemplateModelInterface(TemplateModel tm) {
+        if (tm instanceof BeanModel) {
+            if (tm instanceof CollectionModel) {
+                return TemplateSequenceModel.class;
+            } else if (tm instanceof IteratorModel || tm instanceof EnumerationModel) {
+                return TemplateCollectionModel.class;
+            } else if (tm instanceof MapModel) {
+                return TemplateHashModelEx.class;
+            } else if (tm instanceof NumberModel) {
+                return TemplateNumberModel.class;
+            } else if (tm instanceof BooleanModel) {
+                return TemplateBooleanModel.class;
+            } else if (tm instanceof DateModel) {
+                return TemplateDateModel.class;
+            } else if (tm instanceof StringModel) {
+                Object wrapped = ((BeanModel) tm).getWrappedObject();
+                return wrapped instanceof String
+                        ? TemplateScalarModel.class
+                        : (tm instanceof TemplateHashModelEx ? TemplateHashModelEx.class : null);
+            } else {
+                return null;
+            }
+        } else if (tm instanceof SimpleMethodModel || tm instanceof OverloadedMethodsModel) {
+            return TemplateMethodModelEx.class;
+        } else {
+            return null;
+        }
+    }
+
+    private static void appendTemplateModelTypeName(StringBuilder sb, Set typeNamesAppended, Class cl) {
+        int initalLength = sb.length();
+        
+        if (TemplateNodeModelEx.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "extended node");
+        } else if (TemplateNodeModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "node");
+        }
+        
+        if (TemplateDirectiveModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "directive");
+        } else if (TemplateTransformModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "transform");
+        }
+        
+        if (TemplateSequenceModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "sequence");
+        } else if (TemplateCollectionModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended,
+                    TemplateCollectionModelEx.class.isAssignableFrom(cl) ? "extended_collection" : "collection");
+        } else if (TemplateModelIterator.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "iterator");
+        }
+        
+        if (TemplateMethodModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "method");
+        }
+        
+        if (Environment.Namespace.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "namespace");
+        } else if (TemplateHashModelEx.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "extended_hash");
+        } else if (TemplateHashModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "hash");
+        }
+        
+        if (TemplateNumberModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "number");
+        }
+        
+        if (TemplateDateModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "date_or_time_or_datetime");
+        }
+        
+        if (TemplateBooleanModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "boolean");
+        }
+        
+        if (TemplateScalarModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "string");
+        }
+        
+        if (TemplateMarkupOutputModel.class.isAssignableFrom(cl)) {
+            appendTypeName(sb, typeNamesAppended, "markup_output");
+        }
+        
+        if (sb.length() == initalLength) {
+            appendTypeName(sb, typeNamesAppended, "misc_template_model");
+        }
+    }
+    
+    private static Class getUnwrappedClass(TemplateModel tm) {
+        Object unwrapped;
+        try {
+            if (tm instanceof WrapperTemplateModel) {
+                unwrapped = ((WrapperTemplateModel) tm).getWrappedObject();
+            } else if (tm instanceof AdapterTemplateModel) {
+                unwrapped = ((AdapterTemplateModel) tm).getAdaptedObject(Object.class);
+            } else {
+                unwrapped = null;
+            }
+        } catch (Throwable e) {
+            unwrapped = null;
+        }
+        return unwrapped != null ? unwrapped.getClass() : null;
+    }
+
+    private static void appendTypeName(StringBuilder sb, Set typeNamesAppended, String name) {
+        if (!typeNamesAppended.contains(name)) {
+            if (sb.length() != 0) sb.append("+");
+            sb.append(name);
+            typeNamesAppended.add(name);
+        }
+    }
+
+    /**
+     * Returns the type description of a value with FTL terms (not plain class name), as it should be used in
+     * type-related error messages and for debugging purposes. The exact format is not specified and might change over
+     * time, but currently it's something like {@code "string (wrapper: f.t.SimpleScalar)"} or
+     * {@code "sequence+hash+string (ArrayList wrapped into f.e.b.CollectionModel)"}.
+     * 
+     * @since 2.3.20
+     */
+    public static String getFTLTypeDescription(TemplateModel tm) {
+        if (tm == null) {
+            return "Null";
+        } else {
+            Set typeNamesAppended = new HashSet();
+            
+            StringBuilder sb = new StringBuilder();
+    
+            Class primaryInterface = getPrimaryTemplateModelInterface(tm);
+            if (primaryInterface != null) {
+                appendTemplateModelTypeName(sb, typeNamesAppended, primaryInterface);
+            }
+    
+            if (tm instanceof Macro) {
+                appendTypeName(sb, typeNamesAppended, ((Macro) tm).isFunction() ? "function" : "macro");
+            }
+            
+            appendTemplateModelTypeName(sb, typeNamesAppended, tm.getClass());
+            
+            String javaClassName;
+            Class unwrappedClass = getUnwrappedClass(tm);
+            if (unwrappedClass != null) {
+                javaClassName = getShortClassName(unwrappedClass, true);
+            } else {
+                javaClassName = null;
+            }
+            
+            sb.append(" (");
+            String modelClassName = getShortClassName(tm.getClass(), true);
+            if (javaClassName == null) {
+                sb.append("wrapper: ");
+                sb.append(modelClassName);
+            } else {
+                sb.append(javaClassName);
+                sb.append(" wrapped into ");
+                sb.append(modelClassName);
+            }
+            sb.append(")");
+    
+            return sb.toString();
+        }
+    }
+    
+    /**
+     * Gets the wrapper class for a primitive class, like {@link Integer} for {@code int}, also returns {@link Void}
+     * for {@code void}. 
+     * 
+     * @param primitiveClass A {@link Class} like {@code int.type}, {@code boolean.type}, etc. If it's not a primitive
+     *     class, or it's {@code null}, then the parameter value is returned as is. Note that performance-wise the
+     *     method assumes that it's a primitive class.
+     *     
+     * @since 2.3.21
+     */
+    public static Class primitiveClassToBoxingClass(Class primitiveClass) {
+        // Tried to sort these with decreasing frequency in API-s:
+        if (primitiveClass == int.class) return Integer.class;
+        if (primitiveClass == boolean.class) return Boolean.class;
+        if (primitiveClass == long.class) return Long.class;
+        if (primitiveClass == double.class) return Double.class;
+        if (primitiveClass == char.class) return Character.class;
+        if (primitiveClass == float.class) return Float.class;
+        if (primitiveClass == byte.class) return Byte.class;
+        if (primitiveClass == short.class) return Short.class;
+        if (primitiveClass == void.class) return Void.class;  // not really a primitive, but we normalize it
+        return primitiveClass;
+    }
+
+    /**
+     * The exact reverse of {@link #primitiveClassToBoxingClass}.
+     *     
+     * @since 2.3.21
+     */
+    public static Class boxingClassToPrimitiveClass(Class boxingClass) {
+        // Tried to sort these with decreasing frequency in API-s:
+        if (boxingClass == Integer.class) return int.class;
+        if (boxingClass == Boolean.class) return boolean.class;
+        if (boxingClass == Long.class) return long.class;
+        if (boxingClass == Double.class) return double.class;
+        if (boxingClass == Character.class) return char.class;
+        if (boxingClass == Float.class) return float.class;
+        if (boxingClass == Byte.class) return byte.class;
+        if (boxingClass == Short.class) return short.class;
+        if (boxingClass == Void.class) return void.class;  // not really a primitive, but we normalize to it
+        return boxingClass;
+    }
+    
+    /**
+     * Tells if a type is numerical; works both for primitive types and classes.
+     * 
+     * @param type can't be {@code null}
+     * 
+     * @since 2.3.21
+     */
+    public static boolean isNumerical(Class type) {
+        return Number.class.isAssignableFrom(type)
+                || type.isPrimitive() && type != Boolean.TYPE && type != Character.TYPE && type != Void.TYPE;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java b/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java
new file mode 100644
index 0000000..3772215
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _CollectionUtil {
+    
+    private _CollectionUtil() { }
+
+    public static final Object[] EMPTY_OBJECT_ARRAY = new Object[] { };
+
+    public static final Class[] EMPTY_CLASS_ARRAY = new Class[] { };
+
+    /**
+     * @since 2.3.22
+     */
+    public static final char[] EMPTY_CHAR_ARRAY = new char[] { };
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_DateUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_DateUtil.java b/src/main/java/org/apache/freemarker/core/util/_DateUtil.java
new file mode 100644
index 0000000..b79c0bd
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_DateUtil.java
@@ -0,0 +1,912 @@
+/*
+ * 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 create the calendar used
+     *        internally for calculations. The point of this parameter is that
+     *        creating a new calendar is relatively expensive, so it's desirable
+     *        to reuse calendars and only set their time and zone. (This was
+     *        tested on Sun JDK 1.6 x86 Win, where it gave 2x-3x speedup.) 
+     */
+    public static String dateToISO8601String(
+            Date date,
+            boolean datePart, boolean timePart, boolean offsetPart,
+            int accuracy,
+            TimeZone timeZone,
+            DateToISO8601CalendarFactory calendarFactory) {
+        return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, false, calendarFactory);
+    }
+
+    /**
+     * Same as {@link #dateToISO8601String}, but gives XML Schema compliant format.
+     */
+    public static String dateToXSString(
+            Date date,
+            boolean datePart, boolean timePart, boolean offsetPart,
+            int accuracy,
+            TimeZone timeZone,
+            DateToISO8601CalendarFactory calendarFactory) {
+        return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, true, calendarFactory);
+    }
+    
+    private static String dateToString(
+            Date date,
+            boolean datePart, boolean timePart, boolean offsetPart,
+            int accuracy,
+            TimeZone timeZone, boolean xsMode,
+            DateToISO8601CalendarFactory calendarFactory) {
+        if (!xsMode && !timePart && offsetPart) {
+            throw new IllegalArgumentException(
+                    "ISO 8601:2004 doesn't specify any formats where the "
+                    + "offset is shown but the time isn't.");
+        }
+        
+        if (timeZone == null) {
+            timeZone = UTC;
+        }
+        
+        GregorianCalendar cal = calendarFactory.get(timeZone, date);
+
+        int maxLength;
+        if (!timePart) {
+            maxLength = 10 + (xsMode ? 6 : 0);  // YYYY-MM-DD+00:00
+        } else {
+            if (!datePart) {
+                maxLength = 12 + 6;  // HH:MM:SS.mmm+00:00
+            } else {
+                maxLength = 10 + 1 + 12 + 6;
+            }
+        }
+        char[] res = new char[maxLength];
+        int dstIdx = 0;
+        
+        if (datePart) {
+            int x = cal.get(Calendar.YEAR);
+            if (x > 0 && cal.get(Calendar.ERA) == GregorianCalendar.BC) {
+                x = -x + (xsMode ? 0 : 1);
+            }
+            if (x >= 0 && x < 9999) {
+                res[dstIdx++] = (char) ('0' + x / 1000);
+                res[dstIdx++] = (char) ('0' + x % 1000 / 100);
+                res[dstIdx++] = (char) ('0' + x % 100 / 10);
+                res[dstIdx++] = (char) ('0' + x % 10);
+            } else {
+                String yearString = String.valueOf(x);
+                
+                // Re-allocate buffer:
+                maxLength = maxLength - 4 + yearString.length();
+                res = new char[maxLength];
+                
+                for (int i = 0; i < yearString.length(); i++) {
+                    res[dstIdx++] = yearString.charAt(i);
+                }
+            }
+    
+            res[dstIdx++] = '-';
+            
+            x = cal.get(Calendar.MONTH) + 1;
+            dstIdx = append00(res, dstIdx, x);
+    
+            res[dstIdx++] = '-';
+            
+            x = cal.get(Calendar.DAY_OF_MONTH);
+            dstIdx = append00(res, dstIdx, x);
+
+            if (timePart) {
+                res[dstIdx++] = 'T';
+            }
+        }
+
+        if (timePart) {
+            int x = cal.get(Calendar.HOUR_OF_DAY);
+            dstIdx = append00(res, dstIdx, x);
+    
+            if (accuracy >= ACCURACY_MINUTES) {
+                res[dstIdx++] = ':';
+        
+                x = cal.get(Calendar.MINUTE);
+                dstIdx = append00(res, dstIdx, x);
+        
+                if (accuracy >= ACCURACY_SECONDS) {
+                    res[dstIdx++] = ':';
+            
+                    x = cal.get(Calendar.SECOND);
+                    dstIdx = append00(res, dstIdx, x);
+            
+                    if (accuracy >= ACCURACY_MILLISECONDS) {
+                        x = cal.get(Calendar.MILLISECOND);
+                        int forcedDigits = accuracy == ACCURACY_MILLISECONDS_FORCED ? 3 : 0;
+                        if (x != 0 || forcedDigits != 0) {
+                            if (x > 999) {
+                                // Shouldn't ever happen...
+                                throw new RuntimeException(
+                                        "Calendar.MILLISECOND > 999");
+                            }
+                            res[dstIdx++] = '.';
+                            do {
+                                res[dstIdx++] = (char) ('0' + (x / 100));
+                                forcedDigits--;
+                                x = x % 100 * 10;
+                            } while (x != 0 || forcedDigits > 0);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (offsetPart) {
+            if (timeZone == UTC) {
+                res[dstIdx++] = 'Z';
+            } else {
+                int dt = timeZone.getOffset(date.getTime());
+                boolean positive;
+                if (dt < 0) {
+                    positive = false;
+                    dt = -dt;
+                } else {
+                    positive = true;
+                }
+                
+                dt /= 1000;
+                int offS = dt % 60;
+                dt /= 60;
+                int offM = dt % 60;
+                dt /= 60;
+                int offH = dt;
+                
+                if (offS == 0 && offM == 0 && offH == 0) {
+                    res[dstIdx++] = 'Z';
+                } else {
+                    res[dstIdx++] = positive ? '+' : '-';
+                    dstIdx = append00(res, dstIdx, offH);
+                    res[dstIdx++] = ':';
+                    dstIdx = append00(res, dstIdx, offM);
+                    if (offS != 0) {
+                        res[dstIdx++] = ':';
+                        dstIdx = append00(res, dstIdx, offS);
+                    }
+                }
+            }
+        }
+        
+        return new String(res, 0, dstIdx);
+    }
+    
+    /** 
+     * Appends a number between 0 and 99 padded to 2 digits.
+     */
+    private static int append00(char[] res, int dstIdx, int x) {
+        res[dstIdx++] = (char) ('0' + x / 10);
+        res[dstIdx++] = (char) ('0' + x % 10);
+        return dstIdx;
+    }
+    
+    /**
+     * Parses an W3C XML Schema date string (not time or date-time).
+     * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. 
+     * 
+     * @param dateStr the string to parse. 
+     * @param defaultTimeZone used if the date doesn't specify the
+     *     time zone offset explicitly. Can't be {@code null}.
+     * @param calToDateConverter Used internally to calculate the result from the calendar field values.
+     *     If you don't have a such object around, you can just use
+     *     {@code new }{@link TrivialCalendarFieldsToDateConverter}{@code ()}. 
+     * 
+     * @throws DateParseException if the date is malformed, or if the time
+     *     zone offset is unspecified and the {@code defaultTimeZone} is
+     *     {@code null}.
+     */
+    public static Date parseXSDate(
+            String dateStr, TimeZone defaultTimeZone,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_XS_DATE.matcher(dateStr);
+        if (!m.matches()) {
+            throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_DATE); 
+        }
+        return parseDate_parseMatcher(
+                m, defaultTimeZone, true, calToDateConverter);
+    }
+
+    /**
+     * Same as {@link #parseXSDate(String, TimeZone, CalendarFieldsToDateConverter)}, but for ISO 8601 dates.
+     */
+    public static Date parseISO8601Date(
+            String dateStr, TimeZone defaultTimeZone,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_ISO8601_EXTENDED_DATE.matcher(dateStr);
+        if (!m.matches()) {
+            m = PATTERN_ISO8601_BASIC_DATE.matcher(dateStr);
+            if (!m.matches()) {
+                throw new DateParseException("The value didn't match the expected pattern: "
+                            + PATTERN_ISO8601_EXTENDED_DATE + " or "
+                            + PATTERN_ISO8601_BASIC_DATE);
+            }
+        }
+        return parseDate_parseMatcher(
+                m, defaultTimeZone, false, calToDateConverter);
+    }
+    
+    private static Date parseDate_parseMatcher(
+            Matcher m, TimeZone defaultTZ,
+            boolean xsMode,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        _NullArgumentException.check("defaultTZ", defaultTZ);
+        try {
+            int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE);
+            
+            int era;
+            // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2.
+            // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based
+            // on the earlier version where 0000 didn't exist, and year -1 is BC 1.
+            if (year <= 0) {
+                era = GregorianCalendar.BC;
+                year = -year + (xsMode ? 0 : 1);
+                if (year == 0) {
+                    throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED);
+                }
+            } else {
+                era = GregorianCalendar.AD;
+            }
+            
+            int month = groupToInt(m.group(2), "month", 1, 12) - 1;
+            int day = groupToInt(m.group(3), "day-of-month", 1, 31);
+
+            TimeZone tz = xsMode ? parseMatchingTimeZone(m.group(4), defaultTZ) : defaultTZ;
+            
+            return calToDateConverter.calculate(era, year, month, day, 0, 0, 0, 0, false, tz);
+        } catch (IllegalArgumentException e) {
+            // Calendar methods used to throw this for illegal dates.
+            throw new DateParseException(
+                    "Date calculation faliure. "
+                    + "Probably the date is formally correct, but refers "
+                    + "to an unexistent date (like February 30)."); 
+        }
+    }
+    
+    /**
+     * Parses an W3C XML Schema time string (not date or date-time).
+     * If the time string doesn't specify the time zone offset explicitly,
+     * the value of the {@code defaultTZ} paramter will be used. 
+     */  
+    public static Date parseXSTime(
+            String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_XS_TIME.matcher(timeStr);
+        if (!m.matches()) {
+            throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_TIME);
+        }
+        return parseTime_parseMatcher(m, defaultTZ, calToDateConverter);
+    }
+
+    /**
+     * Same as {@link #parseXSTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 times.
+     */
+    public static Date parseISO8601Time(
+            String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_ISO8601_EXTENDED_TIME.matcher(timeStr);
+        if (!m.matches()) {
+            m = PATTERN_ISO8601_BASIC_TIME.matcher(timeStr);
+            if (!m.matches()) {
+                throw new DateParseException("The value didn't match the expected pattern: "
+                            + PATTERN_ISO8601_EXTENDED_TIME + " or "
+                            + PATTERN_ISO8601_BASIC_TIME);
+            }
+        }
+        return parseTime_parseMatcher(m, defaultTZ, calToDateConverter);
+    }
+    
+    private static Date parseTime_parseMatcher(
+            Matcher m, TimeZone defaultTZ,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        _NullArgumentException.check("defaultTZ", defaultTZ);
+        try {
+            // ISO 8601 allows both 00:00 and 24:00,
+            // but Calendar.set(...) doesn't if the Calendar is not lenient.
+            int hours = groupToInt(m.group(1), "hour-of-day", 0, 24);
+            boolean hourWas24;
+            if (hours == 24) {
+                hours = 0;
+                hourWas24 = true;
+                // And a day will be added later...
+            } else {
+                hourWas24 = false;
+            }
+            
+            final String minutesStr = m.group(2);
+            int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0;
+            
+            final String secsStr = m.group(3);
+            // Allow 60 because of leap seconds
+            int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0;
+            
+            int millisecs = groupToMillisecond(m.group(4));
+            
+            // As a time is just the distance from the beginning of the day,
+            // the time-zone offest should be 0 usually.
+            TimeZone tz = parseMatchingTimeZone(m.group(5), defaultTZ);
+            
+            // Continue handling the 24:00 special case
+            int day;
+            if (hourWas24) {
+                if (minutes == 0 && secs == 0 && millisecs == 0) {
+                    day = 2;
+                } else {
+                    throw new DateParseException(
+                            "Hour 24 is only allowed in the case of "
+                            + "midnight."); 
+                }
+            } else {
+                day = 1;
+            }
+            
+            return calToDateConverter.calculate(
+                    GregorianCalendar.AD, 1970, 0, day, hours, minutes, secs, millisecs, false, tz);
+        } catch (IllegalArgumentException e) {
+            // Calendar methods used to throw this for illegal dates.
+            throw new DateParseException(
+                    "Unexpected time calculation faliure."); 
+        }
+    }
+    
+    /**
+     * Parses an W3C XML Schema date-time string (not date or time).
+     * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. 
+     * 
+     * @param dateTimeStr the string to parse. 
+     * @param defaultTZ used if the dateTime doesn't specify the
+     *     time zone offset explicitly. Can't be {@code null}. 
+     * 
+     * @throws DateParseException if the dateTime is malformed.
+     */
+    public static Date parseXSDateTime(
+            String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_XS_DATE_TIME.matcher(dateTimeStr);
+        if (!m.matches()) {
+            throw new DateParseException(
+                    "The value didn't match the expected pattern: " + PATTERN_XS_DATE_TIME);
+        }
+        return parseDateTime_parseMatcher(
+                m, defaultTZ, true, calToDateConverter);
+    }
+
+    /**
+     * Same as {@link #parseXSDateTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 format. 
+     */
+    public static Date parseISO8601DateTime(
+            String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        Matcher m = PATTERN_ISO8601_EXTENDED_DATE_TIME.matcher(dateTimeStr);
+        if (!m.matches()) {
+            m = PATTERN_ISO8601_BASIC_DATE_TIME.matcher(dateTimeStr);
+            if (!m.matches()) {
+                throw new DateParseException("The value (" + dateTimeStr + ") didn't match the expected pattern: "
+                            + PATTERN_ISO8601_EXTENDED_DATE_TIME + " or "
+                            + PATTERN_ISO8601_BASIC_DATE_TIME);
+            }
+        }
+        return parseDateTime_parseMatcher(
+                m, defaultTZ, false, calToDateConverter);
+    }
+    
+    private static Date parseDateTime_parseMatcher(
+            Matcher m, TimeZone defaultTZ,
+            boolean xsMode,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException {
+        _NullArgumentException.check("defaultTZ", defaultTZ);
+        try {
+            int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE);
+            
+            int era;
+            // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2.
+            // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based
+            // on the earlier version where 0000 didn't exist, and year -1 is BC 1.
+            if (year <= 0) {
+                era = GregorianCalendar.BC;
+                year = -year + (xsMode ? 0 : 1);
+                if (year == 0) {
+                    throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED);
+                }
+            } else {
+                era = GregorianCalendar.AD;
+            }
+            
+            int month = groupToInt(m.group(2), "month", 1, 12) - 1;
+            int day = groupToInt(m.group(3), "day-of-month", 1, 31);
+            
+            // ISO 8601 allows both 00:00 and 24:00,
+            // but cal.set(...) doesn't if the Calendar is not lenient.
+            int hours = groupToInt(m.group(4), "hour-of-day", 0, 24);
+            boolean hourWas24;
+            if (hours == 24) {
+                hours = 0;
+                hourWas24 = true;
+                // And a day will be added later...
+            } else {
+                hourWas24 = false;
+            }
+            
+            final String minutesStr = m.group(5);
+            int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0;
+            
+            final String secsStr = m.group(6);
+            // Allow 60 because of leap seconds
+            int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0;
+            
+            int millisecs = groupToMillisecond(m.group(7));
+            
+            // As a time is just the distance from the beginning of the day,
+            // the time-zone offest should be 0 usually.
+            TimeZone tz = parseMatchingTimeZone(m.group(8), defaultTZ);
+            
+            // Continue handling the 24:00 specail case
+            if (hourWas24) {
+                if (minutes != 0 || secs != 0 || millisecs != 0) {
+                    throw new DateParseException(
+                            "Hour 24 is only allowed in the case of "
+                            + "midnight."); 
+                }
+            }
+            
+            return calToDateConverter.calculate(
+                    era, year, month, day, hours, minutes, secs, millisecs, hourWas24, tz);
+        } catch (IllegalArgumentException e) {
+            // Calendar methods used to throw this for illegal dates.
+            throw new DateParseException(
+                    "Date-time calculation faliure. "
+                    + "Probably the date-time is formally correct, but "
+                    + "refers to an unexistent date-time "
+                    + "(like February 30)."); 
+        }
+    }
+
+    /**
+     * Parses the time zone part from a W3C XML Schema date/time/dateTime. 
+     * @throws DateParseException if the zone is malformed.
+     */
+    public static TimeZone parseXSTimeZone(String timeZoneStr)
+            throws DateParseException {
+        Matcher m = PATTERN_XS_TIME_ZONE.matcher(timeZoneStr);
+        if (!m.matches()) {
+            throw new DateParseException(
+                    "The time zone offset didn't match the expected pattern: " + PATTERN_XS_TIME_ZONE);
+        }
+        return parseMatchingTimeZone(timeZoneStr, null);
+    }
+
+    private static int groupToInt(String g, String gName,
+            int min, int max)
+            throws DateParseException {
+        if (g == null) {
+            throw new DateParseException("The " + gName + " part "
+                    + "is missing.");
+        }
+
+        int start;
+        
+        // Remove minus sign, so we can remove the 0-s later:
+        boolean negative;
+        if (g.startsWith("-")) {
+            negative = true;
+            start = 1;
+        } else {
+            negative = false;
+            start = 0;
+        }
+        
+        // Remove leading 0-s:
+        while (start < g.length() - 1 && g.charAt(start) == '0') {
+            start++;
+        }
+        if (start != 0) {
+            g = g.substring(start);
+        }
+        
+        try {
+            int r = Integer.parseInt(g);
+            if (negative) {
+                r = -r;
+            }
+            if (r < min) {
+                throw new DateParseException("The " + gName + " part "
+                    + "must be at least " + min + ".");
+            }
+            if (r > max) {
+                throw new DateParseException("The " + gName + " part "
+                    + "can't be more than " + max + ".");
+            }
+            return r;
+        } catch (NumberFormatException e) {
+            throw new DateParseException("The " + gName + " part "
+                    + "is a malformed integer.");
+        }
+    }
+
+    private static TimeZone parseMatchingTimeZone(
+            String s, TimeZone defaultZone)
+            throws DateParseException {
+        if (s == null) {
+            return defaultZone;
+        }
+        if (s.equals("Z")) {
+            return _DateUtil.UTC;
+        }
+        
+        StringBuilder sb = new StringBuilder(9);
+        sb.append("GMT");
+        sb.append(s.charAt(0));
+        
+        String h = s.substring(1, 3);
+        groupToInt(h, "offset-hours", 0, 23);
+        sb.append(h);
+        
+        String m;
+        int ln = s.length();
+        if (ln > 3) {
+            int startIdx = s.charAt(3) == ':' ? 4 : 3;
+            m = s.substring(startIdx, startIdx + 2);
+            groupToInt(m, "offset-minutes", 0, 59);
+            sb.append(':');
+            sb.append(m);
+        }
+        
+        return TimeZone.getTimeZone(sb.toString());
+    }
+
+    private static int groupToMillisecond(String g)
+            throws DateParseException {
+        if (g == null) {
+            return 0;
+        }
+        
+        if (g.length() > 3) {
+            g = g.substring(0, 3);
+        }
+        int i = groupToInt(g, "partial-seconds", 0, Integer.MAX_VALUE);
+        return g.length() == 1 ? i * 100 : (g.length() == 2 ? i * 10 : i);
+    }
+    
+    /**
+     * Used internally by {@link _DateUtil}; don't use its implementations for
+     * anything else.
+     */
+    public interface DateToISO8601CalendarFactory {
+        
+        /**
+         * Returns a {@link GregorianCalendar} with the desired time zone and
+         * time and US locale. The returned calendar is used as read-only.
+         * It must be guaranteed that within a thread the instance returned last time
+         * is not in use anymore when this method is called again.
+         */
+        GregorianCalendar get(TimeZone tz, Date date);
+        
+    }
+
+    /**
+     * Used internally by {@link _DateUtil}; don't use its implementations for anything else.
+     */
+    public interface CalendarFieldsToDateConverter {
+
+        /**
+         * Calculates the {@link Date} from the specified calendar fields.
+         */
+        Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs,
+                boolean addOneDay,
+                TimeZone tz);
+
+    }
+
+    /**
+     * Non-thread-safe factory that hard-references a calendar internally.
+     */
+    public static final class TrivialDateToISO8601CalendarFactory
+            implements DateToISO8601CalendarFactory {
+        
+        private GregorianCalendar calendar;
+        private TimeZone lastlySetTimeZone;
+    
+        public GregorianCalendar get(TimeZone tz, Date date) {
+            if (calendar == null) {
+                calendar = new GregorianCalendar(tz, Locale.US);
+                calendar.setGregorianChange(new Date(Long.MIN_VALUE));  // never use Julian calendar
+            } else {
+                // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone.
+                if (lastlySetTimeZone != tz) {  // Deliberately `!=` instead of `!<...>.equals()`  
+                    calendar.setTimeZone(tz);
+                    lastlySetTimeZone = tz;
+                }
+            }
+            calendar.setTime(date);
+            return calendar;
+        }
+        
+    }
+
+    /**
+     * Non-thread-safe implementation that hard-references a calendar internally.
+     */
+    public static final class TrivialCalendarFieldsToDateConverter
+            implements CalendarFieldsToDateConverter {
+
+        private GregorianCalendar calendar;
+        private TimeZone lastlySetTimeZone;
+
+        public Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs,
+                boolean addOneDay, TimeZone tz) {
+            if (calendar == null) {
+                calendar = new GregorianCalendar(tz, Locale.US);
+                calendar.setLenient(false);
+                calendar.setGregorianChange(new Date(Long.MIN_VALUE));  // never use Julian calendar
+            } else {
+                // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone.
+                if (lastlySetTimeZone != tz) {  // Deliberately `!=` instead of `!<...>.equals()`  
+                    calendar.setTimeZone(tz);
+                    lastlySetTimeZone = tz;
+                }
+            }
+
+            calendar.set(Calendar.ERA, era);
+            calendar.set(Calendar.YEAR, year);
+            calendar.set(Calendar.MONTH, month);
+            calendar.set(Calendar.DAY_OF_MONTH, day);
+            calendar.set(Calendar.HOUR_OF_DAY, hours);
+            calendar.set(Calendar.MINUTE, minutes);
+            calendar.set(Calendar.SECOND, secs);
+            calendar.set(Calendar.MILLISECOND, millisecs);
+            if (addOneDay) {
+                calendar.add(Calendar.DAY_OF_MONTH, 1);
+            }
+            
+            return calendar.getTime();
+        }
+
+    }
+    
+    public static final class DateParseException extends ParseException {
+        
+        public DateParseException(String message) {
+            super(message, 0);
+        }
+        
+    }
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_Java8.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_Java8.java b/src/main/java/org/apache/freemarker/core/util/_Java8.java
new file mode 100644
index 0000000..41515cd
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_Java8.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Used internally only, might changes without notice!
+ * Used for accessing functionality that's only present in Java 6 or later.
+ */
+public interface _Java8 {
+
+    /**
+     * This is just a placeholder. Remove this when we have some real functionality here.
+     */
+    void doSomething();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_Java8Impl.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_Java8Impl.java b/src/main/java/org/apache/freemarker/core/util/_Java8Impl.java
new file mode 100644
index 0000000..196ef76
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_Java8Impl.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * Used internally only, might changes without notice!
+ * Used for accessing functionality that's only present in Java 6 or later.
+ */
+// Compile this against Java 8
+public final class _Java8Impl implements _Java8 {
+    
+    public static final _Java8 INSTANCE = new _Java8Impl();
+
+    private _Java8Impl() {
+        // Not meant to be instantiated
+    }
+
+    @Override
+    public void doSomething() {
+        // Do something that requires Java 8 here
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java b/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
new file mode 100644
index 0000000..169f529
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_JavaVersions.java
@@ -0,0 +1,79 @@
+/*
+ * 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 org.apache.freemarker.core.Version;
+import org.apache.freemarker.core._CoreLogs;
+
+/**
+ * Used internally only, might changes without notice!
+ */
+public final class _JavaVersions {
+    
+    private _JavaVersions() {
+        // Not meant to be instantiated
+    }
+
+    private static final boolean IS_AT_LEAST_8;
+    static {
+        boolean result = false;
+        String vStr = _SecurityUtil.getSystemProperty("java.version", null);
+        if (vStr != null) {
+            try {
+                Version v = new Version(vStr);
+                result = v.getMajor() == 1 && v.getMinor() >= 8 || v.getMajor() > 1;
+            } catch (Exception e) {
+                // Ignore
+            }
+        } else {
+            try {
+                Class.forName("java.time.Instant");
+                result = true;
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+        IS_AT_LEAST_8 = result;
+    }
+    
+    /**
+     * {@code null} if Java 8 is not available, otherwise the object through with the Java 8 operations are available.
+     */
+    static public final _Java8 JAVA_8;
+    static {
+        _Java8 java8;
+        if (IS_AT_LEAST_8) {
+            try {
+                java8 = (_Java8) Class.forName("org.apache.freemarker.core.util._Java8Impl")
+                        .getField("INSTANCE").get(null);
+            } catch (Exception e) {
+                try {
+                    _CoreLogs.RUNTIME.error("Failed to access Java 6 functionality", e);
+                } catch (Exception e2) {
+                    // Suppressed
+                }
+                java8 = null;
+            }
+        } else {
+            java8 = null;
+        }
+        JAVA_8 = java8;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java b/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java
new file mode 100644
index 0000000..f3005f0
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_LocaleUtil.java
@@ -0,0 +1,45 @@
+/*
+ * 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!
+ * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
+ * access things inside this package that users shouldn't. 
+ */ 
+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;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java b/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java
new file mode 100644
index 0000000..c808d85
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_NullArgumentException.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+/**
+ * Indicates that an argument that must be non-{@code null} was {@code null}. 
+ * 
+ * @since 2.3.20
+ */
+public class _NullArgumentException extends IllegalArgumentException {
+
+    public _NullArgumentException() {
+        super("The argument can't be null");
+    }
+    
+    public _NullArgumentException(String argumentName) {
+        super("The \"" + argumentName + "\" argument can't be null");
+    }
+
+    public _NullArgumentException(String argumentName, String details) {
+        super("The \"" + argumentName + "\" argument can't be null. " + details);
+    }
+    
+    /**
+     * Convenience method to protect against a {@code null} argument.
+     */
+    public static void check(String argumentName, Object argumentValue) {
+        if (argumentValue == null) {
+            throw new _NullArgumentException(argumentName);
+        }
+    }
+
+    /**
+     * @since 2.3.22
+     */
+    public static void check(Object argumentValue) {
+        if (argumentValue == null) {
+            throw new _NullArgumentException();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_NullWriter.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_NullWriter.java b/src/main/java/org/apache/freemarker/core/util/_NullWriter.java
new file mode 100644
index 0000000..f0ba54e
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_NullWriter.java
@@ -0,0 +1,90 @@
+/*
+ * 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.io.IOException;
+import java.io.Writer;
+
+/**
+ * A {@link Writer} that simply drops what it gets.
+ * 
+ * @since 2.3.20
+ */
+public final class _NullWriter extends Writer {
+    
+    public static final _NullWriter INSTANCE = new _NullWriter();
+    
+    /** Can't be instantiated; use {@link #INSTANCE}. */
+    private _NullWriter() { }
+    
+    @Override
+    public void write(char[] cbuf, int off, int len) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void flush() throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void close() throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void write(int c) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void write(char[] cbuf) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void write(String str) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public void write(String str, int off, int len) throws IOException {
+        // Do nothing
+    }
+
+    @Override
+    public Writer append(CharSequence csq) throws IOException {
+        // Do nothing
+        return this;
+    }
+
+    @Override
+    public Writer append(CharSequence csq, int start, int end) throws IOException {
+        // Do nothing
+        return this;
+    }
+
+    @Override
+    public Writer append(char c) throws IOException {
+        // Do nothing
+        return this;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java b/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java
new file mode 100644
index 0000000..82f7c48
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java
@@ -0,0 +1,205 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _NumberUtil {
+
+    private static final BigDecimal BIG_DECIMAL_INT_MIN = BigDecimal.valueOf(Integer.MIN_VALUE);
+    private static final BigDecimal BIG_DECIMAL_INT_MAX = BigDecimal.valueOf(Integer.MAX_VALUE);
+    private static final BigInteger BIG_INTEGER_INT_MIN = BIG_DECIMAL_INT_MIN.toBigInteger();
+    private static final BigInteger BIG_INTEGER_INT_MAX = BIG_DECIMAL_INT_MAX.toBigInteger();
+    private static final BigInteger BIG_INTEGER_LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE);
+    private static final BigInteger BIG_INTEGER_LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE);
+
+    private _NumberUtil() { }
+    
+    public static boolean isInfinite(Number num) {
+        if (num instanceof Double) {
+            return ((Double) num).isInfinite();
+        } else if (num instanceof Float) {
+            return ((Float) num).isInfinite();
+        } else if (isNonFPNumberOfSupportedClass(num)) {
+            return false;
+        } else {
+            throw new UnsupportedNumberClassException(num.getClass());
+        }           
+    }
+
+    public static boolean isNaN(Number num) {
+        if (num instanceof Double) {
+            return ((Double) num).isNaN();
+        } else if (num instanceof Float) {
+            return ((Float) num).isNaN();
+        } else if (isNonFPNumberOfSupportedClass(num)) {
+            return false;
+        } else {
+            throw new UnsupportedNumberClassException(num.getClass());
+        }           
+    }
+
+    /**
+     * @return -1 for negative, 0 for zero, 1 for positive.
+     * @throws ArithmeticException if the number is NaN
+     */
+    public static int getSignum(Number num) throws ArithmeticException {
+        if (num instanceof Integer) {
+            int n = ((Integer) num).intValue();
+            return n > 0 ? 1 : (n == 0 ? 0 : -1);
+        } else if (num instanceof BigDecimal) {
+            BigDecimal n = (BigDecimal) num;
+            return n.signum();
+        } else if (num instanceof Double) {
+            double n = ((Double) num).doubleValue();
+            if (n > 0) return 1;
+            else if (n == 0) return 0;
+            else if (n < 0) return -1;
+            else throw new ArithmeticException("The signum of " + n + " is not defined.");  // NaN
+        } else if (num instanceof Float) {
+            float n = ((Float) num).floatValue();
+            if (n > 0) return 1;
+            else if (n == 0) return 0;
+            else if (n < 0) return -1;
+            else throw new ArithmeticException("The signum of " + n + " is not defined.");  // NaN
+        } else if (num instanceof Long) {
+            long n = ((Long) num).longValue();
+            return n > 0 ? 1 : (n == 0 ? 0 : -1);
+        } else if (num instanceof Short) {
+            short n = ((Short) num).shortValue();
+            return n > 0 ? 1 : (n == 0 ? 0 : -1);
+        } else if (num instanceof Byte) {
+            byte n = ((Byte) num).byteValue();
+            return n > 0 ? 1 : (n == 0 ? 0 : -1);
+        } else if (num instanceof BigInteger) {
+            BigInteger n = (BigInteger) num;
+            return n.signum();
+        } else {
+            throw new UnsupportedNumberClassException(num.getClass());
+        }
+    }
+    
+    /**
+     * Tells if a {@link BigDecimal} stores a whole number. For example, it returns {@code true} for {@code 1.0000},
+     * but {@code false} for {@code 1.0001}.
+     * 
+     * @since 2.3.21
+     */
+    static public boolean isIntegerBigDecimal(BigDecimal bd) {
+        // [Java 1.5] Try to utilize BigDecimal.toXxxExact methods
+        return bd.scale() <= 0  // A fast check that whole numbers usually (not always) match
+               || bd.setScale(0, BigDecimal.ROUND_DOWN).compareTo(bd) == 0;  // This is rather slow
+        // Note that `bd.signum() == 0 || bd.stripTrailingZeros().scale() <= 0` was also tried for the last
+        // condition, but stripTrailingZeros was slower than setScale + compareTo.
+    }
+    
+    private static boolean isNonFPNumberOfSupportedClass(Number num) {
+        return num instanceof Integer || num instanceof BigDecimal || num instanceof Long
+                || num instanceof Short || num instanceof Byte || num instanceof BigInteger;
+    }
+
+    /**
+     * Converts a {@link Number} to {@code int} whose mathematical value is exactly the same as of the original number.
+     * 
+     * @throws ArithmeticException
+     *             if the conversion to {@code int} is not possible without losing precision or overflow/underflow.
+     * 
+     * @since 2.3.22
+     */
+    public static int toIntExact(Number num) {
+        if (num instanceof Integer || num instanceof Short || num instanceof Byte) {
+            return num.intValue();
+        } else if (num instanceof Long) {
+            final long n = num.longValue();
+            final int result = (int) n;
+            if (n != result) {
+                throw newLossyConverionException(num, Integer.class);
+            }
+            return result;
+        } else if (num instanceof Double || num instanceof Float) {
+            final double n = num.doubleValue();
+            if (n % 1 != 0 || n < Integer.MIN_VALUE || n > Integer.MAX_VALUE) {
+                throw newLossyConverionException(num, Integer.class);
+            }
+            return (int) n;
+        } else if (num instanceof BigDecimal) {
+            // [Java 1.5] Use BigDecimal.toIntegerExact()
+            BigDecimal n = (BigDecimal) num;
+            if (!isIntegerBigDecimal(n)
+                    || n.compareTo(BIG_DECIMAL_INT_MAX) > 0 || n.compareTo(BIG_DECIMAL_INT_MIN) < 0) {
+                throw newLossyConverionException(num, Integer.class);
+            }
+            return n.intValue();
+        } else if (num instanceof BigInteger) {
+            BigInteger n = (BigInteger) num;
+            if (n.compareTo(BIG_INTEGER_INT_MAX) > 0 || n.compareTo(BIG_INTEGER_INT_MIN) < 0) {
+                throw newLossyConverionException(num, Integer.class);
+            }
+            return n.intValue();
+        } else {
+            throw new UnsupportedNumberClassException(num.getClass());
+        }
+    }
+
+    private static ArithmeticException newLossyConverionException(Number fromValue, Class/*<Number>*/ toType) {
+        return new ArithmeticException(
+                "Can't convert " + fromValue + " to type " + _ClassUtil.getShortClassName(toType) + " without loss.");
+    }
+
+    /**
+     * This is needed to reverse the extreme conversions in arithmetic
+     * operations so that numbers can be meaningfully used with models that
+     * don't know what to do with a BigDecimal. Of course, this will make
+     * impossible for these models (i.e. Jython) to receive a BigDecimal even if
+     * it was originally placed as such in the data model. However, since
+     * arithmetic operations aggressively erase the information regarding the
+     * original number type, we have no other choice to ensure expected operation
+     * in majority of cases.
+     */
+    public static Number optimizeNumberRepresentation(Number number) {
+        if (number instanceof BigDecimal) {
+            BigDecimal bd = (BigDecimal) number;
+            if (bd.scale() == 0) {
+                // BigDecimal -> BigInteger
+                number = bd.unscaledValue();
+            } else {
+                double d = bd.doubleValue();
+                if (d != Double.POSITIVE_INFINITY && d != Double.NEGATIVE_INFINITY) {
+                    // BigDecimal -> Double
+                    return Double.valueOf(d);
+                }
+            }
+        }
+        if (number instanceof BigInteger) {
+            BigInteger bi = (BigInteger) number;
+            if (bi.compareTo(BIG_INTEGER_INT_MAX) <= 0 && bi.compareTo(BIG_INTEGER_INT_MIN) >= 0) {
+                // BigInteger -> Integer
+                return Integer.valueOf(bi.intValue());
+            }
+            if (bi.compareTo(BIG_INTEGER_LONG_MAX) <= 0 && bi.compareTo(BIG_INTEGER_LONG_MIN) >= 0) {
+                // BigInteger -> Long
+                return Long.valueOf(bi.longValue());
+            }
+        }
+        return number;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/a5d9575f/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java b/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java
new file mode 100644
index 0000000..f7fe4ce
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/util/_SecurityUtil.java
@@ -0,0 +1,87 @@
+/*
+ * 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.security.AccessControlException;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+import org.apache.freemarker.core._CoreLogs;
+import org.slf4j.Logger;
+
+/**
+ */
+public class _SecurityUtil {
+    
+    private static final Logger LOG = _CoreLogs.SECURITY;
+    
+    private _SecurityUtil() {
+    }
+    
+    public static String getSystemProperty(final String key) {
+        return (String) AccessController.doPrivileged(
+            new PrivilegedAction()
+            {
+                @Override
+                public Object run() {
+                    return System.getProperty(key);
+                }
+            });
+    }
+
+    public static String getSystemProperty(final String key, final String defValue) {
+        try {
+            return (String) AccessController.doPrivileged(
+                new PrivilegedAction()
+                {
+                    @Override
+                    public Object run() {
+                        return System.getProperty(key, defValue);
+                    }
+                });
+        } catch (AccessControlException e) {
+            if (LOG.isWarnEnabled()) {
+                LOG.warn("Insufficient permissions to read system property " + 
+                        _StringUtil.jQuoteNoXSS(key) + ", using default value " +
+                        _StringUtil.jQuoteNoXSS(defValue));
+            }
+            return defValue;
+        }
+    }
+
+    public static Integer getSystemProperty(final String key, final int defValue) {
+        try {
+            return (Integer) AccessController.doPrivileged(
+                new PrivilegedAction()
+                {
+                    @Override
+                    public Object run() {
+                        return Integer.getInteger(key, defValue);
+                    }
+                });
+        } catch (AccessControlException e) {
+            if (LOG.isWarnEnabled()) {
+                LOG.warn("Insufficient permissions to read system property " + 
+                        _StringUtil.jQuote(key) + ", using default value " + defValue);
+            }
+            return Integer.valueOf(defValue);
+        }
+    }
+}


Mime
View raw message