freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [2/6] incubator-freemarker git commit: Moved value formatter related classes from o.a.f.core to core.valueformatter
Date Fri, 24 Feb 2017 22:00:32 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java
new file mode 100644
index 0000000..05a4d91
--- /dev/null
+++ b/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.valueformat.TemplateFormatUtil;
+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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+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/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java
new file mode 100644
index 0000000..83349d8
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java
@@ -0,0 +1,54 @@
+/*
+ * 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.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 Object DATE_TO_CAL_CONVERTER_KEY = new Object();
+    private static final Object CAL_TO_DATE_CONVERTER_KEY = new Object();
+    
+    protected ISOLikeTemplateDateFormatFactory() { }
+
+    public DateToISO8601CalendarFactory getISOBuiltInCalendar(Environment env) {
+        DateToISO8601CalendarFactory r = (DateToISO8601CalendarFactory) env.getCustomState(DATE_TO_CAL_CONVERTER_KEY);
+        if (r == null) {
+            r = new TrivialDateToISO8601CalendarFactory();
+            env.setCustomState(DATE_TO_CAL_CONVERTER_KEY, r);
+        }
+        return r;
+    }
+
+    public CalendarFieldsToDateConverter getCalendarFieldsToDateCalculator(Environment env) {
+        CalendarFieldsToDateConverter r = (CalendarFieldsToDateConverter) env.getCustomState(CAL_TO_DATE_CONVERTER_KEY);
+        if (r == null) {
+            r = new TrivialCalendarFieldsToDateConverter();
+            env.setCustomState(CAL_TO_DATE_CONVERTER_KEY, r);
+        }
+        return r;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java
new file mode 100644
index 0000000..4341755
--- /dev/null
+++ b/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/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java
new file mode 100644
index 0000000..d25e754
--- /dev/null
+++ b/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 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;
+
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * 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);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java
new file mode 100644
index 0000000..99ad68c
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java
@@ -0,0 +1,75 @@
+/*
+ * 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.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
+
+/**
+ * Java {@link DateFormat}-based format.
+ */
+class JavaTemplateDateFormat extends TemplateDateFormat {
+    
+    private final DateFormat javaDateFormat;
+
+    public JavaTemplateDateFormat(DateFormat javaDateFormat) {
+        this.javaDateFormat = javaDateFormat;
+    }
+    
+    @Override
+    public String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException {
+        return javaDateFormat.format(TemplateFormatUtil.getNonNullDate(dateModel));
+    }
+
+    @Override
+    public Date parse(String s, int dateType) throws UnparsableValueException {
+        try {
+            return javaDateFormat.parse(s);
+        } catch (ParseException e) {
+            throw new UnparsableValueException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public String getDescription() {
+        return javaDateFormat instanceof SimpleDateFormat
+                ? ((SimpleDateFormat) javaDateFormat).toPattern()
+                : javaDateFormat.toString();
+    }
+
+    @Override
+    public boolean isLocaleBound() {
+        return true;
+    }
+
+    @Override
+    public boolean isTimeZoneBound() {
+        return true;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java
new file mode 100644
index 0000000..064eabd
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java
@@ -0,0 +1,187 @@
+/*
+ * 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.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.slf4j.Logger;
+
+/**
+ * Deals with {@link TemplateDateFormat}-s that wrap a Java {@link DateFormat}. The parameter string is usually a
+ * {@link java.text.SimpleDateFormat} pattern, but it also recognized the names "short", "medium", "long"
+ * and "full", which correspond to formats defined by {@link DateFormat} with similar names.
+ *
+ * <p>Note that the resulting {@link java.text.SimpleDateFormat}-s are globally cached, and threading issues are
+ * addressed by cloning the cached instance before returning it. So it just makes object creation faster, but doesn't
+ * eliminate it.
+ */
+public class JavaTemplateDateFormatFactory extends TemplateDateFormatFactory {
+    
+    public static final JavaTemplateDateFormatFactory INSTANCE = new JavaTemplateDateFormatFactory();
+    
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+
+    private static final ConcurrentHashMap<CacheKey, DateFormat> GLOBAL_FORMAT_CACHE = new ConcurrentHashMap<>();
+    private static final int LEAK_ALERT_DATE_FORMAT_CACHE_SIZE = 1024;
+    
+    private JavaTemplateDateFormatFactory() {
+        // Can't be instantiated
+    }
+    
+    /**
+     * @param zonelessInput
+     *            Has no effect in this implementation.
+     */
+    @Override
+    public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput,
+                                  Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        return new JavaTemplateDateFormat(getJavaDateFormat(dateType, params, locale, timeZone));
+    }
+
+    /**
+     * Returns a "private" copy (not in the global cache) for the given format.  
+     */
+    private DateFormat getJavaDateFormat(int dateType, String nameOrPattern, Locale locale, TimeZone timeZone)
+            throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+
+        // Get DateFormat from global cache:
+        CacheKey cacheKey = new CacheKey(dateType, nameOrPattern, locale, timeZone);
+        DateFormat jFormat;
+        
+        jFormat = GLOBAL_FORMAT_CACHE.get(cacheKey);
+        if (jFormat == null) {
+            // Add format to global format cache.
+            StringTokenizer tok = new StringTokenizer(nameOrPattern, "_");
+            int tok1Style = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : DateFormat.DEFAULT;
+            if (tok1Style != -1) {
+                switch (dateType) {
+                    case TemplateDateModel.UNKNOWN: {
+                        throw new UnknownDateTypeFormattingUnsupportedException();
+                    }
+                    case TemplateDateModel.TIME: {
+                        jFormat = DateFormat.getTimeInstance(tok1Style, cacheKey.locale);
+                        break;
+                    }
+                    case TemplateDateModel.DATE: {
+                        jFormat = DateFormat.getDateInstance(tok1Style, cacheKey.locale);
+                        break;
+                    }
+                    case TemplateDateModel.DATETIME: {
+                        int tok2Style = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : tok1Style;
+                        if (tok2Style != -1) {
+                            jFormat = DateFormat.getDateTimeInstance(tok1Style, tok2Style, cacheKey.locale);
+                        }
+                        break;
+                    }
+                }
+            }
+            if (jFormat == null) {
+                try {
+                    jFormat = new SimpleDateFormat(nameOrPattern, cacheKey.locale);
+                } catch (IllegalArgumentException e) {
+                    final String msg = e.getMessage();
+                    throw new InvalidFormatParametersException(
+                            msg != null ? msg : "Invalid SimpleDateFormat pattern", e);
+                }
+            }
+            jFormat.setTimeZone(cacheKey.timeZone);
+            
+            if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_DATE_FORMAT_CACHE_SIZE) {
+                boolean triggered = false;
+                synchronized (JavaTemplateDateFormatFactory.class) {
+                    if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_DATE_FORMAT_CACHE_SIZE) {
+                        triggered = true;
+                        GLOBAL_FORMAT_CACHE.clear();
+                    }
+                }
+                if (triggered) {
+                    LOG.warn("Global Java DateFormat cache has exceeded {} entries => cache flushed. "
+                            + "Typical cause: Some template generates high variety of format pattern strings.",
+                            LEAK_ALERT_DATE_FORMAT_CACHE_SIZE);
+                }
+            }
+            
+            DateFormat prevJFormat = GLOBAL_FORMAT_CACHE.putIfAbsent(cacheKey, jFormat);
+            if (prevJFormat != null) {
+                jFormat = prevJFormat;
+            }
+        }  // if cache miss
+        
+        return (DateFormat) jFormat.clone();  // For thread safety
+    }
+
+    private static final class CacheKey {
+        private final int dateType;
+        private final String pattern;
+        private final Locale locale;
+        private final TimeZone timeZone;
+
+        CacheKey(int dateType, String pattern, Locale locale, TimeZone timeZone) {
+            this.dateType = dateType;
+            this.pattern = pattern;
+            this.locale = locale;
+            this.timeZone = timeZone;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof CacheKey) {
+                CacheKey fk = (CacheKey) o;
+                return dateType == fk.dateType && fk.pattern.equals(pattern) && fk.locale.equals(locale)
+                        && fk.timeZone.equals(timeZone);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return dateType ^ pattern.hashCode() ^ locale.hashCode() ^ timeZone.hashCode();
+        }
+    }
+
+    private int parseDateStyleToken(String token) {
+        if ("short".equals(token)) {
+            return DateFormat.SHORT;
+        }
+        if ("medium".equals(token)) {
+            return DateFormat.MEDIUM;
+        }
+        if ("long".equals(token)) {
+            return DateFormat.LONG;
+        }
+        if ("full".equals(token)) {
+            return DateFormat.FULL;
+        }
+        return -1;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java
new file mode 100644
index 0000000..26ee41c
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java
@@ -0,0 +1,64 @@
+/*
+ * 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.text.NumberFormat;
+
+import org.apache.freemarker.core.valueformat.TemplateFormatUtil;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.UnformattableValueException;
+
+final class JavaTemplateNumberFormat extends TemplateNumberFormat {
+    
+    private final String formatString;
+    private final NumberFormat javaNumberFormat;
+
+    public JavaTemplateNumberFormat(NumberFormat javaNumberFormat, String formatString) {
+        this.formatString = formatString;
+        this.javaNumberFormat = javaNumberFormat;
+    }
+
+    @Override
+    public String formatToPlainText(TemplateNumberModel numberModel) throws UnformattableValueException, TemplateModelException {
+        Number number = TemplateFormatUtil.getNonNullNumber(numberModel);
+        try {
+            return javaNumberFormat.format(number);
+        } catch (ArithmeticException e) {
+            throw new UnformattableValueException(
+                    "This format can't format the " + number + " number. Reason: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public boolean isLocaleBound() {
+        return true;
+    }
+
+    public NumberFormat getJavaNumberFormat() {
+        return javaNumberFormat;
+    }
+
+    @Override
+    public String getDescription() {
+        return formatString;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..f511b81
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java
@@ -0,0 +1,133 @@
+/*
+ * 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.text.NumberFormat;
+import java.text.ParseException;
+import java.util.Locale;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.slf4j.Logger;
+
+/**
+ * Deals with {@link TemplateNumberFormat}-s that wrap a Java {@link NumberFormat}. The parameter string is usually
+ * a {@link java.text.DecimalFormat} pattern, with the extensions described in the Manual (see "Extended Jav decimal
+ * format"). There are some names that aren't parsed as patterns: "number", "currency", "percent", which
+ * corresponds to the predefined formats with similar name in {@link NumberFormat}-s, and also "computer" that
+ * behaves like {@code someNumber?c} in templates.
+ *
+ * <p>Note that the resulting {@link java.text.DecimalFormat}-s are globally cached, and threading issues are
+ * addressed by cloning the cached instance before returning it. So it just makes object creation faster, but doesn't
+ * eliminate it.
+ */
+public class JavaTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+    
+    public static final JavaTemplateNumberFormatFactory INSTANCE = new JavaTemplateNumberFormatFactory();
+    
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+
+    private static final ConcurrentHashMap<CacheKey, NumberFormat> GLOBAL_FORMAT_CACHE
+            = new ConcurrentHashMap<>();
+    private static final int LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE = 1024;
+    
+    private JavaTemplateNumberFormatFactory() {
+        // Not meant to be instantiated
+    }
+
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws InvalidFormatParametersException {
+        CacheKey cacheKey = new CacheKey(params, locale);
+        NumberFormat jFormat = GLOBAL_FORMAT_CACHE.get(cacheKey);
+        if (jFormat == null) {
+            if ("number".equals(params)) {
+                jFormat = NumberFormat.getNumberInstance(locale);
+            } else if ("currency".equals(params)) {
+                jFormat = NumberFormat.getCurrencyInstance(locale);
+            } else if ("percent".equals(params)) {
+                jFormat = NumberFormat.getPercentInstance(locale);
+            } else if ("computer".equals(params)) {
+                jFormat = env.getCNumberFormat();
+            } else {
+                try {
+                    jFormat = ExtendedDecimalFormatParser.parse(params, locale);
+                } catch (ParseException e) {
+                    String msg = e.getMessage();
+                    throw new InvalidFormatParametersException(
+                            msg != null ? msg : "Invalid DecimalFormat pattern", e);
+                }
+            }
+
+            if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE) {
+                boolean triggered = false;
+                synchronized (JavaTemplateNumberFormatFactory.class) {
+                    if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE) {
+                        triggered = true;
+                        GLOBAL_FORMAT_CACHE.clear();
+                    }
+                }
+                if (triggered) {
+                    LOG.warn("Global Java NumberFormat cache has exceeded {} entries => cache flushed. "
+                            + "Typical cause: Some template generates high variety of format pattern strings.",
+                            LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE);
+                }
+            }
+            
+            NumberFormat prevJFormat = GLOBAL_FORMAT_CACHE.putIfAbsent(cacheKey, jFormat);
+            if (prevJFormat != null) {
+                jFormat = prevJFormat;
+            }
+        }  // if cache miss
+        
+        // JFormat-s aren't thread-safe; must clone it
+        jFormat = (NumberFormat) jFormat.clone();
+        
+        return new JavaTemplateNumberFormat(jFormat, params);
+    }
+
+    private static final class CacheKey {
+        private final String pattern;
+        private final Locale locale;
+
+        CacheKey(String pattern, Locale locale) {
+            this.pattern = pattern;
+            this.locale = locale;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof CacheKey) {
+                CacheKey fk = (CacheKey) o;
+                return fk.pattern.equals(pattern) && fk.locale.equals(locale);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return pattern.hashCode() ^ locale.hashCode();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java
new file mode 100644
index 0000000..4ca415d
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java
@@ -0,0 +1,94 @@
+/*
+ * 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;
+
+/**
+ * XML Schema format.
+ */
+class XSTemplateDateFormat extends ISOLikeTemplateDateFormat {
+
+    XSTemplateDateFormat(
+            String settingValue, int parsingStart,
+            int dateType,
+            boolean zonelessInput,
+            TimeZone timeZone,
+            ISOLikeTemplateDateFormatFactory factory,
+            Environment env)
+            throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException {
+        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.dateToXSString(
+                date, datePart, timePart, offsetPart, accuracy, timeZone, calendarFactory);
+    }
+
+    @Override
+    protected Date parseDate(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter)
+            throws DateParseException {
+        return _DateUtil.parseXSDate(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected Date parseTime(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter)
+            throws DateParseException {
+        return _DateUtil.parseXSTime(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected Date parseDateTime(String s, TimeZone tz,
+            CalendarFieldsToDateConverter calToDateConverter) throws DateParseException {
+        return _DateUtil.parseXSDateTime(s, tz, calToDateConverter);
+    }
+
+    @Override
+    protected String getDateDescription() {
+        return "W3C XML Schema date";
+    }
+
+    @Override
+    protected String getTimeDescription() {
+        return "W3C XML Schema time";
+    }
+
+    @Override
+    protected String getDateTimeDescription() {
+        return "W3C XML Schema dateTime";
+    }
+
+    @Override
+    protected boolean isXSMode() {
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java
new file mode 100644
index 0000000..e9caa40
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.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.valueformat.impl;
+
+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;
+
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Creates {@link TemplateDateFormat}-s that follows the W3C XML Schema date, time and dateTime syntax.
+ */
+public final class XSTemplateDateFormatFactory extends ISOLikeTemplateDateFormatFactory {
+    
+    public static final XSTemplateDateFormatFactory INSTANCE = new XSTemplateDateFormatFactory();
+
+    private XSTemplateDateFormatFactory() {
+        // 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 XSTemplateDateFormat(
+                params, 2,
+                dateType, zonelessInput,
+                timeZone, this, env);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/manual/en_US/FM3-CHANGE-LOG.txt
----------------------------------------------------------------------
diff --git a/src/manual/en_US/FM3-CHANGE-LOG.txt b/src/manual/en_US/FM3-CHANGE-LOG.txt
index a471514..94b62ad 100644
--- a/src/manual/en_US/FM3-CHANGE-LOG.txt
+++ b/src/manual/en_US/FM3-CHANGE-LOG.txt
@@ -71,6 +71,7 @@ the FreeMarer 3 changelog here:
   to org.apache.freemarker.core.templateresolver (because later we will have a class called
   TemplateResolver, which is the central class of loading and caching and template name rules).
   OutputFormat realted classes were moved to org.apache.freemarker.core.outputformat.
+  ValueFormat related classes were moved to org.apache.freemarker.core.valueformat.
   freemarker.ext.beans were moved under org.apache.freemarker.core.model.impl.beans for now (but later
   we only want a DefaultObject wrapper, no BeansWrapper, so this will change) and freemarker.ext.dom
   was moved to org.apache.freemarker.core.model.impl.dom.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/ConfigurationTest.java b/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
index cd8594b..93bef63 100644
--- a/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
+++ b/src/test/java/org/apache/freemarker/core/ConfigurationTest.java
@@ -85,6 +85,8 @@ import org.apache.freemarker.core.util._DateUtil;
 import org.apache.freemarker.core.util._NullArgumentException;
 import org.apache.freemarker.core.util._NullWriter;
 import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/test/java/org/apache/freemarker/core/DateFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/DateFormatTest.java b/src/test/java/org/apache/freemarker/core/DateFormatTest.java
index 3e7cb41..8bcca28 100644
--- a/src/test/java/org/apache/freemarker/core/DateFormatTest.java
+++ b/src/test/java/org/apache/freemarker/core/DateFormatTest.java
@@ -41,6 +41,10 @@ import org.apache.freemarker.core.userpkg.EpochMillisDivTemplateDateFormatFactor
 import org.apache.freemarker.core.userpkg.EpochMillisTemplateDateFormatFactory;
 import org.apache.freemarker.core.userpkg.HTMLISOTemplateDateFormatFactory;
 import org.apache.freemarker.core.userpkg.LocAndTZSensitiveTemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.AliasTemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.UndefinedCustomFormatException;
 import org.apache.freemarker.test.TemplateTest;
 import org.junit.Before;
 import org.junit.Test;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/test/java/org/apache/freemarker/core/ExtendedDecimalFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/ExtendedDecimalFormatTest.java b/src/test/java/org/apache/freemarker/core/ExtendedDecimalFormatTest.java
deleted file mode 100644
index cc9e136..0000000
--- a/src/test/java/org/apache/freemarker/core/ExtendedDecimalFormatTest.java
+++ /dev/null
@@ -1,341 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.freemarker.core;
-
-import static org.apache.freemarker.test.hamcerst.Matchers.containsStringIgnoringCase;
-import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.containsString;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-import java.text.DecimalFormat;
-import java.text.ParseException;
-import java.util.Locale;
-
-import org.apache.freemarker.test.TemplateTest;
-import org.junit.Test;
-
-public class ExtendedDecimalFormatTest extends TemplateTest {
-    
-    private static final Locale LOC = Locale.US;
-    
-    @Test
-    public void testNonExtended() throws ParseException {
-        for (String fStr : new String[] { "0.00", "0.###", "#,#0.###", "#0.####", "0.0;m", "0.0;",
-                "0'x'", "0'x';'m'", "0';'", "0';';m", "0';';'#'m';'", "0';;'", "" }) {
-            assertFormatsEquivalent(new DecimalFormat(fStr), ExtendedDecimalFormatParser.parse(fStr, LOC));
-        }
-        
-        try {
-            new DecimalFormat(";");
-            fail();
-        } catch (IllegalArgumentException e) {
-            // Expected
-        }
-        try {
-            ExtendedDecimalFormatParser.parse(";", LOC);
-        } catch (ParseException e) {
-            // Expected
-        }
-    }
-
-    @Test
-    public void testNonExtended2() throws ParseException {
-        assertFormatsEquivalent(new DecimalFormat("0.0"), ExtendedDecimalFormatParser.parse("0.0;", LOC));
-        assertFormatsEquivalent(new DecimalFormat("0.0"), ExtendedDecimalFormatParser.parse("0.0;;", LOC));
-        assertFormatsEquivalent(new DecimalFormat("0.0;m"), ExtendedDecimalFormatParser.parse("0.0;m;", LOC));
-        assertFormatsEquivalent(new DecimalFormat(""), ExtendedDecimalFormatParser.parse(";;", LOC));
-        assertFormatsEquivalent(new DecimalFormat("0'x'"), ExtendedDecimalFormatParser.parse("0'x';;", LOC));
-        assertFormatsEquivalent(new DecimalFormat("0'x';'m'"), ExtendedDecimalFormatParser.parse("0'x';'m';", LOC));
-        assertFormatsEquivalent(new DecimalFormat("0';'"), ExtendedDecimalFormatParser.parse("0';';;", LOC));
-        assertFormatsEquivalent(new DecimalFormat("0';';m"), ExtendedDecimalFormatParser.parse("0';';m;", LOC));
-        assertFormatsEquivalent(new DecimalFormat("0';';'#'m';'"), ExtendedDecimalFormatParser.parse("0';';'#'m';';",
-                LOC));
-        assertFormatsEquivalent(new DecimalFormat("0';;'"), ExtendedDecimalFormatParser.parse("0';;';;", LOC));
-        
-        try {
-            new DecimalFormat(";m");
-            fail();
-        } catch (IllegalArgumentException e) {
-            // Expected
-        }
-        try {
-            new DecimalFormat("; ;");
-            fail();
-        } catch (IllegalArgumentException e) {
-            // Expected
-        }
-        try {
-            ExtendedDecimalFormatParser.parse("; ;", LOC);
-            fail();
-        } catch (ParseException e) {
-            // Expected
-        }
-        try {
-            ExtendedDecimalFormatParser.parse(";m", LOC);
-            fail();
-        } catch (ParseException e) {
-            // Expected
-        }
-        try {
-            ExtendedDecimalFormatParser.parse(";m;", LOC);
-            fail();
-        } catch (ParseException e) {
-            // Expected
-        }
-    }
-    
-    @SuppressWarnings("boxing")
-    @Test
-    public void testExtendedParamsParsing() throws ParseException {
-        for (String fs : new String[] {
-                "00.##;; decimalSeparator='D'",
-                "00.##;;decimalSeparator=D",
-                "00.##;;  decimalSeparator  =  D ", "00.##;; decimalSeparator = 'D' " }) {
-            assertFormatted(fs, 1.125, "01D12");
-        }
-        for (String fs : new String[] {
-                ",#0.0;; decimalSeparator=D, groupingSeparator=_",
-                ",#0.0;;decimalSeparator=D,groupingSeparator=_",
-                ",#0.0;; decimalSeparator = D , groupingSeparator = _ ",
-                ",#0.0;; decimalSeparator='D', groupingSeparator='_'"
-                }) {
-            assertFormatted(fs, 12345, "1_23_45D0");
-        }
-        
-        assertFormatted("0.0;;infinity=infinity", Double.POSITIVE_INFINITY, "infinity");
-        assertFormatted("0.0;;infinity='infinity'", Double.POSITIVE_INFINITY, "infinity");
-        assertFormatted("0.0;;infinity=\"infinity\"", Double.POSITIVE_INFINITY, "infinity");
-        assertFormatted("0.0;;infinity=''", Double.POSITIVE_INFINITY, "");
-        assertFormatted("0.0;;infinity=\"\"", Double.POSITIVE_INFINITY, "");
-        assertFormatted("0.0;;infinity='x''y'", Double.POSITIVE_INFINITY, "x'y");
-        assertFormatted("0.0;;infinity=\"x'y\"", Double.POSITIVE_INFINITY, "x'y");
-        assertFormatted("0.0;;infinity='x\"\"y'", Double.POSITIVE_INFINITY, "x\"\"y");
-        assertFormatted("0.0;;infinity=\"x''y\"", Double.POSITIVE_INFINITY, "x''y");
-        assertFormatted("0.0;;decimalSeparator=''''", 1, "1'0");
-        assertFormatted("0.0;;decimalSeparator=\"'\"", 1, "1'0");
-        assertFormatted("0.0;;decimalSeparator='\"'", 1, "1\"0");
-        assertFormatted("0.0;;decimalSeparator=\"\"\"\"", 1, "1\"0");
-        
-        try {
-            ExtendedDecimalFormatParser.parse(";;decimalSeparator=D,", LOC);
-            fail();
-        } catch (java.text.ParseException e) {
-            assertThat(e.getMessage(),
-                    allOf(containsStringIgnoringCase("expected a(n) name"), containsString(" end of ")));
-        }
-        try {
-            ExtendedDecimalFormatParser.parse(";;foo=D,", LOC);
-            fail();
-        } catch (java.text.ParseException e) {
-            assertThat(e.getMessage(),
-                    allOf(containsString("\"foo\""), containsString("name")));
-        }
-        try {
-            ExtendedDecimalFormatParser.parse(";;decimalSeparator='D", LOC);
-            fail();
-        } catch (java.text.ParseException e) {
-            assertThat(e.getMessage(),
-                    allOf(containsString("quotation"), containsString("closed")));
-        }
-        try {
-            ExtendedDecimalFormatParser.parse(";;decimalSeparator=\"D", LOC);
-            fail();
-        } catch (java.text.ParseException e) {
-            assertThat(e.getMessage(),
-                    allOf(containsString("quotation"), containsString("closed")));
-        }
-        try {
-            ExtendedDecimalFormatParser.parse(";;decimalSeparator='D'groupingSeparator=G", LOC);
-            fail();
-        } catch (java.text.ParseException e) {
-            assertThat(e.getMessage(), allOf(
-                    containsString("separator"), containsString("whitespace"), containsString("comma")));
-        }
-        try {
-            ExtendedDecimalFormatParser.parse(";;decimalSeparator=., groupingSeparator=G", LOC);
-            fail();
-        } catch (java.text.ParseException e) {
-            assertThat(e.getMessage(), allOf(
-                    containsStringIgnoringCase("expected a(n) value"), containsString("., gr[...]")));
-        }
-        try {
-            ExtendedDecimalFormatParser.parse("0.0;;decimalSeparator=''", LOC);
-            fail();
-        } catch (java.text.ParseException e) {
-            assertThat(e.getMessage(), allOf(
-                    containsStringIgnoringCase("\"decimalSeparator\""), containsString("exactly 1 char")));
-        }
-        try {
-            ExtendedDecimalFormatParser.parse("0.0;;multipier=ten", LOC);
-            fail();
-        } catch (java.text.ParseException e) {
-            assertThat(e.getMessage(), allOf(
-                    containsString("\"multipier\""), containsString("\"ten\""), containsString("integer")));
-        }
-    }
-    
-    @SuppressWarnings("boxing")
-    @Test
-    public void testExtendedParamsEffect() throws ParseException {
-        assertFormatted("0",
-                1.5, "2", 2.5, "2", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-2", -1.6, "-2");
-        assertFormatted("0;; roundingMode=halfEven",
-                1.5, "2", 2.5, "2", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-2", -1.6, "-2");
-        assertFormatted("0;; roundingMode=halfUp",
-                1.5, "2", 2.5, "3", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-3", -1.6, "-2");
-        assertFormatted("0;; roundingMode=halfDown",
-                1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-2");
-        assertFormatted("0;; roundingMode=floor",
-                1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "1", -1.4, "-2", -1.5, "-2", -2.5, "-3", -1.6, "-2");
-        assertFormatted("0;; roundingMode=ceiling",
-                1.5, "2", 2.5, "3", 3.5, "4", 1.4, "2", 1.6, "2", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-1");
-        assertFormatted("0;; roundingMode=up",
-                1.5, "2", 2.5, "3", 3.5, "4", 1.4, "2", 1.6, "2", -1.4, "-2", -1.5, "-2", -2.5, "-3", -1.6, "-2");
-        assertFormatted("0;; roundingMode=down",
-                1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "1", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-1");
-        assertFormatted("0;; roundingMode=unnecessary", 2, "2");
-        try {
-            assertFormatted("0;; roundingMode=unnecessary", 2.5, "2");
-            fail();
-        } catch (ArithmeticException e) {
-            // Expected
-        }
-
-        assertFormatted("0.##;; multipier=100", 12.345, "1234.5");
-        assertFormatted("0.##;; multipier=1000", 12.345, "12345");
-        
-        assertFormatted(",##0.##;; groupingSeparator=_ decimalSeparator=D", 12345.1, "12_345D1", 1, "1");
-        
-        assertFormatted("0.##E0;; exponentSeparator='*10^'", 12345.1, "1.23*10^4");
-        
-        assertFormatted("0.##;; minusSign=m", -1, "m1", 1, "1");
-        
-        assertFormatted("0.##;; infinity=foo", Double.POSITIVE_INFINITY, "foo", Double.NEGATIVE_INFINITY, "-foo");
-        
-        assertFormatted("0.##;; nan=foo", Double.NaN, "foo");
-        
-        assertFormatted("0%;; percent='c'", 0.75, "75c");
-        
-        assertFormatted("0\u2030;; perMill='m'", 0.75, "750m");
-        
-        assertFormatted("0.00;; zeroDigit='@'", 10.5, "A@.E@");
-        
-        assertFormatted("0;; currencyCode=USD", 10, "10");
-        assertFormatted("0 \u00A4;; currencyCode=USD", 10, "10 $");
-        assertFormatted("0 \u00A4\u00A4;; currencyCode=USD", 10, "10 USD");
-        assertFormatted(Locale.GERMANY, "0 \u00A4;; currencyCode=EUR", 10, "10 \u20AC");
-        assertFormatted(Locale.GERMANY, "0 \u00A4\u00A4;; currencyCode=EUR", 10, "10 EUR");
-        try {
-            assertFormatted("0;; currencyCode=USDX", 10, "10");
-        } catch (ParseException e) {
-            assertThat(e.getMessage(), containsString("ISO 4217"));
-        }
-        assertFormatted("0 \u00A4;; currencyCode=USD currencySymbol=bucks", 10, "10 bucks");
-     // Order doesn't mater:
-        assertFormatted("0 \u00A4;; currencySymbol=bucks currencyCode=USD", 10, "10 bucks");
-        // International symbol isn't affected:
-        assertFormatted("0 \u00A4\u00A4;; currencyCode=USD currencySymbol=bucks", 10, "10 USD");
-        
-        assertFormatted("0.0 \u00A4;; monetaryDecimalSeparator=m", 10.5, "10m5 $");
-        assertFormatted("0.0 kg;; monetaryDecimalSeparator=m", 10.5, "10.5 kg");
-        assertFormatted("0.0 \u00A4;; decimalSeparator=d", 10.5, "10.5 $");
-        assertFormatted("0.0 kg;; decimalSeparator=d", 10.5, "10d5 kg");
-        assertFormatted("0.0 \u00A4;; monetaryDecimalSeparator=m decimalSeparator=d", 10.5, "10m5 $");
-        assertFormatted("0.0 kg;; monetaryDecimalSeparator=m decimalSeparator=d", 10.5, "10d5 kg");
-    }
-    
-    @Test
-    public void testLocale() throws ParseException {
-        assertEquals("1000.0", ExtendedDecimalFormatParser.parse("0.0", Locale.US).format(1000));
-        assertEquals("1000,0", ExtendedDecimalFormatParser.parse("0.0", Locale.FRANCE).format(1000));
-        assertEquals("1_000.0", ExtendedDecimalFormatParser.parse(",000.0;;groupingSeparator=_", Locale.US).format(1000));
-        assertEquals("1_000,0", ExtendedDecimalFormatParser.parse(",000.0;;groupingSeparator=_", Locale.FRANCE).format(1000));
-    }
-    
-    @Test
-    public void testTemplates() throws IOException, TemplateException {
-        Configuration cfg = getConfiguration();
-        cfg.setLocale(Locale.US);
-        
-        cfg.setNumberFormat(",000.#");
-        assertOutput("${1000.15} ${1000.25}", "1,000.2 1,000.2");
-        cfg.setNumberFormat(",000.#;; roundingMode=halfUp groupingSeparator=_");
-        assertOutput("${1000.15} ${1000.25}", "1_000.2 1_000.3");
-        cfg.setLocale(Locale.GERMANY);
-        assertOutput("${1000.15} ${1000.25}", "1_000,2 1_000,3");
-        cfg.setLocale(Locale.US);
-        assertOutput(
-                "${1000.15}; "
-                + "${1000.15?string(',##.#;;groupingSeparator=\" \"')}; "
-                + "<#setting locale='de_DE'>${1000.15}; "
-                + "<#setting numberFormat='0.0;;roundingMode=down'>${1000.15}",
-                "1_000.2; 10 00.2; 1_000,2; 1000,1");
-        assertErrorContains("${1?string('#E')}",
-                TemplateException.class, "\"#E\"", "format string", "exponential");
-        assertErrorContains("<#setting numberFormat='#E'>${1}",
-                TemplateException.class, "\"#E\"", "format string", "exponential");
-        assertErrorContains("<#setting numberFormat=';;foo=bar'>${1}",
-                TemplateException.class, "\"foo\"", "supported");
-        assertErrorContains("<#setting numberFormat='0;;roundingMode=unnecessary'>${1.5}",
-                TemplateException.class, "can't format", "1.5", "UNNECESSARY");
-    }
-
-    private void assertFormatted(String formatString, Object... numberAndExpectedOutput) throws ParseException {
-        assertFormatted(LOC, formatString, numberAndExpectedOutput);
-    }
-    
-    private void assertFormatted(Locale loc, String formatString, Object... numberAndExpectedOutput) throws ParseException {
-        if (numberAndExpectedOutput.length % 2 != 0) {
-            throw new IllegalArgumentException();
-        }
-        
-        DecimalFormat df = ExtendedDecimalFormatParser.parse(formatString, loc);
-        Number num = null;
-        for (int i = 0; i < numberAndExpectedOutput.length; i++) {
-            if (i % 2 == 0) {
-                num = (Number) numberAndExpectedOutput[i];
-            } else {
-                assertEquals(numberAndExpectedOutput[i], df.format(num));
-            }
-        }
-    }
-    
-    private void assertFormatsEquivalent(DecimalFormat dfExpected, DecimalFormat dfActual) {
-        for (int signum : new int[] { 1, -1 }) {
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 0);
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 0.5);
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 0.25);
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 0.125);
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 1);
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 10);
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 100);
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 1000);
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 10000);
-            assertFormatsEquivalent(dfExpected, dfActual, signum * 100000);
-        }
-    }
-
-    private void assertFormatsEquivalent(DecimalFormat dfExpected, DecimalFormat dfActual, double n) {
-        assertEquals(dfExpected.format(n), dfActual.format(n));
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/test/java/org/apache/freemarker/core/NumberFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/NumberFormatTest.java b/src/test/java/org/apache/freemarker/core/NumberFormatTest.java
deleted file mode 100644
index 0d5650a..0000000
--- a/src/test/java/org/apache/freemarker/core/NumberFormatTest.java
+++ /dev/null
@@ -1,321 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.freemarker.core;
-
-import static org.hamcrest.Matchers.instanceOf;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertThat;
-
-import java.io.IOException;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.util.Collections;
-import java.util.Locale;
-import java.util.Map;
-
-import org.apache.freemarker.core.model.TemplateDirectiveBody;
-import org.apache.freemarker.core.model.TemplateDirectiveModel;
-import org.apache.freemarker.core.model.TemplateModel;
-import org.apache.freemarker.core.model.TemplateModelException;
-import org.apache.freemarker.core.model.TemplateNumberModel;
-import org.apache.freemarker.core.model.impl.SimpleNumber;
-import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
-import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
-import org.apache.freemarker.core.userpkg.BaseNTemplateNumberFormatFactory;
-import org.apache.freemarker.core.userpkg.HexTemplateNumberFormatFactory;
-import org.apache.freemarker.core.userpkg.LocaleSensitiveTemplateNumberFormatFactory;
-import org.apache.freemarker.core.userpkg.PrintfGTemplateNumberFormatFactory;
-import org.apache.freemarker.test.TemplateTest;
-import org.junit.Before;
-import org.junit.Test;
-
-import com.google.common.collect.ImmutableMap;
-
-@SuppressWarnings("boxing")
-public class NumberFormatTest extends TemplateTest {
-    
-    @Before
-    public void setup() {
-        Configuration cfg = getConfiguration();
-        cfg.setLocale(Locale.US);
-        
-        cfg.setCustomNumberFormats(ImmutableMap.of(
-                "hex", HexTemplateNumberFormatFactory.INSTANCE,
-                "loc", LocaleSensitiveTemplateNumberFormatFactory.INSTANCE,
-                "base", BaseNTemplateNumberFormatFactory.INSTANCE,
-                "printfG", PrintfGTemplateNumberFormatFactory.INSTANCE));
-    }
-
-    @Test
-    public void testUnknownCustomFormat() throws Exception {
-        {
-            getConfiguration().setNumberFormat("@noSuchFormat");
-            Throwable exc = assertErrorContains("${1}", "\"@noSuchFormat\"", "\"noSuchFormat\"");
-            assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
-        }
-
-        {
-            getConfiguration().setNumberFormat("number");
-            Throwable exc = assertErrorContains("${1?string('@noSuchFormat2')}",
-                    "\"@noSuchFormat2\"", "\"noSuchFormat2\"");
-            assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
-        }
-    }
-    
-    @Test
-    public void testStringBI() throws Exception {
-        assertOutput("${11} ${11?string.@hex} ${12} ${12?string.@hex}", "11 b 12 c");
-    }
-
-    @Test
-    public void testSetting() throws Exception {
-        getConfiguration().setNumberFormat("@hex");
-        assertOutput("${11?string.number} ${11} ${12?string.number} ${12}", "11 b 12 c");
-    }
-
-    @Test
-    public void testSetting2() throws Exception {
-        assertOutput(
-                "<#setting numberFormat='@hex'>${11?string.number} ${11} ${12?string.number} ${12} ${13?string}"
-                + "<#setting numberFormat='@loc'>${11?string.number} ${11} ${12?string.number} ${12} ${13?string}",
-                "11 b 12 c d"
-                + "11 11_en_US 12 12_en_US 13_en_US");
-    }
-    
-    @Test
-    public void testUnformattableNumber() throws Exception {
-        getConfiguration().setNumberFormat("@hex");
-        assertErrorContains("${1.1}", "hexadecimal int", "doesn't fit into an int");
-    }
-
-    @Test
-    public void testLocaleSensitive() throws Exception {
-        Configuration cfg = getConfiguration();
-        cfg.setNumberFormat("@loc");
-        assertOutput("${1.1}", "1.1_en_US");
-        cfg.setLocale(Locale.GERMANY);
-        assertOutput("${1.1}", "1.1_de_DE");
-    }
-
-    @Test
-    public void testLocaleSensitive2() throws Exception {
-        Configuration cfg = getConfiguration();
-        cfg.setNumberFormat("@loc");
-        assertOutput("${1.1} <#setting locale='de_DE'>${1.1}", "1.1_en_US 1.1_de_DE");
-    }
-
-    @Test
-    public void testCustomParameterized() throws Exception {
-        Configuration cfg = getConfiguration();
-        cfg.setNumberFormat("@base 2");
-        assertOutput("${11}", "1011");
-        assertOutput("${11?string}", "1011");
-        assertOutput("${11?string.@base_3}", "102");
-        
-        assertErrorContains("${11?string.@base_xyz}", "\"@base_xyz\"", "\"xyz\"");
-        cfg.setNumberFormat("@base");
-        assertErrorContains("${11}", "\"@base\"", "format parameter is required");
-    }
-
-    @Test
-    public void testCustomWithFallback() throws Exception {
-        Configuration cfg = getConfiguration();
-        cfg.setNumberFormat("@base 2|0.0#");
-        assertOutput("${11}", "1011");
-        assertOutput("${11.34}", "11.34");
-        assertOutput("${11?string('@base 3|0.00')}", "102");
-        assertOutput("${11.2?string('@base 3|0.00')}", "11.20");
-    }
-
-    @Test
-    public void testEnvironmentGetters() throws Exception {
-        Template t = new Template(null, "", getConfiguration());
-        Environment env = t.createProcessingEnvironment(null, null);
-        
-        TemplateNumberFormat defF = env.getTemplateNumberFormat();
-        //
-        TemplateNumberFormat explF = env.getTemplateNumberFormat("0.00");
-        assertEquals("1.25", explF.formatToPlainText(new SimpleNumber(1.25)));
-        //
-        TemplateNumberFormat expl2F = env.getTemplateNumberFormat("@loc");
-        assertEquals("1.25_en_US", expl2F.formatToPlainText(new SimpleNumber(1.25)));
-        
-        TemplateNumberFormat explFFr = env.getTemplateNumberFormat("0.00", Locale.FRANCE);
-        assertNotSame(explF, explFFr);
-        assertEquals("1,25", explFFr.formatToPlainText(new SimpleNumber(1.25)));
-        //
-        TemplateNumberFormat expl2FFr = env.getTemplateNumberFormat("@loc", Locale.FRANCE);
-        assertEquals("1.25_fr_FR", expl2FFr.formatToPlainText(new SimpleNumber(1.25)));
-        
-        assertSame(env.getTemplateNumberFormat(), defF);
-        //
-        assertSame(env.getTemplateNumberFormat("0.00"), explF);
-        //
-        assertSame(env.getTemplateNumberFormat("@loc"), expl2F);
-    }
-    
-    /**
-     * ?string formats lazily (at least in 2.3.x), so it must make a snapshot of the format inputs when it's called.
-     */
-    @Test
-    public void testStringBIDoesSnapshot() throws Exception {
-        // TemplateNumberModel-s shouldn't change, but we have to keep BC when that still happens.
-        final MutableTemplateNumberModel nm = new MutableTemplateNumberModel();
-        nm.setNumber(123);
-        addToDataModel("n", nm);
-        addToDataModel("incN", new TemplateDirectiveModel() {
-            
-            @Override
-            public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
-                    throws TemplateException, IOException {
-                nm.setNumber(nm.getAsNumber().intValue() + 1);
-            }
-        });
-        assertOutput(
-                "<#assign s1 = n?string>"
-                + "<#setting numberFormat='@loc'>"
-                + "<#assign s2 = n?string>"
-                + "<#setting numberFormat='@hex'>"
-                + "<#assign s3 = n?string>"
-                + "${s1} ${s2} ${s3}",
-                "123 123_en_US 7b");
-        assertOutput(
-                "<#assign s1 = n?string>"
-                + "<@incN />"
-                + "<#assign s2 = n?string>"
-                + "${s1} ${s2}",
-                "123 124");
-    }
-
-    @Test
-    public void testNullInModel() throws Exception {
-        addToDataModel("n", new MutableTemplateNumberModel());
-        assertErrorContains("${n}", "nothing inside it");
-        assertErrorContains("${n?string}", "nothing inside it");
-    }
-    
-    @Test
-    public void testAtPrefix() throws Exception {
-        Configuration cfg = getConfiguration();
-        
-        cfg.setNumberFormat("@hex");
-        assertOutput("${10}", "a");
-        cfg.setNumberFormat("'@'0");
-        assertOutput("${10}", "@10");
-        cfg.setNumberFormat("@@0");
-        assertOutput("${10}", "@@10");
-        
-        cfg.setCustomNumberFormats(Collections.<String, TemplateNumberFormatFactory>emptyMap());
-        cfg.setNumberFormat("@hex");
-        assertErrorContains("${10}", "custom", "\"hex\"");
-        cfg.setNumberFormat("'@'0");
-        assertOutput("${10}", "@10");
-        cfg.setNumberFormat("@@0");
-        assertOutput("${10}", "@@10");
-    }
-
-    @Test
-    public void testAlieses() throws Exception {
-        Configuration cfg = getConfiguration();
-        cfg.setCustomNumberFormats(ImmutableMap.of(
-                "f", new AliasTemplateNumberFormatFactory("0.#'f'"),
-                "d", new AliasTemplateNumberFormatFactory("0.0#"),
-                "hex", HexTemplateNumberFormatFactory.INSTANCE));
-        
-        TemplateConfiguration tc = new TemplateConfiguration();
-        tc.setCustomNumberFormats(ImmutableMap.of(
-                "d", new AliasTemplateNumberFormatFactory("0.#'d'"),
-                "i", new AliasTemplateNumberFormatFactory("@hex")));
-        cfg.setTemplateConfigurations(new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*2*"), tc));
-        
-        String commonFtl = "${1?string.@f} ${1?string.@d} "
-                + "<#setting locale='fr_FR'>${1.5?string.@d} "
-                + "<#attempt>${10?string.@i}<#recover>E</#attempt>";
-        addTemplate("t1.ftl", commonFtl);
-        addTemplate("t2.ftl", commonFtl);
-        
-        assertOutputForNamed("t1.ftl", "1f 1.0 1,5 E");
-        assertOutputForNamed("t2.ftl", "1f 1d 1,5d a");
-    }
-
-    @Test
-    public void testAlieses2() throws Exception {
-        Configuration cfg = getConfiguration();
-        cfg.setCustomNumberFormats(ImmutableMap.of(
-                "n", new AliasTemplateNumberFormatFactory("0.0",
-                        ImmutableMap.of(
-                                new Locale("en"), "0.0'_en'",
-                                Locale.UK, "0.0'_en_GB'",
-                                Locale.FRANCE, "0.0'_fr_FR'"))));
-        cfg.setNumberFormat("@n");
-        assertOutput(
-                "<#setting locale='en_US'>${1} "
-                + "<#setting locale='en_GB'>${1} "
-                + "<#setting locale='en_GB_Win'>${1} "
-                + "<#setting locale='fr_FR'>${1} "
-                + "<#setting locale='hu_HU'>${1}",
-                "1.0_en 1.0_en_GB 1.0_en_GB 1,0_fr_FR 1,0");
-    }
-    
-    @Test
-    public void testMarkupFormat() throws IOException, TemplateException {
-        getConfiguration().setNumberFormat("@printfG_3");
-
-        String commonFTL = "${1234567} ${'cat:' + 1234567} ${0.0000123}";
-        String commonOutput = "1.23*10<sup>6</sup> cat:1.23*10<sup>6</sup> 1.23*10<sup>-5</sup>";
-        assertOutput(commonFTL, commonOutput);
-        assertOutput("<#ftl outputFormat='HTML'>" + commonFTL, commonOutput);
-        assertOutput("<#escape x as x?html>" + commonFTL + "</#escape>", commonOutput);
-        assertOutput("<#escape x as x?xhtml>" + commonFTL + "</#escape>", commonOutput);
-        assertOutput("<#escape x as x?xml>" + commonFTL + "</#escape>", commonOutput);
-        assertOutput("${\"" + commonFTL + "\"}", "1.23*10<sup>6</sup> cat:1.23*10<sup>6</sup> 1.23*10<sup>-5</sup>");
-        assertErrorContains("<#ftl outputFormat='plainText'>" + commonFTL, "HTML", "plainText", "conversion");
-    }
-
-    @Test
-    public void testPrintG() throws IOException, TemplateException {
-        for (Number n : new Number[] {
-                1234567, 1234567L, 1234567d, 1234567f, BigInteger.valueOf(1234567), BigDecimal.valueOf(1234567) }) {
-            addToDataModel("n", n);
-            
-            assertOutput("${n?string.@printfG}", "1.23457E+06");
-            assertOutput("${n?string.@printfG_3}", "1.23E+06");
-            assertOutput("${n?string.@printfG_7}", "1234567");
-            assertOutput("${0.0000123?string.@printfG}", "1.23000E-05");
-        }
-    }
-    
-    private static class MutableTemplateNumberModel implements TemplateNumberModel {
-        
-        private Number number;
-
-        public void setNumber(Number number) {
-            this.number = number;
-        }
-
-        @Override
-        public Number getAsNumber() throws TemplateModelException {
-            return number;
-        }
-        
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java b/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
index 6af8a25..1348ac0 100644
--- a/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
+++ b/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
@@ -61,6 +61,8 @@ import org.apache.freemarker.core.userpkg.HexTemplateNumberFormatFactory;
 import org.apache.freemarker.core.userpkg.LocAndTZSensitiveTemplateDateFormatFactory;
 import org.apache.freemarker.core.userpkg.LocaleSensitiveTemplateNumberFormatFactory;
 import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java b/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java
index be49664..4e82c71 100644
--- a/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java
+++ b/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java
@@ -23,13 +23,13 @@ import java.util.Locale;
 import java.util.TimeZone;
 
 import org.apache.freemarker.core.Environment;
-import org.apache.freemarker.core.InvalidFormatParametersException;
-import org.apache.freemarker.core.TemplateDateFormat;
-import org.apache.freemarker.core.TemplateDateFormatFactory;
-import org.apache.freemarker.core.TemplateFormatUtil;
-import org.apache.freemarker.core.UnformattableValueException;
-import org.apache.freemarker.core.UnknownDateTypeFormattingUnsupportedException;
-import org.apache.freemarker.core.UnparsableValueException;
+import org.apache.freemarker.core.valueformat.InvalidFormatParametersException;
+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.UnformattableValueException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.UnparsableValueException;
 import org.apache.freemarker.core.model.TemplateDateModel;
 import org.apache.freemarker.core.model.TemplateModelException;
 



Mime
View raw message