You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/14 10:52:53 UTC

[10/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java
new file mode 100644
index 0000000..4029c78
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateFormatUtil.java
@@ -0,0 +1,77 @@
+/*
+ * 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.valueformat;
+
+import java.util.Date;
+
+import org.apache.freemarker.core._EvalUtil;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+/**
+ * Utility classes for implementing {@link TemplateValueFormat}-s.
+ * 
+ * @since 2.3.24 
+ */
+public final class TemplateFormatUtil {
+    
+    private TemplateFormatUtil() {
+        // Not meant to be instantiated
+    }
+
+    public static void checkHasNoParameters(String params) throws InvalidFormatParametersException
+             {
+        if (params.length() != 0) {
+            throw new InvalidFormatParametersException(
+                    "This number format doesn't support any parameters.");
+        }
+    }
+
+    /**
+     * Utility method to extract the {@link Number} from an {@link TemplateNumberModel}, and throws
+     * {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateNumberModel}
+     * that store {@code null} are in principle not allowed, and so are considered to be bugs in the
+     * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation.
+     */
+    public static Number getNonNullNumber(TemplateNumberModel numberModel)
+            throws TemplateModelException, UnformattableValueException {
+        Number number = numberModel.getAsNumber();
+        if (number == null) {
+            throw _EvalUtil.newModelHasStoredNullException(Number.class, numberModel, null);
+        }
+        return number;
+    }
+
+    /**
+     * Utility method to extract the {@link Date} from an {@link TemplateDateModel}, and throw
+     * {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateDateModel}
+     * that store {@code null} are in principle not allowed, and so are considered to be bugs in the
+     * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation.
+     */
+    public static Date getNonNullDate(TemplateDateModel dateModel) throws TemplateModelException {
+        Date date = dateModel.getAsDate();
+        if (date == null) {
+            throw _EvalUtil.newModelHasStoredNullException(Date.class, dateModel, null);
+        }
+        return date;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
new file mode 100644
index 0000000..54873d6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java
@@ -0,0 +1,93 @@
+/*
+ * 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.valueformat;
+
+import java.text.NumberFormat;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+/**
+ * Represents a number format; used in templates for formatting and parsing with that format. This is similar to Java's
+ * {@link NumberFormat}, but made to fit the requirements of FreeMarker. Also, it makes easier to define formats that
+ * can't be represented with Java's existing {@link NumberFormat} implementations.
+ * 
+ * <p>
+ * Implementations need not be thread-safe if the {@link TemplateNumberFormatFactory} doesn't recycle them among
+ * different {@link Environment}-s. As far as FreeMarker's concerned, instances are bound to a single
+ * {@link Environment}, and {@link Environment}-s are thread-local objects.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateNumberFormat extends TemplateValueFormat {
+
+    /**
+     * @param numberModel
+     *            The number to format; not {@code null}. Most implementations will just work with the return value of
+     *            {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of
+     *            a custom {@link TemplateDateModel} implementation.
+     *            
+     * @return The number as text, with no escaping (like no HTML escaping); can't be {@code null}.
+     * 
+     * @throws TemplateValueFormatException
+     *             If any problem occurs while parsing/getting the format. Notable subclass:
+     *             {@link UnformattableValueException}.
+     * @throws TemplateModelException
+     *             Exception thrown by the {@code dateModel} object when calling its methods.
+     */
+    public abstract String formatToPlainText(TemplateNumberModel numberModel)
+            throws TemplateValueFormatException, TemplateModelException;
+
+    /**
+     * Formats the model to markup instead of to plain text if the result markup will be more than just plain text
+     * escaped, otherwise falls back to formatting to plain text. If the markup result would be just the result of
+     * {@link #formatToPlainText(TemplateNumberModel)} escaped, it must return the {@link String} that
+     * {@link #formatToPlainText(TemplateNumberModel)} does.
+     * 
+     * <p>
+     * The implementation in {@link TemplateNumberFormat} simply calls {@link #formatToPlainText(TemplateNumberModel)}.
+     * 
+     * @return A {@link String} or a {@link TemplateMarkupOutputModel}; not {@code null}.
+     */
+    public Object format(TemplateNumberModel numberModel)
+            throws TemplateValueFormatException, TemplateModelException {
+        return formatToPlainText(numberModel);
+    }
+    
+    /**
+     * Tells if this formatter should be re-created if the locale changes.
+     */
+    public abstract boolean isLocaleBound();
+
+    /**
+     * This method is reserved for future purposes; currently it always throws {@link ParsingNotSupportedException}. We
+     * don't yet support number parsing with {@link TemplateNumberFormat}-s, because currently FTL parses strings to
+     * number with the {@link ArithmeticEngine} ({@link TemplateNumberFormat} were only introduced in 2.3.24). If it
+     * will be support, it will be similar to {@link TemplateDateFormat#parse(String, int)}.
+     */
+    public final Object parse(String s) throws TemplateValueFormatException {
+        throw new ParsingNotSupportedException("Number formats currenly don't support parsing");
+    }
+    
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
new file mode 100644
index 0000000..a4cac22
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormatFactory.java
@@ -0,0 +1,67 @@
+/*
+ * 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.valueformat;
+
+import java.util.Locale;
+
+import org.apache.freemarker.core.CustomStateKey;
+import org.apache.freemarker.core.MutableProcessingConfiguration;
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Environment;
+
+/**
+ * Factory for a certain kind of number formatting ({@link TemplateNumberFormat}). Usually a singleton (one-per-VM or
+ * one-per-{@link Configuration}), and so must be thread-safe.
+ * 
+ * @see MutableProcessingConfiguration#setCustomNumberFormats(java.util.Map)
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateNumberFormatFactory extends TemplateValueFormatFactory {
+
+    /**
+     * Returns a formatter for the given parameters.
+     * 
+     * <p>
+     * The returned formatter can be a new instance or a reused (cached) instance. Note that {@link Environment} itself
+     * caches the returned instances, though that cache is lost with the {@link Environment} (i.e., when the top-level
+     * template execution ends), also it might flushes lot of entries if the locale or time zone is changed during
+     * template execution. So caching on the factory level is still useful, unless creating the formatters is
+     * sufficiently cheap.
+     * 
+     * @param params
+     *            The string that further describes how the format should look. For example, when the
+     *            {@link MutableProcessingConfiguration#getNumberFormat() numberFormat} is {@code "@fooBar 1, 2"}, then it will be
+     *            {@code "1, 2"} (and {@code "@fooBar"} selects the factory). The format of this string is up to the
+     *            {@link TemplateNumberFormatFactory} implementation. Not {@code null}, often an empty string.
+     * @param locale
+     *            The locale to format for. Not {@code null}. The resulting format must be bound to this locale
+     *            forever (i.e. locale changes in the {@link Environment} must not be followed).
+     * @param env
+     *            The runtime environment from which the formatting was called. This is mostly meant to be used for
+     *            {@link Environment#getCustomState(CustomStateKey)}.
+     *            
+     * @throws TemplateValueFormatException
+     *             if any problem occurs while parsing/getting the format. Notable subclasses:
+     *             {@link InvalidFormatParametersException} if the {@code params} is malformed.
+     */
+    public abstract TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws TemplateValueFormatException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java
new file mode 100644
index 0000000..9203e5a
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormat.java
@@ -0,0 +1,42 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Superclass of all value format objects; objects that convert values to strings, or parse strings.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateValueFormat {
+
+    /**
+     * Meant to be used in error messages to tell what format the parsed string didn't fit.
+     */
+    public abstract String getDescription();
+
+    /**
+     * The implementation in {@link TemplateValueFormat} returns {@code package.className(description)}, where
+     * description comes from {@link #getDescription()}.
+     */
+    @Override
+    public String toString() {
+        return getClass().getName() + "(" + getDescription() + ")";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
new file mode 100644
index 0000000..dd538e6
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Error while getting, creating or applying {@link TemplateValueFormat}-s (including its subclasses, like
+ * {@link TemplateNumberFormat}).
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateValueFormatException extends Exception {
+
+    public TemplateValueFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public TemplateValueFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
new file mode 100644
index 0000000..04f706e
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/TemplateValueFormatFactory.java
@@ -0,0 +1,28 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Superclass of all format factories.
+ * 
+ * @since 2.3.24
+ */
+public abstract class TemplateValueFormatFactory {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
new file mode 100644
index 0000000..fa4ed3d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UndefinedCustomFormatException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.valueformat;
+
+/**
+ * @since 2.3.24
+ */
+public class UndefinedCustomFormatException extends InvalidFormatStringException {
+
+    public UndefinedCustomFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UndefinedCustomFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java
new file mode 100644
index 0000000..6ef3b10
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnformattableValueException.java
@@ -0,0 +1,41 @@
+/*
+ * 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.valueformat;
+
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * Thrown when a {@link TemplateModel} can't be formatted because of the value/properties of it are outside of that the
+ * {@link TemplateValueFormat} supports. For example, a formatter may not support dates before year 1, or can't format
+ * NaN. The most often used subclass is {@link UnknownDateTypeFormattingUnsupportedException}.
+ * 
+ * @since 2.3.24
+ */
+public class UnformattableValueException extends TemplateValueFormatException {
+
+    public UnformattableValueException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UnformattableValueException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.java
new file mode 100644
index 0000000..90ae4be
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeFormattingUnsupportedException.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.valueformat;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+
+/**
+ * Thrown when a {@link TemplateDateModel} can't be formatted because its type is {@link TemplateDateModel#UNKNOWN}.
+ * 
+ * @since 2.3.24
+ */
+public final class UnknownDateTypeFormattingUnsupportedException extends UnformattableValueException {
+
+    public UnknownDateTypeFormattingUnsupportedException() {
+        super("Can't format the date-like value because it isn't "
+                + "known if it's desired result should be a date (no time part), a time, or a date-time value.");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
new file mode 100644
index 0000000..ef6cca2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnknownDateTypeParsingUnsupportedException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.valueformat;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+
+/**
+ * Thrown when a string can't be parsed to {@link TemplateDateModel}, because the provided target type is
+ * {@link TemplateDateModel#UNKNOWN}.
+ * 
+ * @since 2.3.24
+ */
+public final class UnknownDateTypeParsingUnsupportedException extends UnformattableValueException {
+
+    public UnknownDateTypeParsingUnsupportedException() {
+        super("Can't parse the string to date-like value because it isn't "
+                + "known if it's desired result should be a date (no time part), a time, or a date-time value.");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
new file mode 100644
index 0000000..78af935
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/UnparsableValueException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.valueformat;
+
+/**
+ * Thrown when the content of the string that should be parsed by the {@link TemplateValueFormat} doesn't match what the
+ * format expects.
+ * 
+ * @since 2.3.24
+ */
+public class UnparsableValueException extends TemplateValueFormatException {
+
+    public UnparsableValueException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UnparsableValueException(String message) {
+        this(message, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
new file mode 100644
index 0000000..b4625a4
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTargetTemplateValueFormatException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.valueformat.impl;
+
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Can't invoke a template format that the template format refers to (typically thrown by alias template formats).
+ * 
+ * @since 2.3.24
+ */
+class AliasTargetTemplateValueFormatException extends TemplateValueFormatException {
+
+    public AliasTargetTemplateValueFormatException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public AliasTargetTemplateValueFormatException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
new file mode 100644
index 0000000..a964bc2
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateDateFormatFactory.java
@@ -0,0 +1,97 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util._LocaleUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather
+ * than as a concrete pattern or other kind of format string.
+ * 
+ * @since 2.3.24
+ */
+public final class AliasTemplateDateFormatFactory extends TemplateDateFormatFactory {
+
+    private final String defaultTargetFormatString;
+    private final Map<Locale, String> localizedTargetFormatStrings;
+
+    /**
+     * @param targetFormatString
+     *            The format string this format will be an alias to.
+     */
+    public AliasTemplateDateFormatFactory(String targetFormatString) {
+        defaultTargetFormatString = targetFormatString;
+        localizedTargetFormatStrings = null;
+    }
+
+    /**
+     * @param defaultTargetFormatString
+     *            The format string this format will be an alias to if there's no locale-specific format string for the
+     *            requested locale in {@code localizedTargetFormatStrings}
+     * @param localizedTargetFormatStrings
+     *            Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less
+     *            specific locale is tried, repeatedly until only the language part remains. For example, if locale is
+     *            {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in
+     *            this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")},
+     *            {@code new Locale("en")}. If there's still no matching key, the value of the
+     *            {@code targetFormatString} will be used.
+     */
+    public AliasTemplateDateFormatFactory(
+            String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) {
+        this.defaultTargetFormatString = defaultTargetFormatString;
+        this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+    }
+    
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+                                  Environment env) throws TemplateValueFormatException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        try {
+            String targetFormatString;
+            if (localizedTargetFormatStrings != null) {
+                Locale lookupLocale = locale;
+                targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                while (targetFormatString == null
+                        && (lookupLocale = _LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) {
+                    targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                }
+            } else {
+                targetFormatString = null;
+            }
+            if (targetFormatString == null) {
+                targetFormatString = defaultTargetFormatString;
+            }
+            return env.getTemplateDateFormat(targetFormatString, dateType, locale, timeZone, zonelessInput);
+        } catch (TemplateValueFormatException e) {
+            throw new AliasTargetTemplateValueFormatException("Failed to invoke format based on target format string,  "
+                    + _StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..72e8abd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/AliasTemplateNumberFormatFactory.java
@@ -0,0 +1,96 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util._LocaleUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+
+/**
+ * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather
+ * than as a concrete pattern or other kind of format string.
+ * 
+ * @since 2.3.24
+ */
+public final class AliasTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+
+    private final String defaultTargetFormatString;
+    private final Map<Locale, String> localizedTargetFormatStrings;
+
+    /**
+     * @param targetFormatString
+     *            The format string this format will be an alias to
+     */
+    public AliasTemplateNumberFormatFactory(String targetFormatString) {
+        defaultTargetFormatString = targetFormatString;
+        localizedTargetFormatStrings = null;
+    }
+
+    /**
+     * @param defaultTargetFormatString
+     *            The format string this format will be an alias to if there's no locale-specific format string for the
+     *            requested locale in {@code localizedTargetFormatStrings}
+     * @param localizedTargetFormatStrings
+     *            Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less
+     *            specific locale is tried, repeatedly until only the language part remains. For example, if locale is
+     *            {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in
+     *            this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")},
+     *            {@code new Locale("en")}. If there's still no matching key, the value of the
+     *            {@code targetFormatString} will be used.
+     */
+    public AliasTemplateNumberFormatFactory(
+            String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) {
+        this.defaultTargetFormatString = defaultTargetFormatString;
+        this.localizedTargetFormatStrings = localizedTargetFormatStrings;
+    }
+
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws TemplateValueFormatException {
+        TemplateFormatUtil.checkHasNoParameters(params);
+        try {
+            String targetFormatString;
+            if (localizedTargetFormatStrings != null) {
+                Locale lookupLocale = locale;
+                targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                while (targetFormatString == null
+                        && (lookupLocale = _LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) {
+                    targetFormatString = localizedTargetFormatStrings.get(lookupLocale);
+                }
+            } else {
+                targetFormatString = null;
+            }
+            if (targetFormatString == null) {
+                targetFormatString = defaultTargetFormatString;
+            }
+            return env.getTemplateNumberFormat(targetFormatString, locale);
+        } catch (TemplateValueFormatException e) {
+            throw new AliasTargetTemplateValueFormatException("Failed to invoke format based on target format string,  "
+                    + _StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
new file mode 100644
index 0000000..dc1709c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ExtendedDecimalFormatParser.java
@@ -0,0 +1,530 @@
+/*
+ * 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.valueformat.impl;
+
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Currency;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Parses a {@link DecimalFormat} pattern string to a {@link DecimalFormat} instance, with the pattern string extensions
+ * described in the Manual (see "Extended Java decimal format"). The result is a standard {@link DecimalFormat} object,
+ * but further configured according the extension part.
+ */
+class ExtendedDecimalFormatParser {
+    
+    private static final String PARAM_ROUNDING_MODE = "roundingMode";
+    private static final String PARAM_MULTIPIER = "multipier";
+    private static final String PARAM_DECIMAL_SEPARATOR = "decimalSeparator";
+    private static final String PARAM_MONETARY_DECIMAL_SEPARATOR = "monetaryDecimalSeparator";
+    private static final String PARAM_GROUP_SEPARATOR = "groupingSeparator";
+    private static final String PARAM_EXPONENT_SEPARATOR = "exponentSeparator";
+    private static final String PARAM_MINUS_SIGN = "minusSign";
+    private static final String PARAM_INFINITY = "infinity";
+    private static final String PARAM_NAN = "nan";
+    private static final String PARAM_PERCENT = "percent";
+    private static final String PARAM_PER_MILL = "perMill";
+    private static final String PARAM_ZERO_DIGIT = "zeroDigit";
+    private static final String PARAM_CURRENCY_CODE = "currencyCode";
+    private static final String PARAM_CURRENCY_SYMBOL = "currencySymbol";
+
+    private static final String PARAM_VALUE_RND_UP = "up";
+    private static final String PARAM_VALUE_RND_DOWN = "down";
+    private static final String PARAM_VALUE_RND_CEILING = "ceiling";
+    private static final String PARAM_VALUE_RND_FLOOR = "floor";
+    private static final String PARAM_VALUE_RND_HALF_DOWN = "halfDown";
+    private static final String PARAM_VALUE_RND_HALF_EVEN = "halfEven";
+    private static final String PARAM_VALUE_RND_HALF_UP = "halfUp";
+    private static final String PARAM_VALUE_RND_UNNECESSARY = "unnecessary";
+    
+    private static final HashMap<String, ? extends ParameterHandler> PARAM_HANDLERS;
+    static {
+        HashMap<String, ParameterHandler> m = new HashMap<>();
+        m.put(PARAM_ROUNDING_MODE, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                RoundingMode parsedValue;
+                if (value.equals(PARAM_VALUE_RND_UP)) {
+                    parsedValue = RoundingMode.UP;
+                } else if (value.equals(PARAM_VALUE_RND_DOWN)) {
+                    parsedValue = RoundingMode.DOWN;
+                } else if (value.equals(PARAM_VALUE_RND_CEILING)) {
+                    parsedValue = RoundingMode.CEILING;
+                } else if (value.equals(PARAM_VALUE_RND_FLOOR)) {
+                    parsedValue = RoundingMode.FLOOR;
+                } else if (value.equals(PARAM_VALUE_RND_HALF_DOWN)) {
+                    parsedValue = RoundingMode.HALF_DOWN;
+                } else if (value.equals(PARAM_VALUE_RND_HALF_EVEN)) {
+                    parsedValue = RoundingMode.HALF_EVEN;
+                } else if (value.equals(PARAM_VALUE_RND_HALF_UP)) {
+                    parsedValue = RoundingMode.HALF_UP;
+                } else if (value.equals(PARAM_VALUE_RND_UNNECESSARY)) {
+                    parsedValue = RoundingMode.UNNECESSARY;
+                } else {
+                    throw new InvalidParameterValueException("Should be one of: u, d, c, f, hd, he, hu, un");
+                }
+
+                parser.roundingMode = parsedValue;
+            }
+        });
+        m.put(PARAM_MULTIPIER, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                try {
+                    parser.multipier = Integer.valueOf(value);
+                } catch (NumberFormatException e) {
+                    throw new InvalidParameterValueException("Malformed integer.");
+                }
+            }
+        });
+        m.put(PARAM_DECIMAL_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setDecimalSeparator(value.charAt(0));
+            }
+        });
+        m.put(PARAM_MONETARY_DECIMAL_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setMonetaryDecimalSeparator(value.charAt(0));
+            }
+        });
+        m.put(PARAM_GROUP_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setGroupingSeparator(value.charAt(0));
+            }
+        });
+        m.put(PARAM_EXPONENT_SEPARATOR, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                parser.symbols.setExponentSeparator(value);
+            }
+        });
+        m.put(PARAM_MINUS_SIGN, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setMinusSign(value.charAt(0));
+            }
+        });
+        m.put(PARAM_INFINITY, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                parser.symbols.setInfinity(value);
+            }
+        });
+        m.put(PARAM_NAN, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                parser.symbols.setNaN(value);
+            }
+        });
+        m.put(PARAM_PERCENT, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setPercent(value.charAt(0));
+            }
+        });
+        m.put(PARAM_PER_MILL, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setPerMill(value.charAt(0));
+            }
+        });
+        m.put(PARAM_ZERO_DIGIT, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                if (value.length() != 1) {
+                    throw new InvalidParameterValueException("Must contain exactly 1 character.");
+                }
+                parser.symbols.setZeroDigit(value.charAt(0));
+            }
+        });
+        m.put(PARAM_CURRENCY_CODE, new ParameterHandler() {
+            @Override
+            public void handle(ExtendedDecimalFormatParser parser, String value)
+                    throws InvalidParameterValueException {
+                Currency currency;
+                try {
+                    currency = Currency.getInstance(value);
+                } catch (IllegalArgumentException e) {
+                    throw new InvalidParameterValueException("Not a known ISO 4217 code.");
+                }
+                parser.symbols.setCurrency(currency);
+            }
+        });
+        PARAM_HANDLERS = m;
+    }
+
+    private static final String SNIP_MARK = "[...]";
+    private static final int MAX_QUOTATION_LENGTH = 10; // Must be more than SNIP_MARK.length!
+
+    private final String src;
+    private int pos = 0;
+
+    private final DecimalFormatSymbols symbols;
+    private RoundingMode roundingMode;
+    private Integer multipier;
+
+    static DecimalFormat parse(String formatString, Locale locale) throws ParseException {
+        return new ExtendedDecimalFormatParser(formatString, locale).parse();
+    }
+
+    private DecimalFormat parse() throws ParseException {
+        String stdPattern = fetchStandardPattern();
+        skipWS();
+        parseFormatStringExtension();
+
+        DecimalFormat decimalFormat;
+        try {
+            decimalFormat = new DecimalFormat(stdPattern, symbols);
+        } catch (IllegalArgumentException e) {
+            ParseException pe = new ParseException(e.getMessage(), 0);
+            if (e.getCause() != null) {
+                try {
+                    e.initCause(e.getCause());
+                } catch (Exception e2) {
+                    // Supress
+                }
+            }
+            throw pe;
+        }
+
+        if (roundingMode != null) {
+            decimalFormat.setRoundingMode(roundingMode);
+        }
+
+        if (multipier != null) {
+            decimalFormat.setMultiplier(multipier.intValue());
+        }
+
+        return decimalFormat;
+    }
+
+    private void parseFormatStringExtension() throws ParseException {
+        int ln = src.length();
+
+        if (pos == ln) {
+            return;
+        }
+
+        String currencySymbol = null;  // Exceptional, as must be applied after "currency code"
+        fetchParamters: do {
+            int namePos = pos;
+            String name = fetchName();
+            if (name == null) {
+                throw newExpectedSgParseException("name");
+            }
+
+            skipWS();
+
+            if (!fetchChar('=')) {
+                throw newExpectedSgParseException("\"=\"");
+            }
+
+            skipWS();
+
+            int valuePos = pos;
+            String value = fetchValue();
+            if (value == null) {
+                throw newExpectedSgParseException("value");
+            }
+            int paramEndPos = pos;
+
+            ParameterHandler handler = PARAM_HANDLERS.get(name);
+            if (handler == null) {
+                if (name.equals(PARAM_CURRENCY_SYMBOL)) {
+                    currencySymbol = value;
+                } else {
+                    throw newUnknownParameterException(name, namePos);
+                }
+            } else {
+                try {
+                    handler.handle(this, value);
+                } catch (InvalidParameterValueException e) {
+                    throw newInvalidParameterValueException(name, value, valuePos, e);
+                }
+            }
+
+            skipWS();
+
+            // Optional comma
+            if (fetchChar(',')) {
+                skipWS();
+            } else {
+                if (pos == ln) {
+                    break fetchParamters;
+                }
+                if (pos == paramEndPos) {
+                    throw newExpectedSgParseException("parameter separator whitespace or comma");
+                }
+            }
+        } while (true);
+        
+        // This is brought out to here to ensure that it's applied after "currency code":
+        if (currencySymbol != null) {
+            symbols.setCurrencySymbol(currencySymbol);
+        }
+    }
+
+    private ParseException newInvalidParameterValueException(String name, String value, int valuePos,
+            InvalidParameterValueException e) {
+        return new java.text.ParseException(
+                _StringUtil.jQuote(value) + " is an invalid value for the \"" + name + "\" parameter: "
+                + e.message,
+                valuePos);
+    }
+
+    private ParseException newUnknownParameterException(String name, int namePos) throws ParseException {
+        StringBuilder sb = new StringBuilder(128);
+        sb.append("Unsupported parameter name, ").append(_StringUtil.jQuote(name));
+        sb.append(". The supported names are: ");
+        Set<String> legalNames = PARAM_HANDLERS.keySet();
+        String[] legalNameArr = legalNames.toArray(new String[legalNames.size()]);
+        Arrays.sort(legalNameArr);
+        for (int i = 0; i < legalNameArr.length; i++) {
+            if (i != 0) {
+                sb.append(", ");
+            }
+            sb.append(legalNameArr[i]);
+        }
+        return new java.text.ParseException(sb.toString(), namePos);
+    }
+
+    private void skipWS() {
+        int ln = src.length();
+        while (pos < ln && isWS(src.charAt(pos))) {
+            pos++;
+        }
+    }
+
+    private boolean fetchChar(char fetchedChar) {
+        if (pos < src.length() && src.charAt(pos) == fetchedChar) {
+            pos++;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isWS(char c) {
+        return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\u00A0';
+    }
+
+    private String fetchName() throws ParseException {
+        int ln = src.length();
+        int startPos = pos;
+        boolean firstChar = true;
+        scanUntilEnd: while (pos < ln) {
+            char c = src.charAt(pos);
+            if (firstChar) {
+                if (!Character.isJavaIdentifierStart(c)) {
+                    break scanUntilEnd;
+                }
+                firstChar = false;
+            } else if (!Character.isJavaIdentifierPart(c)) {
+                break scanUntilEnd;
+            }
+            pos++;
+        }
+        return !firstChar ? src.substring(startPos, pos) : null;
+    }
+
+    private String fetchValue() throws ParseException {
+        int ln = src.length();
+        int startPos = pos;
+        char openedQuot = 0;
+        boolean needsUnescaping = false;
+        scanUntilEnd: while (pos < ln) {
+            char c = src.charAt(pos);
+            if (c == '\'' || c == '"') {
+                if (openedQuot == 0) {
+                    if (startPos != pos) {
+                        throw new java.text.ParseException(
+                                "The " + c + " character can only be used for quoting values, "
+                                        + "but it was in the middle of an non-quoted value.",
+                                pos);
+                    }
+                    openedQuot = c;
+                } else if (c == openedQuot) {
+                    if (pos + 1 < ln && src.charAt(pos + 1) == openedQuot) {
+                        pos++; // skip doubled quote (escaping)
+                        needsUnescaping = true;
+                    } else {
+                        String str = src.substring(startPos + 1, pos);
+                        pos++;
+                        return needsUnescaping ? unescape(str, openedQuot) : str;
+                    }
+                }
+            } else {
+                if (openedQuot == 0 && !Character.isJavaIdentifierPart(c)) {
+                    break scanUntilEnd;
+                }
+            }
+            pos++;
+        } // while
+        if (openedQuot != 0) {
+            throw new java.text.ParseException(
+                    "The " + openedQuot 
+                    + " quotation wasn't closed when the end of the source was reached.",
+                    pos);
+        }
+        return startPos == pos ? null : src.substring(startPos, pos);
+    }
+
+    private String unescape(String s, char openedQuot) {
+        return openedQuot == '\'' ? _StringUtil.replace(s, "\'\'", "\'") : _StringUtil.replace(s, "\"\"", "\"");
+    }
+
+    private String fetchStandardPattern() {
+        int pos = this.pos;
+        int ln = src.length();
+        int semicolonCnt = 0;
+        boolean quotedMode = false;
+        findStdPartEnd: while (pos < ln) {
+            char c = src.charAt(pos);
+            if (c == ';' && !quotedMode) {
+                semicolonCnt++;
+                if (semicolonCnt == 2) {
+                    break findStdPartEnd;
+                }
+            } else if (c == '\'') {
+                if (quotedMode) {
+                    if (pos + 1 < ln && src.charAt(pos + 1) == '\'') {
+                        // Skips "''" used for escaping "'"
+                        pos++;
+                    } else {
+                        quotedMode = false;
+                    }
+                } else {
+                    quotedMode = true;
+                }
+            }
+            pos++;
+        }
+
+        String stdFormatStr;
+        if (semicolonCnt < 2) { // We have a standard DecimalFormat string
+            // Note that "0.0;" and "0.0" gives the same result with DecimalFormat, so we leave a ';' there
+            stdFormatStr = src;
+        } else { // `pos` points to the 2nd ';'
+            int stdEndPos = pos;
+            if (src.charAt(pos - 1) == ';') { // we have a ";;"
+                // Note that ";;" is illegal in DecimalFormat, so this is backward compatible.
+                stdEndPos--;
+            }
+            stdFormatStr = src.substring(0, stdEndPos);
+        }
+
+        if (pos < ln) {
+            pos++; // Skips closing ';'
+        }
+        this.pos = pos;
+
+        return stdFormatStr;
+    }
+
+    private ExtendedDecimalFormatParser(String formatString, Locale locale) {
+        src = formatString;
+        symbols = new DecimalFormatSymbols(locale);
+    }
+
+    private ParseException newExpectedSgParseException(String expectedThing) {
+        String quotation;
+
+        // Ignore trailing WS when calculating the length:
+        int i = src.length() - 1;
+        while (i >= 0 && Character.isWhitespace(src.charAt(i))) {
+            i--;
+        }
+        int ln = i + 1;
+
+        if (pos < ln) {
+            int qEndPos = pos + MAX_QUOTATION_LENGTH;
+            if (qEndPos >= ln) {
+                quotation = src.substring(pos, ln);
+            } else {
+                quotation = src.substring(pos, qEndPos - SNIP_MARK.length()) + SNIP_MARK;
+            }
+        } else {
+            quotation = null;
+        }
+
+        return new ParseException(
+                "Expected a(n) " + expectedThing + " at position " + pos + " (0-based), but "
+                        + (quotation == null ? "reached the end of the input." : "found: " + quotation),
+                pos);
+    }
+
+    private interface ParameterHandler {
+
+        void handle(ExtendedDecimalFormatParser parser, String value)
+                throws InvalidParameterValueException;
+
+    }
+
+    private static class InvalidParameterValueException extends Exception {
+
+        private final String message;
+
+        public InvalidParameterValueException(String message) {
+            this.message = message;
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java
new file mode 100644
index 0000000..8790d00
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java
@@ -0,0 +1,270 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.DateParseException;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+abstract class ISOLikeTemplateDateFormat  extends TemplateDateFormat {
+    
+    private static final String XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE
+            = "Less than seconds accuracy isn't allowed by the XML Schema format";
+    private final ISOLikeTemplateDateFormatFactory factory;
+    private final Environment env;
+    protected final int dateType;
+    protected final boolean zonelessInput;
+    protected final TimeZone timeZone;
+    protected final Boolean forceUTC;
+    protected final Boolean showZoneOffset;
+    protected final int accuracy;
+
+    /**
+     * @param formatString The value of the ..._format setting, like "iso nz".
+     * @param parsingStart The index of the char in the {@code settingValue} that directly after the prefix that has
+     *     indicated the exact formatter class (like "iso" or "xs") 
+     */
+    public ISOLikeTemplateDateFormat(
+            final String formatString, int parsingStart,
+            int dateType, boolean zonelessInput,
+            TimeZone timeZone,
+            ISOLikeTemplateDateFormatFactory factory, Environment env)
+            throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException {
+        this.factory = factory;
+        this.env = env;
+        if (dateType == TemplateDateModel.UNKNOWN) {
+            throw new UnknownDateTypeFormattingUnsupportedException();
+        }
+        
+        this.dateType = dateType;
+        this.zonelessInput = zonelessInput;
+        
+        final int ln = formatString.length();
+        boolean afterSeparator = false;
+        int i = parsingStart;
+        int accuracy = _DateUtil.ACCURACY_MILLISECONDS;
+        Boolean showZoneOffset = null;
+        Boolean forceUTC = Boolean.FALSE;
+        while (i < ln) {
+            final char c = formatString.charAt(i++);
+            if (c == '_' || c == ' ') {
+                afterSeparator = true;
+            } else {
+                if (!afterSeparator) {
+                    throw new InvalidFormatParametersException(
+                            "Missing space or \"_\" before \"" + c + "\" (at char pos. " + i + ").");
+                }
+                
+                switch (c) {
+                case 'h':
+                case 'm':
+                case 's':
+                    if (accuracy != _DateUtil.ACCURACY_MILLISECONDS) {
+                        throw new InvalidFormatParametersException(
+                                "Character \"" + c + "\" is unexpected as accuracy was already specified earlier "
+                                + "(at char pos. " + i + ").");
+                    }
+                    switch (c) {
+                    case 'h':
+                        if (isXSMode()) {
+                            throw new InvalidFormatParametersException(
+                                    XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE);
+                        }
+                        accuracy = _DateUtil.ACCURACY_HOURS;
+                        break;
+                    case 'm':
+                        if (i < ln && formatString.charAt(i) == 's') {
+                            i++;
+                            accuracy = _DateUtil.ACCURACY_MILLISECONDS_FORCED;
+                        } else {
+                            if (isXSMode()) {
+                                throw new InvalidFormatParametersException(
+                                        XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE);
+                            }
+                            accuracy = _DateUtil.ACCURACY_MINUTES;
+                        }
+                        break;
+                    case 's':
+                        accuracy = _DateUtil.ACCURACY_SECONDS;
+                        break;
+                    }
+                    break;
+                case 'f':
+                    if (i < ln && formatString.charAt(i) == 'u') {
+                        checkForceUTCNotSet(forceUTC);
+                        i++;
+                        forceUTC = Boolean.TRUE;
+                        break;
+                    }
+                    // Falls through
+                case 'n':
+                    if (showZoneOffset != null) {
+                        throw new InvalidFormatParametersException(
+                                "Character \"" + c + "\" is unexpected as zone offset visibility was already "
+                                + "specified earlier. (at char pos. " + i + ").");
+                    }
+                    switch (c) {
+                    case 'n':
+                        if (i < ln && formatString.charAt(i) == 'z') {
+                            i++;
+                            showZoneOffset = Boolean.FALSE;
+                        } else {
+                            throw new InvalidFormatParametersException(
+                                    "\"n\" must be followed by \"z\" (at char pos. " + i + ").");
+                        }
+                        break;
+                    case 'f':
+                        if (i < ln && formatString.charAt(i) == 'z') {
+                            i++;
+                            showZoneOffset = Boolean.TRUE;
+                        } else {
+                            throw new InvalidFormatParametersException(
+                                    "\"f\" must be followed by \"z\" (at char pos. " + i + ").");
+                        }
+                        break;
+                    }
+                    break;
+                case 'u':
+                    checkForceUTCNotSet(forceUTC);
+                    forceUTC = null;  // means UTC will be used except for zonelessInput
+                    break;
+                default:
+                    throw new InvalidFormatParametersException(
+                            "Unexpected character, " + _StringUtil.jQuote(String.valueOf(c))
+                            + ". Expected the beginning of one of: h, m, s, ms, nz, fz, u"
+                            + " (at char pos. " + i + ").");
+                } // switch
+                afterSeparator = false;
+            } // else
+        } // while
+        
+        this.accuracy = accuracy;
+        this.showZoneOffset = showZoneOffset;
+        this.forceUTC = forceUTC;
+        this.timeZone = timeZone;
+    }
+
+    private void checkForceUTCNotSet(Boolean fourceUTC) throws InvalidFormatParametersException {
+        if (fourceUTC != Boolean.FALSE) {
+            throw new InvalidFormatParametersException(
+                    "The UTC usage option was already set earlier.");
+        }
+    }
+    
+    @Override
+    public final String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException {
+        final Date date = TemplateFormatUtil.getNonNullDate(dateModel);
+        return format(
+                date,
+                dateType != TemplateDateModel.TIME,
+                dateType != TemplateDateModel.DATE,
+                showZoneOffset == null
+                        ? !zonelessInput
+                        : showZoneOffset.booleanValue(),
+                accuracy,
+                (forceUTC == null ? !zonelessInput : forceUTC.booleanValue()) ? _DateUtil.UTC : timeZone,
+                factory.getISOBuiltInCalendar(env));
+    }
+    
+    protected abstract String format(Date date,
+            boolean datePart, boolean timePart, boolean offsetPart,
+            int accuracy,
+            TimeZone timeZone,
+            DateToISO8601CalendarFactory calendarFactory);
+
+    @Override
+    @SuppressFBWarnings(value = "RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN",
+            justification = "Known to use the singleton Boolean-s only")
+    public final Date parse(String s, int dateType) throws UnparsableValueException {
+        CalendarFieldsToDateConverter calToDateConverter = factory.getCalendarFieldsToDateCalculator(env);
+        TimeZone tz = forceUTC != Boolean.FALSE ? _DateUtil.UTC : timeZone;
+        try {
+            if (dateType == TemplateDateModel.DATE) {
+                return parseDate(s, tz, calToDateConverter);
+            } else if (dateType == TemplateDateModel.TIME) {
+                return parseTime(s, tz, calToDateConverter);
+            } else if (dateType == TemplateDateModel.DATETIME) {
+                return parseDateTime(s, tz, calToDateConverter);
+            } else {
+                throw new BugException("Unexpected date type: " + dateType);
+            }
+        } catch (DateParseException e) {
+            throw new UnparsableValueException(e.getMessage(), e);
+        }
+    }
+    
+    protected abstract Date parseDate(
+            String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException;
+    
+    protected abstract Date parseTime(
+            String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException;
+    
+    protected abstract Date parseDateTime(
+            String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) 
+            throws DateParseException;
+
+    @Override
+    public final String getDescription() {
+        switch (dateType) {
+            case TemplateDateModel.DATE: return getDateDescription();
+            case TemplateDateModel.TIME: return getTimeDescription();
+            case TemplateDateModel.DATETIME: return getDateTimeDescription();
+            default: return "<error: wrong format dateType>";
+        }
+    }
+    
+    protected abstract String getDateDescription();
+    protected abstract String getTimeDescription();
+    protected abstract String getDateTimeDescription();
+    
+    @Override
+    public final boolean isLocaleBound() {
+        return false;
+    }
+    
+    @Override
+    public boolean isTimeZoneBound() {
+        return true;
+    }
+
+    protected abstract boolean isXSMode();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java
new file mode 100644
index 0000000..5db8f46
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java
@@ -0,0 +1,57 @@
+/*
+ * 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.valueformat.impl;
+
+import org.apache.freemarker.core.CustomStateKey;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import org.apache.freemarker.core.util._DateUtil.TrivialCalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.TrivialDateToISO8601CalendarFactory;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+
+abstract class ISOLikeTemplateDateFormatFactory extends TemplateDateFormatFactory {
+    
+    private static final CustomStateKey<TrivialDateToISO8601CalendarFactory> DATE_TO_CAL_CONVERTER_KEY
+            = new CustomStateKey<TrivialDateToISO8601CalendarFactory>() {
+        @Override
+        protected TrivialDateToISO8601CalendarFactory create() {
+            return new TrivialDateToISO8601CalendarFactory();
+        }
+    };
+    private static final CustomStateKey<TrivialCalendarFieldsToDateConverter> CAL_TO_DATE_CONVERTER_KEY
+            = new CustomStateKey<TrivialCalendarFieldsToDateConverter>() {
+        @Override
+        protected TrivialCalendarFieldsToDateConverter create() {
+            return new TrivialCalendarFieldsToDateConverter();
+        }
+    };
+    
+    protected ISOLikeTemplateDateFormatFactory() { }
+
+    public DateToISO8601CalendarFactory getISOBuiltInCalendar(Environment env) {
+        return (DateToISO8601CalendarFactory) env.getCustomState(DATE_TO_CAL_CONVERTER_KEY);
+    }
+
+    public CalendarFieldsToDateConverter getCalendarFieldsToDateCalculator(Environment env) {
+        return (CalendarFieldsToDateConverter) env.getCustomState(CAL_TO_DATE_CONVERTER_KEY);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java
new file mode 100644
index 0000000..4856ee0
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.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.valueformat.impl;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter;
+import org.apache.freemarker.core.util._DateUtil.DateParseException;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+
+class ISOTemplateDateFormat extends ISOLikeTemplateDateFormat {
+
+    ISOTemplateDateFormat(
+            String settingValue, int parsingStart,
+            int dateType, boolean zonelessInput,
+            TimeZone timeZone,
+            ISOLikeTemplateDateFormatFactory factory,
+            Environment env)
+            throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException {
+        super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory, env);
+    }
+
+    @Override
+    protected String format(Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy,
+            TimeZone timeZone, DateToISO8601CalendarFactory calendarFactory) {
+        return _DateUtil.dateToISO8601String(
+                date, datePart, timePart, timePart && offsetPart, accuracy, timeZone, calendarFactory);
+    }
+
+    @Override
+    protected Date parseDate(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter)
+            throws DateParseException {
+        return _DateUtil.parseISO8601Date(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected Date parseTime(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter)
+            throws DateParseException {
+        return _DateUtil.parseISO8601Time(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected Date parseDateTime(String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) throws DateParseException {
+        return _DateUtil.parseISO8601DateTime(s, tz, calToDateConverter);
+    }
+    
+    @Override
+    protected String getDateDescription() {
+        return "ISO 8601 (subset) date";
+    }
+
+    @Override
+    protected String getTimeDescription() {
+        return "ISO 8601 (subset) time";
+    }
+
+    @Override
+    protected String getDateTimeDescription() {
+        return "ISO 8601 (subset) date-time";
+    }
+
+    @Override
+    protected boolean isXSMode() {
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java
new file mode 100644
index 0000000..ddace3d
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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.valueformat.impl;
+
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+
+/**
+ * Creates {@link TemplateDateFormat}-s that follows ISO 8601 extended format 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.
+ */
+public final class ISOTemplateDateFormatFactory extends ISOLikeTemplateDateFormatFactory {
+    
+    public static final ISOTemplateDateFormatFactory INSTANCE = new ISOTemplateDateFormatFactory();
+
+    private ISOTemplateDateFormatFactory() {
+        // Not meant to be instantiated
+    }
+
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+                                  Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        // We don't cache these as creating them is cheap (only 10% speedup of ${d?string.xs} with caching)
+        return new ISOTemplateDateFormat(
+                params, 3,
+                dateType, zonelessInput,
+                timeZone, this, env);
+    }
+
+}