freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [37/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
Date Sun, 14 May 2017 10:53:20 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
new file mode 100644
index 0000000..6713200
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
@@ -0,0 +1,3213 @@
+/*
+ * 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 java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.text.Collator;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateDirectiveBody;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateNodeModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateTransformModel;
+import org.apache.freemarker.core.model.TransformControl;
+import org.apache.freemarker.core.model.impl.SimpleHash;
+import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.TemplateResolver;
+import org.apache.freemarker.core.templateresolver._CacheAPI;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory;
+import org.apache.freemarker.core.util._NullWriter;
+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.TemplateNumberFormat;
+import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateValueFormatException;
+import org.apache.freemarker.core.valueformat.UndefinedCustomFormatException;
+import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException;
+import org.apache.freemarker.core.valueformat.impl.ISOTemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.JavaTemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.JavaTemplateNumberFormatFactory;
+import org.apache.freemarker.core.valueformat.impl.XSTemplateDateFormatFactory;
+import org.slf4j.Logger;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Object that represents the runtime environment during template processing. For every invocation of a
+ * <tt>Template.process()</tt> method, a new instance of this object is created, and then discarded when
+ * <tt>process()</tt> returns. This object stores the set of temporary variables created by the template, the value of
+ * settings set by the template, the reference to the data model root, etc. Everything that is needed to fulfill the
+ * template processing job.
+ *
+ * <p>
+ * Data models that need to access the <tt>Environment</tt> object that represents the template processing on the
+ * current thread can use the {@link #getCurrentEnvironment()} method.
+ *
+ * <p>
+ * If you need to modify or read this object before or after the <tt>process</tt> call, use
+ * {@link Template#createProcessingEnvironment(Object rootMap, Writer out, ObjectWrapper wrapper)}
+ */
+public final class Environment extends MutableProcessingConfiguration<Environment> implements CustomStateScope {
+    
+    private static final ThreadLocal<Environment> TLS_ENVIRONMENT = new ThreadLocal();
+
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+    private static final Logger LOG_ATTEMPT = _CoreLogs.ATTEMPT;
+
+    // Do not use this object directly; deepClone it first! DecimalFormat isn't
+    // thread-safe.
+    private static final DecimalFormat C_NUMBER_FORMAT = new DecimalFormat(
+            "0.################",
+            new DecimalFormatSymbols(Locale.US));
+
+    static {
+        C_NUMBER_FORMAT.setGroupingUsed(false);
+        C_NUMBER_FORMAT.setDecimalSeparatorAlwaysShown(false);
+    }
+
+    private final Configuration configuration;
+    private final TemplateHashModel rootDataModel;
+    private ASTElement[] instructionStack = new ASTElement[16];
+    private int instructionStackSize = 0;
+    private final ArrayList recoveredErrorStack = new ArrayList();
+
+    private TemplateNumberFormat cachedTemplateNumberFormat;
+    private Map<String, TemplateNumberFormat> cachedTemplateNumberFormats;
+    private Map<CustomStateKey, Object> customStateMap;
+
+    private TemplateBooleanFormat cachedTemplateBooleanFormat;
+
+    /**
+     * Stores the date/time/date-time formatters that are used when no format is explicitly given at the place of
+     * formatting. That is, in situations like ${lastModified} or even ${lastModified?date}, but not in situations like
+     * ${lastModified?string.iso}.
+     * 
+     * <p>
+     * The index of the array is calculated from what kind of formatter we want (see
+     * {@link #getTemplateDateFormatCacheArrayIndex(int, boolean, boolean)}):<br>
+     * Zoned input: 0: U, 1: T, 2: D, 3: DT<br>
+     * Zoneless input: 4: U, 5: T, 6: D, 7: DT<br>
+     * SQL D T TZ + Zoned input: 8: U, 9: T, 10: D, 11: DT<br>
+     * SQL D T TZ + Zoneless input: 12: U, 13: T, 14: D, 15: DT
+     * 
+     * <p>
+     * This is a lazily filled cache. It starts out as {@code null}, then when first needed the array will be created.
+     * The array elements also start out as {@code null}-s, and they are filled as the particular kind of formatter is
+     * first needed.
+     */
+    private TemplateDateFormat[] cachedTempDateFormatArray;
+    /** Similar to {@link #cachedTempDateFormatArray}, but used when a formatting string was specified. */
+    private HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray;
+    private static final int CACHED_TDFS_ZONELESS_INPUT_OFFS = 4;
+    private static final int CACHED_TDFS_SQL_D_T_TZ_OFFS = CACHED_TDFS_ZONELESS_INPUT_OFFS * 2;
+    private static final int CACHED_TDFS_LENGTH = CACHED_TDFS_SQL_D_T_TZ_OFFS * 2;
+
+    /** Caches the result of {@link #isSQLDateAndTimeTimeZoneSameAsNormal()}. */
+    private Boolean cachedSQLDateAndTimeTimeZoneSameAsNormal;
+
+    private NumberFormat cNumberFormat;
+
+    /**
+     * Used by the "iso_" built-ins to accelerate formatting.
+     * 
+     * @see #getISOBuiltInCalendarFactory()
+     */
+    private DateToISO8601CalendarFactory isoBuiltInCalendarFactory;
+
+    private Collator cachedCollator;
+
+    private Writer out;
+    private ASTDirMacro.Context currentMacroContext;
+    private LocalContextStack localContextStack;
+    private final Template mainTemplate;
+    private final Namespace mainNamespace;
+    private Namespace currentNamespace, globalNamespace;
+    private HashMap<String, Namespace> loadedLibs;
+
+    private boolean inAttemptBlock;
+    private Throwable lastThrowable;
+
+    private TemplateModel lastReturnValue;
+    private HashMap macroToNamespaceLookup = new HashMap();
+
+    private TemplateNodeModel currentVisitorNode;
+    private TemplateSequenceModel nodeNamespaces;
+    // Things we keep track of for the fallback mechanism.
+    private int nodeNamespaceIndex;
+    private String currentNodeName, currentNodeNS;
+
+    private Charset cachedURLEscapingCharset;
+    private boolean cachedURLEscapingCharsetSet;
+
+    private boolean fastInvalidReferenceExceptions;
+
+    /**
+     * Retrieves the environment object associated with the current thread, or {@code null} if there's no template
+     * processing going on in this thread. Data model implementations that need access to the environment can call this
+     * method to obtain the environment object that represents the template processing that is currently running on the
+     * current thread.
+     */
+    public static Environment getCurrentEnvironment() {
+        return TLS_ENVIRONMENT.get();
+    }
+
+    public static Environment getCurrentEnvironmentNotNull() {
+        Environment currentEnvironment = getCurrentEnvironment();
+        if (currentEnvironment == null) {
+            throw new IllegalStateException("There's no FreeMarker Environemnt in this this thread.");
+        }
+        return currentEnvironment;
+    }
+
+    static void setCurrentEnvironment(Environment env) {
+        TLS_ENVIRONMENT.set(env);
+    }
+
+    public Environment(Template template, final TemplateHashModel rootDataModel, Writer out) {
+        mainTemplate = template;
+        configuration = template.getConfiguration();
+        globalNamespace = new Namespace(null);
+        currentNamespace = mainNamespace = new Namespace(mainTemplate);
+        this.out = out;
+        this.rootDataModel = rootDataModel;
+        importMacros(template);
+    }
+
+    /**
+     * Returns the topmost {@link Template}, with other words, the one for which this {@link Environment} was created.
+     * That template will never change, like {@code #include} or macro calls don't change it.
+     * 
+     * @see #getCurrentNamespace()
+     * 
+     * @since 2.3.22
+     */
+    public Template getMainTemplate() {
+        return mainTemplate;
+    }
+
+    /**
+     * Returns the {@link Template} that we are "lexically" inside at the moment. This template will change when
+     * entering an {@code #include} or calling a macro or function in another template, or returning to yet another
+     * template with {@code #nested}. As such, it's useful in {@link TemplateDirectiveModel} to find out if from where
+     * the directive was called from.
+     * 
+     * @see #getMainTemplate()
+     * @see #getCurrentNamespace()
+     * 
+     * @since 2.3.23
+     */
+    @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm")
+    public Template getCurrentTemplate() {
+        int ln = instructionStackSize;
+        return ln == 0 ? getMainTemplate() : instructionStack[ln - 1].getTemplate();
+    }
+
+    public Template getCurrentTemplateNotNull() {
+        Template currentTemplate = getCurrentTemplate();
+        if (currentTemplate == null) {
+            throw new IllegalStateException("There's no current template at the moment.");
+        }
+        return currentTemplate;
+    }
+
+    /**
+     * Gets the currently executing <em>custom</em> directive's call place information, or {@code null} if there's no
+     * executing custom directive. This currently only works for calls made from templates with the {@code <@...>}
+     * syntax. This should only be called from the {@link TemplateDirectiveModel} that was invoked with {@code <@...>},
+     * otherwise its return value is not defined by this API (it's usually {@code null}).
+     * 
+     * @since 2.3.22
+     */
+    @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm")
+    public DirectiveCallPlace getCurrentDirectiveCallPlace() {
+        int ln = instructionStackSize;
+        if (ln == 0) return null;
+        ASTElement te = instructionStack[ln - 1];
+        if (te instanceof ASTDirUserDefined) return (ASTDirUserDefined) te;
+        if (te instanceof ASTDirMacro && ln > 1 && instructionStack[ln - 2] instanceof ASTDirUserDefined) {
+            return (ASTDirUserDefined) instructionStack[ln - 2];
+        }
+        return null;
+    }
+
+    /**
+     * Deletes cached values that meant to be valid only during a single template execution.
+     */
+    private void clearCachedValues() {
+        cachedTemplateNumberFormats = null;
+        cachedTemplateNumberFormat = null;
+
+        cachedTempDateFormatArray = null;
+        cachedTempDateFormatsByFmtStrArray = null;
+
+        cachedCollator = null;
+        cachedURLEscapingCharset = null;
+        cachedURLEscapingCharsetSet = false;
+    }
+
+    /**
+     * Processes the template to which this environment belongs to.
+     */
+    public void process() throws TemplateException, IOException {
+        Environment savedEnv = TLS_ENVIRONMENT.get();
+        TLS_ENVIRONMENT.set(this);
+        try {
+            // Cached values from a previous execution are possibly outdated.
+            clearCachedValues();
+            try {
+                doAutoImportsAndIncludes(this);
+                visit(getMainTemplate().getRootASTNode());
+                // It's here as we must not flush if there was an exception.
+                if (getAutoFlush()) {
+                    out.flush();
+                }
+            } finally {
+                // It's just to allow the GC to free memory...
+                clearCachedValues();
+            }
+        } finally {
+            TLS_ENVIRONMENT.set(savedEnv);
+        }
+    }
+
+    /**
+     * Executes the auto-imports and auto-includes for the main template of this environment.
+     * This is not meant to be called or overridden by code outside of FreeMarker.
+     */
+    private void doAutoImportsAndIncludes(Environment env) throws TemplateException, IOException {
+        Template t = getMainTemplate();
+        doAutoImports(t);
+        doAutoIncludes(t);
+    }
+
+    private void doAutoImports(Template t) throws IOException, TemplateException {
+        Map<String, String> envAutoImports = isAutoImportsSet() ? getAutoImports() : null;
+        Map<String, String> tAutoImports = t.isAutoImportsSet() ? t.getAutoImports() : null;
+
+        boolean lazyAutoImports = getLazyAutoImports() != null ? getLazyAutoImports() : getLazyImports();
+
+        for (Map.Entry<String, String> autoImport : configuration.getAutoImports().entrySet()) {
+            String nsVarName = autoImport.getKey();
+            if ((tAutoImports == null || !tAutoImports.containsKey(nsVarName))
+                    && (envAutoImports == null || !envAutoImports.containsKey(nsVarName))) {
+                importLib(autoImport.getValue(), nsVarName, lazyAutoImports);
+            }
+        }
+        if (tAutoImports != null) {
+            for (Map.Entry<String, String> autoImport : tAutoImports.entrySet()) {
+                String nsVarName = autoImport.getKey();
+                if (envAutoImports == null || !envAutoImports.containsKey(nsVarName)) {
+                    importLib(autoImport.getValue(), nsVarName, lazyAutoImports);
+                }
+            }
+        }
+        if (envAutoImports != null) {
+            for (Map.Entry<String, String> autoImport : envAutoImports.entrySet()) {
+                String nsVarName = autoImport.getKey();
+                importLib(autoImport.getValue(), nsVarName, lazyAutoImports);
+            }
+        }
+    }
+
+    private void doAutoIncludes(Template t) throws TemplateException, IOException {
+        // We can't store autoIncludes in LinkedHashSet-s because setAutoIncludes(List) allows duplicates,
+        // unfortunately. Yet we have to prevent duplicates among Configuration levels, with the lowest levels having
+        // priority. So we build some Set-s to do that, but we avoid the most common cases where they aren't needed.
+
+        List<String> tAutoIncludes = t.isAutoIncludesSet() ? t.getAutoIncludes() : null;
+        List<String> envAutoIncludes = isAutoIncludesSet() ? getAutoIncludes() : null;
+
+        for (String templateName : configuration.getAutoIncludes()) {
+            if ((tAutoIncludes == null || !tAutoIncludes.contains(templateName))
+                    && (envAutoIncludes == null || !envAutoIncludes.contains(templateName))) {
+                include(configuration.getTemplate(templateName, getLocale()));
+            }
+        }
+
+        if (tAutoIncludes != null) {
+            for (String templateName : tAutoIncludes) {
+                if (envAutoIncludes == null || !envAutoIncludes.contains(templateName)) {
+                    include(configuration.getTemplate(templateName, getLocale()));
+                }
+            }
+        }
+
+        if (envAutoIncludes != null) {
+            for (String templateName : envAutoIncludes) {
+                include(configuration.getTemplate(templateName, getLocale()));
+            }
+        }
+    }
+
+    /**
+     * "Visit" the template element.
+     */
+    void visit(ASTElement element) throws IOException, TemplateException {
+        // ATTENTION: This method body is manually "inlined" into visit(ASTElement[]); keep them in sync!
+        pushElement(element);
+        try {
+            ASTElement[] templateElementsToVisit = element.accept(this);
+            if (templateElementsToVisit != null) {
+                for (ASTElement el : templateElementsToVisit) {
+                    if (el == null) {
+                        break;  // Skip unused trailing buffer capacity 
+                    }
+                    visit(el);
+                }
+            }
+        } catch (TemplateException te) {
+            handleTemplateException(te);
+        } finally {
+            popElement();
+        }
+        // ATTENTION: This method body above is manually "inlined" into visit(ASTElement[]); keep them in sync!
+    }
+    
+    /**
+     * @param elementBuffer
+     *            The elements to visit; might contains trailing {@code null}-s. Can be {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    final void visit(ASTElement[] elementBuffer) throws IOException, TemplateException {
+        if (elementBuffer == null) {
+            return;
+        }
+        for (ASTElement element : elementBuffer) {
+            if (element == null) {
+                break;  // Skip unused trailing buffer capacity 
+            }
+            
+            // ATTENTION: This part is the manually "inlining" of visit(ASTElement[]); keep them in sync!
+            // We don't just let Hotspot to do it, as we want a hard guarantee regarding maximum stack usage. 
+            pushElement(element);
+            try {
+                ASTElement[] templateElementsToVisit = element.accept(this);
+                if (templateElementsToVisit != null) {
+                    for (ASTElement el : templateElementsToVisit) {
+                        if (el == null) {
+                            break;  // Skip unused trailing buffer capacity 
+                        }
+                        visit(el);
+                    }
+                }
+            } catch (TemplateException te) {
+                handleTemplateException(te);
+            } finally {
+                popElement();
+            }
+            // ATTENTION: This part above is the manually "inlining" of visit(ASTElement[]); keep them in sync!
+        }
+    }
+
+    @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "Not called when stack is empty")
+    private ASTElement replaceTopElement(ASTElement element) {
+        return instructionStack[instructionStackSize - 1] = element;
+    }
+
+    private static final TemplateModel[] NO_OUT_ARGS = new TemplateModel[0];
+
+    void visit(final ASTElement element,
+            TemplateDirectiveModel directiveModel, Map args,
+            final List bodyParameterNames) throws TemplateException, IOException {
+        visit(new ASTElement[] { element }, directiveModel, args, bodyParameterNames);
+    }
+    
+    void visit(final ASTElement[] childBuffer,
+            TemplateDirectiveModel directiveModel, Map args,
+            final List bodyParameterNames) throws TemplateException, IOException {
+        TemplateDirectiveBody nested;
+        if (childBuffer == null) {
+            nested = null;
+        } else {
+            nested = new NestedElementTemplateDirectiveBody(childBuffer);
+        }
+        final TemplateModel[] outArgs;
+        if (bodyParameterNames == null || bodyParameterNames.isEmpty()) {
+            outArgs = NO_OUT_ARGS;
+        } else {
+            outArgs = new TemplateModel[bodyParameterNames.size()];
+        }
+        if (outArgs.length > 0) {
+            pushLocalContext(new LocalContext() {
+
+                @Override
+                public TemplateModel getLocalVariable(String name) {
+                    int index = bodyParameterNames.indexOf(name);
+                    return index != -1 ? outArgs[index] : null;
+                }
+
+                @Override
+                public Collection getLocalVariableNames() {
+                    return bodyParameterNames;
+                }
+            });
+        }
+        try {
+            directiveModel.execute(this, args, outArgs, nested);
+        } finally {
+            if (outArgs.length > 0) {
+                localContextStack.pop();
+            }
+        }
+    }
+
+    /**
+     * "Visit" the template element, passing the output through a TemplateTransformModel
+     * 
+     * @param elementBuffer
+     *            the element to visit through a transform; might contains trailing {@code null}-s
+     * @param transform
+     *            the transform to pass the element output through
+     * @param args
+     *            optional arguments fed to the transform
+     */
+    void visitAndTransform(ASTElement[] elementBuffer,
+            TemplateTransformModel transform,
+            Map args)
+                    throws TemplateException, IOException {
+        try {
+            Writer tw = transform.getWriter(out, args);
+            if (tw == null) tw = EMPTY_BODY_WRITER;
+            TransformControl tc = tw instanceof TransformControl
+                    ? (TransformControl) tw
+                    : null;
+
+            Writer prevOut = out;
+            out = tw;
+            try {
+                if (tc == null || tc.onStart() != TransformControl.SKIP_BODY) {
+                    do {
+                        visit(elementBuffer);
+                    } while (tc != null && tc.afterBody() == TransformControl.REPEAT_EVALUATION);
+                }
+            } catch (Throwable t) {
+                try {
+                    if (tc != null) {
+                        tc.onError(t);
+                    } else {
+                        throw t;
+                    }
+                } catch (TemplateException e) {
+                    throw e;
+                } catch (IOException e) {
+                    throw e;
+                } catch (RuntimeException e) {
+                    throw e;
+                } catch (Error e) {
+                    throw e;
+                } catch (Throwable e) {
+                    throw new UndeclaredThrowableException(e);
+                }
+            } finally {
+                out = prevOut;
+                tw.close();
+            }
+        } catch (TemplateException te) {
+            handleTemplateException(te);
+        }
+    }
+
+    /**
+     * Visit a block using buffering/recovery
+     */
+     void visitAttemptRecover(
+             ASTDirAttemptRecoverContainer attemptBlock, ASTElement attemptedSection, ASTDirRecover recoverySection)
+             throws TemplateException, IOException {
+        Writer prevOut = out;
+        StringWriter sw = new StringWriter();
+         out = sw;
+        TemplateException thrownException = null;
+        boolean lastFIRE = setFastInvalidReferenceExceptions(false);
+        boolean lastInAttemptBlock = inAttemptBlock;
+        try {
+            inAttemptBlock = true;
+            visit(attemptedSection);
+        } catch (TemplateException te) {
+            thrownException = te;
+        } finally {
+            inAttemptBlock = lastInAttemptBlock;
+            setFastInvalidReferenceExceptions(lastFIRE);
+            out = prevOut;
+        }
+        if (thrownException != null) {
+            if (LOG_ATTEMPT.isDebugEnabled()) {
+                LOG_ATTEMPT.debug("Error in attempt block " +
+                        attemptBlock.getStartLocationQuoted(), thrownException);
+            }
+            try {
+                recoveredErrorStack.add(thrownException);
+                visit(recoverySection);
+            } finally {
+                recoveredErrorStack.remove(recoveredErrorStack.size() - 1);
+            }
+        } else {
+            out.write(sw.toString());
+        }
+    }
+
+    String getCurrentRecoveredErrorMessage() throws TemplateException {
+        if (recoveredErrorStack.isEmpty()) {
+            throw new _MiscTemplateException(this, ".error is not available outside of a #recover block");
+        }
+        return ((Throwable) recoveredErrorStack.get(recoveredErrorStack.size() - 1)).getMessage();
+    }
+
+    /**
+     * Tells if we are inside an <tt>#attempt</tt> block (but before <tt>#recover</tt>). This can be useful for
+     * {@link TemplateExceptionHandler}-s, as then they may don't want to print the error to the output, as
+     * <tt>#attempt</tt> will roll it back anyway.
+     * 
+     * @since 2.3.20
+     */
+    public boolean isInAttemptBlock() {
+        return inAttemptBlock;
+    }
+
+    /**
+     * Used for {@code #nested}.
+     */
+    void invokeNestedContent(ASTDirNested.Context bodyCtx) throws TemplateException, IOException {
+        ASTDirMacro.Context invokingMacroContext = getCurrentMacroContext();
+        LocalContextStack prevLocalContextStack = localContextStack;
+        ASTElement[] nestedContentBuffer = invokingMacroContext.nestedContentBuffer;
+        if (nestedContentBuffer != null) {
+            currentMacroContext = invokingMacroContext.prevMacroContext;
+            currentNamespace = invokingMacroContext.nestedContentNamespace;
+
+            localContextStack = invokingMacroContext.prevLocalContextStack;
+            if (invokingMacroContext.nestedContentParameterNames != null) {
+                pushLocalContext(bodyCtx);
+            }
+            try {
+                visit(nestedContentBuffer);
+            } finally {
+                if (invokingMacroContext.nestedContentParameterNames != null) {
+                    localContextStack.pop();
+                }
+                currentMacroContext = invokingMacroContext;
+                currentNamespace = getMacroNamespace(invokingMacroContext.getMacro());
+                localContextStack = prevLocalContextStack;
+            }
+        }
+    }
+
+    /**
+     * "visit" an ASTDirList
+     */
+    boolean visitIteratorBlock(ASTDirList.IterationContext ictxt)
+            throws TemplateException, IOException {
+        pushLocalContext(ictxt);
+        try {
+            return ictxt.accept(this);
+        } catch (TemplateException te) {
+            handleTemplateException(te);
+            return true;
+        } finally {
+            localContextStack.pop();
+        }
+    }
+
+    /**
+     * Used for {@code #visit} and {@code #recurse}.
+     */
+    void invokeNodeHandlerFor(TemplateNodeModel node, TemplateSequenceModel namespaces)
+            throws TemplateException, IOException {
+        if (nodeNamespaces == null) {
+            NativeSequence seq = new NativeSequence(1);
+            seq.add(currentNamespace);
+            nodeNamespaces = seq;
+        }
+        int prevNodeNamespaceIndex = nodeNamespaceIndex;
+        String prevNodeName = currentNodeName;
+        String prevNodeNS = currentNodeNS;
+        TemplateSequenceModel prevNodeNamespaces = nodeNamespaces;
+        TemplateNodeModel prevVisitorNode = currentVisitorNode;
+        currentVisitorNode = node;
+        if (namespaces != null) {
+            nodeNamespaces = namespaces;
+        }
+        try {
+            TemplateModel macroOrTransform = getNodeProcessor(node);
+            if (macroOrTransform instanceof ASTDirMacro) {
+                invoke((ASTDirMacro) macroOrTransform, null, null, null, null);
+            } else if (macroOrTransform instanceof TemplateTransformModel) {
+                visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null);
+            } else {
+                String nodeType = node.getNodeType();
+                if (nodeType != null) {
+                    // If the node's type is 'text', we just output it.
+                    if ((nodeType.equals("text") && node instanceof TemplateScalarModel)) {
+                        out.write(((TemplateScalarModel) node).getAsString());
+                    } else if (nodeType.equals("document")) {
+                        recurse(node, namespaces);
+                    }
+                    // We complain here, unless the node's type is 'pi', or "comment" or "document_type", in which case
+                    // we just ignore it.
+                    else if (!nodeType.equals("pi")
+                            && !nodeType.equals("comment")
+                            && !nodeType.equals("document_type")) {
+                        throw new _MiscTemplateException(
+                                this, noNodeHandlerDefinedDescription(node, node.getNodeNamespace(), nodeType));
+                    }
+                } else {
+                    throw new _MiscTemplateException(
+                            this, noNodeHandlerDefinedDescription(node, node.getNodeNamespace(), "default"));
+                }
+            }
+        } finally {
+            currentVisitorNode = prevVisitorNode;
+            nodeNamespaceIndex = prevNodeNamespaceIndex;
+            currentNodeName = prevNodeName;
+            currentNodeNS = prevNodeNS;
+            nodeNamespaces = prevNodeNamespaces;
+        }
+    }
+
+    private Object[] noNodeHandlerDefinedDescription(
+            TemplateNodeModel node, String ns, String nodeType)
+                    throws TemplateModelException {
+        String nsPrefix;
+        if (ns != null) {
+            if (ns.length() > 0) {
+                nsPrefix = " and namespace ";
+            } else {
+                nsPrefix = " and no namespace";
+            }
+        } else {
+            nsPrefix = "";
+            ns = "";
+        }
+        return new Object[] { "No macro or directive is defined for node named ",
+                new _DelayedJQuote(node.getNodeName()), nsPrefix, ns,
+                ", and there is no fallback handler called @", nodeType, " either." };
+    }
+
+    void fallback() throws TemplateException, IOException {
+        TemplateModel macroOrTransform = getNodeProcessor(currentNodeName, currentNodeNS, nodeNamespaceIndex);
+        if (macroOrTransform instanceof ASTDirMacro) {
+            invoke((ASTDirMacro) macroOrTransform, null, null, null, null);
+        } else if (macroOrTransform instanceof TemplateTransformModel) {
+            visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null);
+        }
+    }
+
+    /**
+     * Calls the macro or function with the given arguments and nested block.
+     */
+    void invoke(ASTDirMacro macro,
+            Map namedArgs, List positionalArgs,
+            List bodyParameterNames, ASTElement[] childBuffer) throws TemplateException, IOException {
+        if (macro == ASTDirMacro.DO_NOTHING_MACRO) {
+            return;
+        }
+
+        pushElement(macro);
+        try {
+            final ASTDirMacro.Context macroCtx = macro.new Context(this, childBuffer, bodyParameterNames);
+            setMacroContextLocalsFromArguments(macroCtx, macro, namedArgs, positionalArgs);
+
+            final ASTDirMacro.Context prevMacroCtx = currentMacroContext;
+            currentMacroContext = macroCtx;
+
+            final LocalContextStack prevLocalContextStack = localContextStack;
+            localContextStack = null;
+
+            final Namespace prevNamespace = currentNamespace;
+            currentNamespace = (Namespace) macroToNamespaceLookup.get(macro);
+
+            try {
+                macroCtx.sanityCheck(this);
+                visit(macro.getChildBuffer());
+            } catch (ASTDirReturn.Return re) {
+                // Not an error, just a <#return>
+            } catch (TemplateException te) {
+                handleTemplateException(te);
+            } finally {
+                currentMacroContext = prevMacroCtx;
+                localContextStack = prevLocalContextStack;
+                currentNamespace = prevNamespace;
+            }
+        } finally {
+            popElement();
+        }
+    }
+
+    /**
+     * Sets the local variables corresponding to the macro call arguments in the macro context.
+     */
+    private void setMacroContextLocalsFromArguments(
+            final ASTDirMacro.Context macroCtx,
+            final ASTDirMacro macro,
+            final Map namedArgs, final List positionalArgs) throws TemplateException {
+        String catchAllParamName = macro.getCatchAll();
+        if (namedArgs != null) {
+            final NativeHashEx2 catchAllParamValue;
+            if (catchAllParamName != null) {
+                catchAllParamValue = new NativeHashEx2();
+                macroCtx.setLocalVar(catchAllParamName, catchAllParamValue);
+            } else {
+                catchAllParamValue = null;
+            }
+
+             for (Map.Entry argNameAndValExp : (Set<Map.Entry>) namedArgs.entrySet()) {
+                final String argName = (String) argNameAndValExp.getKey();
+                final boolean isArgNameDeclared = macro.hasArgNamed(argName);
+                if (isArgNameDeclared || catchAllParamName != null) {
+                    ASTExpression argValueExp = (ASTExpression) argNameAndValExp.getValue();
+                    TemplateModel argValue = argValueExp.eval(this);
+                    if (isArgNameDeclared) {
+                        macroCtx.setLocalVar(argName, argValue);
+                    } else {
+                        catchAllParamValue.put(argName, argValue);
+                    }
+                } else {
+                    throw new _MiscTemplateException(this,
+                            (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+                            " has no parameter with name ", new _DelayedJQuote(argName), ".");
+                }
+            }
+        } else if (positionalArgs != null) {
+            final NativeSequence catchAllParamValue;
+            if (catchAllParamName != null) {
+                catchAllParamValue = new NativeSequence(8);
+                macroCtx.setLocalVar(catchAllParamName, catchAllParamValue);
+            } else {
+                catchAllParamValue = null;
+            }
+
+            String[] argNames = macro.getArgumentNamesInternal();
+            final int argsCnt = positionalArgs.size();
+            if (argNames.length < argsCnt && catchAllParamName == null) {
+                throw new _MiscTemplateException(this,
+                        (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
+                        " only accepts ", new _DelayedToString(argNames.length), " parameters, but got ",
+                        new _DelayedToString(argsCnt), ".");
+            }
+            for (int i = 0; i < argsCnt; i++) {
+                ASTExpression argValueExp = (ASTExpression) positionalArgs.get(i);
+                TemplateModel argValue = argValueExp.eval(this);
+                try {
+                    if (i < argNames.length) {
+                        String argName = argNames[i];
+                        macroCtx.setLocalVar(argName, argValue);
+                    } else {
+                        catchAllParamValue.add(argValue);
+                    }
+                } catch (RuntimeException re) {
+                    throw new _MiscTemplateException(re, this);
+                }
+            }
+        }
+    }
+
+    /**
+     * Defines the given macro in the current namespace (doesn't call it).
+     */
+    void visitMacroDef(ASTDirMacro macro) {
+        macroToNamespaceLookup.put(macro, currentNamespace);
+        currentNamespace.put(macro.getName(), macro);
+    }
+
+    Namespace getMacroNamespace(ASTDirMacro macro) {
+        return (Namespace) macroToNamespaceLookup.get(macro);
+    }
+
+    void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces)
+            throws TemplateException, IOException {
+        if (node == null) {
+            node = getCurrentVisitorNode();
+            if (node == null) {
+                throw new _TemplateModelException(
+                        "The target node of recursion is missing or null.");
+            }
+        }
+        TemplateSequenceModel children = node.getChildNodes();
+        if (children == null) return;
+        for (int i = 0; i < children.size(); i++) {
+            TemplateNodeModel child = (TemplateNodeModel) children.get(i);
+            if (child != null) {
+                invokeNodeHandlerFor(child, namespaces);
+            }
+        }
+    }
+
+    ASTDirMacro.Context getCurrentMacroContext() {
+        return currentMacroContext;
+    }
+
+    private void handleTemplateException(TemplateException templateException)
+            throws TemplateException {
+        // Logic to prevent double-handling of the exception in
+        // nested visit() calls.
+        if (lastThrowable == templateException) {
+            throw templateException;
+        }
+        lastThrowable = templateException;
+
+        // Log the exception, if logTemplateExceptions isn't false. However, even if it's false, if we are inside
+        // an #attempt block, it has to be logged, as it certainly won't bubble up to the caller of FreeMarker.
+        if (LOG.isErrorEnabled() && (isInAttemptBlock() || getLogTemplateExceptions())) {
+            LOG.error("Error executing FreeMarker template", templateException);
+        }
+
+        // Stop exception is not passed to the handler, but
+        // explicitly rethrown.
+        if (templateException instanceof StopException) {
+            throw templateException;
+        }
+
+        // Finally, pass the exception to the handler
+        getTemplateExceptionHandler().handleTemplateException(templateException, this, out);
+    }
+
+    @Override
+    public void setTemplateExceptionHandler(TemplateExceptionHandler templateExceptionHandler) {
+        super.setTemplateExceptionHandler(templateExceptionHandler);
+        lastThrowable = null;
+    }
+
+    @Override
+    protected TemplateExceptionHandler getDefaultTemplateExceptionHandler() {
+        return getMainTemplate().getTemplateExceptionHandler();
+    }
+
+    @Override
+    protected ArithmeticEngine getDefaultArithmeticEngine() {
+        return getMainTemplate().getArithmeticEngine();
+    }
+
+    @Override
+    protected ObjectWrapper getDefaultObjectWrapper() {
+        return getMainTemplate().getObjectWrapper();
+    }
+
+    @Override
+    public void setLocale(Locale locale) {
+        Locale prevLocale = getLocale();
+        super.setLocale(locale);
+        if (!locale.equals(prevLocale)) {
+            cachedTemplateNumberFormats = null;
+            if (cachedTemplateNumberFormat != null && cachedTemplateNumberFormat.isLocaleBound()) {
+                cachedTemplateNumberFormat = null;
+            }
+
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_LENGTH; i++) {
+                    final TemplateDateFormat f = cachedTempDateFormatArray[i];
+                    if (f != null && f.isLocaleBound()) {
+                        cachedTempDateFormatArray[i] = null;
+                    }
+                }
+            }
+
+            cachedTempDateFormatsByFmtStrArray = null;
+
+            cachedCollator = null;
+        }
+    }
+
+    @Override
+    protected Locale getDefaultLocale() {
+        return getMainTemplate().getLocale();
+    }
+
+    @Override
+    public void setTimeZone(TimeZone timeZone) {
+        TimeZone prevTimeZone = getTimeZone();
+        super.setTimeZone(timeZone);
+
+        if (!timeZone.equals(prevTimeZone)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_SQL_D_T_TZ_OFFS; i++) {
+                    TemplateDateFormat f = cachedTempDateFormatArray[i];
+                    if (f != null && f.isTimeZoneBound()) {
+                        cachedTempDateFormatArray[i] = null;
+                    }
+                }
+            }
+            if (cachedTempDateFormatsByFmtStrArray != null) {
+                for (int i = 0; i < CACHED_TDFS_SQL_D_T_TZ_OFFS; i++) {
+                    cachedTempDateFormatsByFmtStrArray[i] = null;
+                }
+            }
+
+            cachedSQLDateAndTimeTimeZoneSameAsNormal = null;
+        }
+    }
+
+    @Override
+    protected TimeZone getDefaultTimeZone() {
+        return getMainTemplate().getTimeZone();
+    }
+
+    @Override
+    public void setSQLDateAndTimeTimeZone(TimeZone timeZone) {
+        TimeZone prevTimeZone = getSQLDateAndTimeTimeZone();
+        super.setSQLDateAndTimeTimeZone(timeZone);
+
+        if (!nullSafeEquals(timeZone, prevTimeZone)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) {
+                    TemplateDateFormat format = cachedTempDateFormatArray[i];
+                    if (format != null && format.isTimeZoneBound()) {
+                        cachedTempDateFormatArray[i] = null;
+                    }
+                }
+            }
+            if (cachedTempDateFormatsByFmtStrArray != null) {
+                for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) {
+                    cachedTempDateFormatsByFmtStrArray[i] = null;
+                }
+            }
+
+            cachedSQLDateAndTimeTimeZoneSameAsNormal = null;
+        }
+    }
+
+    @Override
+    protected TimeZone getDefaultSQLDateAndTimeTimeZone() {
+        return getMainTemplate().getSQLDateAndTimeTimeZone();
+    }
+
+    // Replace with Objects.equals in Java 7
+    private static boolean nullSafeEquals(Object o1, Object o2) {
+        if (o1 == o2) return true;
+        if (o1 == null || o2 == null) return false;
+        return o1.equals(o2);
+    }
+
+    /**
+     * Tells if the same concrete time zone is used for SQL date-only and time-only values as for other
+     * date/time/date-time values.
+     */
+    boolean isSQLDateAndTimeTimeZoneSameAsNormal() {
+        if (cachedSQLDateAndTimeTimeZoneSameAsNormal == null) {
+            cachedSQLDateAndTimeTimeZoneSameAsNormal = Boolean.valueOf(
+                    getSQLDateAndTimeTimeZone() == null
+                            || getSQLDateAndTimeTimeZone().equals(getTimeZone()));
+        }
+        return cachedSQLDateAndTimeTimeZoneSameAsNormal.booleanValue();
+    }
+
+    @Override
+    public void setURLEscapingCharset(Charset urlEscapingCharset) {
+        cachedURLEscapingCharsetSet = false;
+        super.setURLEscapingCharset(urlEscapingCharset);
+    }
+
+    @Override
+    protected Charset getDefaultURLEscapingCharset() {
+        return getMainTemplate().getURLEscapingCharset();
+    }
+
+    @Override
+    protected TemplateClassResolver getDefaultNewBuiltinClassResolver() {
+        return getMainTemplate().getNewBuiltinClassResolver();
+    }
+
+    @Override
+    protected boolean getDefaultAutoFlush() {
+        return getMainTemplate().getAutoFlush();
+    }
+
+    @Override
+    protected boolean getDefaultShowErrorTips() {
+        return getMainTemplate().getShowErrorTips();
+    }
+
+    @Override
+    protected boolean getDefaultAPIBuiltinEnabled() {
+        return getMainTemplate().getAPIBuiltinEnabled();
+    }
+
+    @Override
+    protected boolean getDefaultLogTemplateExceptions() {
+        return getMainTemplate().getLogTemplateExceptions();
+    }
+
+    @Override
+    protected boolean getDefaultLazyImports() {
+        return getMainTemplate().getLazyImports();
+    }
+
+    @Override
+    protected Boolean getDefaultLazyAutoImports() {
+        return getMainTemplate().getLazyAutoImports();
+    }
+
+    @Override
+    protected Map<String, String> getDefaultAutoImports() {
+        return getMainTemplate().getAutoImports();
+    }
+
+    @Override
+    protected List<String> getDefaultAutoIncludes() {
+        return getMainTemplate().getAutoIncludes();
+    }
+
+    @Override
+    protected Object getDefaultCustomAttribute(Object name) {
+        return getMainTemplate().getCustomAttribute(name);
+    }
+
+    @Override
+    protected Map<Object, Object> getDefaultCustomAttributes() {
+        return getMainTemplate().getCustomAttributes();
+    }
+
+    /*
+     * Note that altough it's not allowed to set this setting with the <tt>setting</tt> directive, it still must be
+     * allowed to set it from Java code while the template executes, since some frameworks allow templates to actually
+     * change the output encoding on-the-fly.
+     */
+    @Override
+    public void setOutputEncoding(Charset outputEncoding) {
+        cachedURLEscapingCharsetSet = false;
+        super.setOutputEncoding(outputEncoding);
+    }
+
+    @Override
+    protected Charset getDefaultOutputEncoding() {
+        return getMainTemplate().getOutputEncoding();
+    }
+
+    /**
+     * Returns the name of the charset that should be used for URL encoding. This will be <code>null</code> if the
+     * information is not available. The function caches the return value, so it's quick to call it repeatedly.
+     */
+    Charset getEffectiveURLEscapingCharset() {
+        if (!cachedURLEscapingCharsetSet) {
+            cachedURLEscapingCharset = getURLEscapingCharset();
+            if (cachedURLEscapingCharset == null) {
+                cachedURLEscapingCharset = getOutputEncoding();
+            }
+            cachedURLEscapingCharsetSet = true;
+        }
+        return cachedURLEscapingCharset;
+    }
+
+    Collator getCollator() {
+        if (cachedCollator == null) {
+            cachedCollator = Collator.getInstance(getLocale());
+        }
+        return cachedCollator;
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "==" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyEqualsOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_EQUALS, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "==" operator, except that if the two types
+     * are incompatible, they are treated as non-equal instead of throwing an exception. Comparing dates of different
+     * types (date-only VS time-only VS date-time) will still throw an exception, however.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyEqualsOperatorLenient(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compareLenient(leftValue, _EvalUtil.CMP_OP_EQUALS, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "&lt;" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyLessThanOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_LESS_THAN, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "&lt;" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyLessThanOrEqualsOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_LESS_THAN_EQUALS, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "&gt;" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyGreaterThanOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_GREATER_THAN, rightValue, this);
+    }
+
+    /**
+     * Compares two {@link TemplateModel}-s according the rules of the FTL "&gt;=" operator.
+     * 
+     * @since 2.3.20
+     */
+    public boolean applyWithGreaterThanOrEqualsOperator(TemplateModel leftValue, TemplateModel rightValue)
+            throws TemplateException {
+        return _EvalUtil.compare(leftValue, _EvalUtil.CMP_OP_GREATER_THAN_EQUALS, rightValue, this);
+    }
+
+    public void setOut(Writer out) {
+        this.out = out;
+    }
+
+    public Writer getOut() {
+        return out;
+    }
+
+    @Override
+    public void setNumberFormat(String formatName) {
+        super.setNumberFormat(formatName);
+        cachedTemplateNumberFormat = null;
+    }
+
+    @Override
+    protected String getDefaultNumberFormat() {
+        return getMainTemplate().getNumberFormat();
+    }
+
+    @Override
+    protected Map<String, TemplateNumberFormatFactory> getDefaultCustomNumberFormats() {
+        return getMainTemplate().getCustomNumberFormats();
+    }
+
+    @Override
+    protected TemplateNumberFormatFactory getDefaultCustomNumberFormat(String name) {
+        return getMainTemplate().getCustomNumberFormat(name);
+    }
+
+    @Override
+    protected String getDefaultBooleanFormat() {
+        return getMainTemplate().getBooleanFormat();
+    }
+
+    String formatBoolean(boolean value, boolean fallbackToTrueFalse) throws TemplateException {
+        TemplateBooleanFormat templateBooleanFormat = getTemplateBooleanFormat();
+        if (value) {
+            String s = templateBooleanFormat.getTrueStringValue();
+            if (s == null) {
+                if (fallbackToTrueFalse) {
+                    return MiscUtil.C_TRUE;
+                } else {
+                    throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
+                }
+            } else {
+                return s;
+            }
+        } else {
+            String s = templateBooleanFormat.getFalseStringValue();
+            if (s == null) {
+                if (fallbackToTrueFalse) {
+                    return MiscUtil.C_FALSE;
+                } else {
+                    throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
+                }
+            } else {
+                return s;
+            }
+        }
+    }
+
+    TemplateBooleanFormat getTemplateBooleanFormat() {
+        TemplateBooleanFormat format = cachedTemplateBooleanFormat;
+        if (format == null) {
+            format = TemplateBooleanFormat.getInstance(getBooleanFormat());
+            cachedTemplateBooleanFormat = format;
+        }
+        return format;
+    }
+
+    @Override
+    public void setBooleanFormat(String booleanFormat) {
+        String previousFormat = getBooleanFormat();
+        super.setBooleanFormat(booleanFormat);
+        if (!booleanFormat.equals(previousFormat)) {
+            cachedTemplateBooleanFormat = null;
+        }
+    }
+
+    private _ErrorDescriptionBuilder getNullBooleanFormatErrorDescription() {
+        return new _ErrorDescriptionBuilder(
+                "Can't convert boolean to string automatically, because the \"", BOOLEAN_FORMAT_KEY ,"\" setting was ",
+                new _DelayedJQuote(getBooleanFormat()),
+                (getBooleanFormat().equals(TemplateBooleanFormat.C_TRUE_FALSE)
+                        ? ", which is the legacy default computer-language format, and hence isn't accepted."
+                        : ".")
+        ).tips(
+                "If you just want \"true\"/\"false\" result as you are generting computer-language output, "
+                        + "use \"?c\", like ${myBool?c}.",
+                "You can write myBool?string('yes', 'no') and like to specify boolean formatting in place.",
+                new Object[] {
+                        "If you need the same two values on most places, the programmers should set the \"",
+                        BOOLEAN_FORMAT_KEY ,"\" setting to something like \"yes,no\"." }
+        );
+    }
+
+    /**
+     * Format number with the default number format.
+     * 
+     * @param exp
+     *            The blamed expression if an error occurs; it's only needed for better error messages
+     */
+    String formatNumberToPlainText(TemplateNumberModel number, ASTExpression exp, boolean useTempModelExc)
+            throws TemplateException {
+        return formatNumberToPlainText(number, getTemplateNumberFormat(exp, useTempModelExc), exp, useTempModelExc);
+    }
+
+    /**
+     * Format number with the number format specified as the parameter, with the current locale.
+     * 
+     * @param exp
+     *            The blamed expression if an error occurs; it's only needed for better error messages
+     */
+    String formatNumberToPlainText(
+            TemplateNumberModel number, TemplateNumberFormat format, ASTExpression exp,
+            boolean useTempModelExc)
+            throws TemplateException {
+        try {
+            return _EvalUtil.assertFormatResultNotNull(format.formatToPlainText(number));
+        } catch (TemplateValueFormatException e) {
+            throw MessageUtil.newCantFormatNumberException(format, exp, e, useTempModelExc);
+        }
+    }
+
+    /**
+     * Returns the current number format ({@link #getNumberFormat()}) as {@link TemplateNumberFormat}.
+     * 
+     * <p>
+     * Performance notes: The result is stored for reuse, so calling this method frequently is usually not a problem.
+     * However, at least as of this writing (2.3.24), changing the current locale {@link #setLocale(Locale)} or changing
+     * the current number format ({@link #setNumberFormat(String)}) will drop the stored value, so it will have to be
+     * recalculated.
+     * 
+     * @since 2.3.24
+     */
+    public TemplateNumberFormat getTemplateNumberFormat() throws TemplateValueFormatException {
+        TemplateNumberFormat format = cachedTemplateNumberFormat;
+        if (format == null) {
+            format = getTemplateNumberFormat(getNumberFormat(), false);
+            cachedTemplateNumberFormat = format;
+        }
+        return format;
+    }
+
+    /**
+     * Returns the number format as {@link TemplateNumberFormat} for the given format string and the current locale.
+     * (The current locale is the locale returned by {@link #getLocale()}.) Note that the result will be cached in the
+     * {@link Environment} instance (though at least in 2.3.24 the cache will be flushed if the current locale of the
+     * {@link Environment} is changed).
+     * 
+     * @param formatString
+     *            A string that you could also use as the value of the {@code numberFormat} configuration setting. Can't
+     *            be {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    public TemplateNumberFormat getTemplateNumberFormat(String formatString) throws TemplateValueFormatException {
+        return getTemplateNumberFormat(formatString, true);
+    }
+
+    /**
+     * Returns the number format as {@link TemplateNumberFormat}, for the given format string and locale. To get a
+     * number format for the current locale, use {@link #getTemplateNumberFormat(String)} instead.
+     * 
+     * <p>
+     * Note on performance (which was true at least for 2.3.24): Unless the locale happens to be equal to the current
+     * locale, the {@link Environment}-level format cache can't be used, so the format string has to be parsed and the
+     * matching factory has to be get an invoked, which is much more expensive than getting the format from the cache.
+     * Thus the returned format should be stored by the caller for later reuse (but only within the current thread and
+     * in relation to the current {@link Environment}), if it will be needed frequently.
+     * 
+     * @param formatString
+     *            A string that you could also use as the value of the {@code numberFormat} configuration setting.
+     * @param locale
+     *            The locale of the number format; not {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    public TemplateNumberFormat getTemplateNumberFormat(String formatString, Locale locale)
+            throws TemplateValueFormatException {
+        if (locale.equals(getLocale())) {
+            getTemplateNumberFormat(formatString);
+        }
+
+        return getTemplateNumberFormatWithoutCache(formatString, locale);
+    }
+
+    /**
+     * Convenience wrapper around {@link #getTemplateNumberFormat()} to be called during expression evaluation.
+     */
+    TemplateNumberFormat getTemplateNumberFormat(ASTExpression exp, boolean useTempModelExc) throws TemplateException {
+        TemplateNumberFormat format;
+        try {
+            format = getTemplateNumberFormat();
+        } catch (TemplateValueFormatException e) {
+            _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                    "Failed to get number format object for the current number format string, ",
+                    new _DelayedJQuote(getNumberFormat()), ": ", e.getMessage())
+                    .blame(exp); 
+            throw useTempModelExc
+                    ? new _TemplateModelException(e, this, desc) : new _MiscTemplateException(e, this, desc);
+        }
+        return format;
+    }
+
+    /**
+     * Convenience wrapper around {@link #getTemplateNumberFormat(String)} to be called during expression evaluation.
+     * 
+     * @param exp
+     *            The blamed expression if an error occurs; it's only needed for better error messages
+     */
+    TemplateNumberFormat getTemplateNumberFormat(String formatString, ASTExpression exp, boolean useTempModelExc)
+            throws TemplateException {
+        TemplateNumberFormat format;
+        try {
+            format = getTemplateNumberFormat(formatString);
+        } catch (TemplateValueFormatException e) {
+            _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                    "Failed to get number format object for the ", new _DelayedJQuote(formatString),
+                    " number format string: ", e.getMessage())
+                    .blame(exp);
+            throw useTempModelExc
+                    ? new _TemplateModelException(e, this, desc) : new _MiscTemplateException(e, this, desc);
+        }
+        return format;
+    }
+
+    /**
+     * Gets the {@link TemplateNumberFormat} <em>for the current locale</em>.
+     * 
+     * @param formatString
+     *            Not {@code null}
+     * @param cacheResult
+     *            If the results should stored in the {@link Environment}-level cache. It will still try to get the
+     *            result from the cache regardless of this parameter.
+     */
+    private TemplateNumberFormat getTemplateNumberFormat(String formatString, boolean cacheResult)
+            throws TemplateValueFormatException {
+        if (cachedTemplateNumberFormats == null) {
+            if (cacheResult) {
+                cachedTemplateNumberFormats = new HashMap<>();
+            }
+        } else {
+            TemplateNumberFormat format = cachedTemplateNumberFormats.get(formatString);
+            if (format != null) {
+                return format;
+            }
+        }
+
+        TemplateNumberFormat format = getTemplateNumberFormatWithoutCache(formatString, getLocale());
+
+        if (cacheResult) {
+            cachedTemplateNumberFormats.put(formatString, format);
+        }
+        return format;
+    }
+
+    /**
+     * Returns the {@link TemplateNumberFormat} for the given parameters without using the {@link Environment}-level
+     * cache. Of course, the {@link TemplateNumberFormatFactory} involved might still uses its own cache.
+     * 
+     * @param formatString
+     *            Not {@code null}
+     * @param locale
+     *            Not {@code null}
+     */
+    private TemplateNumberFormat getTemplateNumberFormatWithoutCache(String formatString, Locale locale)
+            throws TemplateValueFormatException {
+        int formatStringLen = formatString.length();
+        if (formatStringLen > 1
+                && formatString.charAt(0) == '@'
+                && Character.isLetter(formatString.charAt(1))) {
+            final String name;
+            final String params;
+            {
+                int endIdx;
+                findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) {
+                    char c = formatString.charAt(endIdx);
+                    if (c == ' ' || c == '_') {
+                        break findParamsStart;
+                    }
+                }
+                name = formatString.substring(1, endIdx);
+                params = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : "";
+            }
+
+            TemplateNumberFormatFactory formatFactory = getCustomNumberFormat(name);
+            if (formatFactory == null) {
+                throw new UndefinedCustomFormatException(
+                        "No custom number format was defined with name " + _StringUtil.jQuote(name));
+            }
+
+            return formatFactory.get(params, locale, this);
+        } else {
+            return JavaTemplateNumberFormatFactory.INSTANCE.get(formatString, locale, this);
+        }
+    }
+
+    /**
+     * Returns the {@link NumberFormat} used for the <tt>c</tt> built-in. This is always US English
+     * <code>"0.################"</code>, without grouping and without superfluous decimal separator.
+     */
+    public NumberFormat getCNumberFormat() {
+        // It can't be cached in a static field, because DecimalFormat-s aren't
+        // thread-safe.
+        if (cNumberFormat == null) {
+            cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT.clone();
+        }
+        return cNumberFormat;
+    }
+
+    @Override
+    public void setTimeFormat(String timeFormat) {
+        String prevTimeFormat = getTimeFormat();
+        super.setTimeFormat(timeFormat);
+        if (!timeFormat.equals(prevTimeFormat)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) {
+                    cachedTempDateFormatArray[i + TemplateDateModel.TIME] = null;
+                }
+            }
+        }
+    }
+
+    @Override
+    protected String getDefaultTimeFormat() {
+        return getMainTemplate().getTimeFormat();
+    }
+
+    @Override
+    public void setDateFormat(String dateFormat) {
+        String prevDateFormat = getDateFormat();
+        super.setDateFormat(dateFormat);
+        if (!dateFormat.equals(prevDateFormat)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) {
+                    cachedTempDateFormatArray[i + TemplateDateModel.DATE] = null;
+                }
+            }
+        }
+    }
+
+    @Override
+    protected String getDefaultDateFormat() {
+        return getMainTemplate().getDateFormat();
+    }
+
+    @Override
+    public void setDateTimeFormat(String dateTimeFormat) {
+        String prevDateTimeFormat = getDateTimeFormat();
+        super.setDateTimeFormat(dateTimeFormat);
+        if (!dateTimeFormat.equals(prevDateTimeFormat)) {
+            if (cachedTempDateFormatArray != null) {
+                for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) {
+                    cachedTempDateFormatArray[i + TemplateDateModel.DATETIME] = null;
+                }
+            }
+        }
+    }
+
+    @Override
+    protected String getDefaultDateTimeFormat() {
+        return getMainTemplate().getDateTimeFormat();
+    }
+
+    @Override
+    protected Map<String, TemplateDateFormatFactory> getDefaultCustomDateFormats() {
+        return getMainTemplate().getCustomDateFormats();
+    }
+
+    @Override
+    protected TemplateDateFormatFactory getDefaultCustomDateFormat(String name) {
+        return getMainTemplate().getCustomDateFormat(name);
+    }
+
+    public Configuration getConfiguration() {
+        return configuration;
+    }
+
+    TemplateModel getLastReturnValue() {
+        return lastReturnValue;
+    }
+
+    void setLastReturnValue(TemplateModel lastReturnValue) {
+        this.lastReturnValue = lastReturnValue;
+    }
+
+    void clearLastReturnValue() {
+        lastReturnValue = null;
+    }
+
+    /**
+     * @param tdmSourceExpr
+     *            The blamed expression if an error occurs; only used for error messages.
+     */
+    String formatDateToPlainText(TemplateDateModel tdm, ASTExpression tdmSourceExpr,
+            boolean useTempModelExc) throws TemplateException {
+        TemplateDateFormat format = getTemplateDateFormat(tdm, tdmSourceExpr, useTempModelExc);
+        
+        try {
+            return _EvalUtil.assertFormatResultNotNull(format.formatToPlainText(tdm));
+        } catch (TemplateValueFormatException e) {
+            throw MessageUtil.newCantFormatDateException(format, tdmSourceExpr, e, useTempModelExc);
+        }
+    }
+
+    /**
+     * @param blamedDateSourceExp
+     *            The blamed expression if an error occurs; only used for error messages.
+     * @param blamedFormatterExp
+     *            The blamed expression if an error occurs; only used for error messages.
+     */
+    String formatDateToPlainText(TemplateDateModel tdm, String formatString,
+            ASTExpression blamedDateSourceExp, ASTExpression blamedFormatterExp,
+            boolean useTempModelExc) throws TemplateException {
+        Date date = _EvalUtil.modelToDate(tdm, blamedDateSourceExp);
+        
+        TemplateDateFormat format = getTemplateDateFormat(
+                formatString, tdm.getDateType(), date.getClass(),
+                blamedDateSourceExp, blamedFormatterExp,
+                useTempModelExc);
+        
+        try {
+            return _EvalUtil.assertFormatResultNotNull(format.formatToPlainText(tdm));
+        } catch (TemplateValueFormatException e) {
+            throw MessageUtil.newCantFormatDateException(format, blamedDateSourceExp, e, useTempModelExc);
+        }
+    }
+
+    /**
+     * Gets a {@link TemplateDateFormat} using the date/time/datetime format settings and the current locale and time
+     * zone. (The current locale is the locale returned by {@link #getLocale()}. The current time zone is
+     * {@link #getTimeZone()} or {@link #getSQLDateAndTimeTimeZone()}).
+     * 
+     * @param dateType
+     *            The FTL date type; see the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * @param dateClass
+     *            The exact {@link Date} class, like {@link java.sql.Date} or {@link java.sql.Time}; this can influences
+     *            time zone selection. See also: {@link #setSQLDateAndTimeTimeZone(TimeZone)}
+     */
+    public TemplateDateFormat getTemplateDateFormat(int dateType, Class<? extends Date> dateClass)
+            throws TemplateValueFormatException {
+        boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
+        return getTemplateDateFormat(dateType, shouldUseSQLDTTimeZone(isSQLDateOrTime), isSQLDateOrTime);
+    }
+    
+    /**
+     * Gets a {@link TemplateDateFormat} for the specified format string and the current locale and time zone. (The
+     * current locale is the locale returned by {@link #getLocale()}. The current time zone is {@link #getTimeZone()} or
+     * {@link #getSQLDateAndTimeTimeZone()}).
+     * 
+     * <p>
+     * Note on performance: The result will be cached in the {@link Environment} instance. However, at least in 2.3.24
+     * the cached entries that depend on the current locale or the current time zone or the current date/time/datetime
+     * format of the {@link Environment} will be lost when those settings are changed.
+     * 
+     * @param formatString
+     *            Like {@code "iso m"} or {@code "dd.MM.yyyy HH:mm"} or {@code "@somethingCustom"} or
+     *            {@code "@somethingCustom params"}
+     * 
+     * @since 2.3.24
+     */
+    public TemplateDateFormat getTemplateDateFormat(
+            String formatString, int dateType, Class<? extends Date> dateClass)
+                    throws TemplateValueFormatException {
+        boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
+        return getTemplateDateFormat(
+                formatString, dateType,
+                shouldUseSQLDTTimeZone(isSQLDateOrTime), isSQLDateOrTime, true);
+    }
+
+    /**
+     * Like {@link #getTemplateDateFormat(String, int, Class)}, but allows you to use a different locale than the
+     * current one. If you want to use the current locale, use {@link #getTemplateDateFormat(String, int, Class)}
+     * instead.
+     * 
+     * <p>
+     * Performance notes regarding the locale and time zone parameters of
+     * {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} apply.
+     * 
+     * @param locale
+     *            Can't be {@code null}; See the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * 
+     * @see #getTemplateDateFormat(String, int, Class)
+     * 
+     * @since 2.4
+     */
+    public TemplateDateFormat getTemplateDateFormat(
+            String formatString,
+            int dateType, Class<? extends Date> dateClass,
+            Locale locale)
+                    throws TemplateValueFormatException {
+        boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
+        boolean useSQLDTTZ = shouldUseSQLDTTimeZone(isSQLDateOrTime);
+        return getTemplateDateFormat(
+                formatString,
+                dateType, locale, useSQLDTTZ ? getSQLDateAndTimeTimeZone() : getTimeZone(), isSQLDateOrTime);        
+    }
+
+    /**
+     * Like {@link #getTemplateDateFormat(String, int, Class)}, but allows you to use a different locale and time zone
+     * than the current one. If you want to use the current locale and time zone, use
+     * {@link #getTemplateDateFormat(String, int, Class)} instead.
+     * 
+     * <p>
+     * Performance notes regarding the locale and time zone parameters of
+     * {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} apply.
+     * 
+     * @param timeZone
+     *            The {@link TimeZone} used if {@code dateClass} is not an SQL date-only or time-only type. Can't be
+     *            {@code null}.
+     * @param sqlDateAndTimeTimeZone
+     *            The {@link TimeZone} used if {@code dateClass} is an SQL date-only or time-only type. Can't be
+     *            {@code null}.
+     * 
+     * @see #getTemplateDateFormat(String, int, Class)
+     * 
+     * @since 2.4
+     */
+    public TemplateDateFormat getTemplateDateFormat(
+            String formatString,
+            int dateType, Class<? extends Date> dateClass,
+            Locale locale, TimeZone timeZone, TimeZone sqlDateAndTimeTimeZone)
+                    throws TemplateValueFormatException {
+        boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
+        boolean useSQLDTTZ = shouldUseSQLDTTimeZone(isSQLDateOrTime);
+        return getTemplateDateFormat(
+                formatString,
+                dateType, locale, useSQLDTTZ ? sqlDateAndTimeTimeZone : timeZone, isSQLDateOrTime);        
+    }
+    
+    /**
+     * Gets a {@link TemplateDateFormat} for the specified parameters. This is mostly meant to be used by
+     * {@link TemplateDateFormatFactory} implementations to delegate to a format based on a specific format string. It
+     * works well for that, as its parameters are the same low level values as the parameters of
+     * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}. For other tasks
+     * consider the other overloads of this method.
+     * 
+     * <p>
+     * Note on performance (which was true at least for 2.3.24): Unless the locale happens to be equal to the current
+     * locale and the time zone with one of the current time zones ({@link #getTimeZone()} or
+     * {@link #getSQLDateAndTimeTimeZone()}), the {@link Environment}-level format cache can't be used, so the format
+     * string has to be parsed and the matching factory has to be get an invoked, which is much more expensive than
+     * getting the format from the cache. Thus the returned format should be stored by the caller for later reuse (but
+     * only within the current thread and in relation to the current {@link Environment}), if it will be needed
+     * frequently.
+     * 
+     * @param formatString
+     *            Like {@code "iso m"} or {@code "dd.MM.yyyy HH:mm"} or {@code "@somethingCustom"} or
+     *            {@code "@somethingCustom params"}
+     * @param dateType
+     *            The FTL date type; see the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * @param timeZone
+     *            Not {@code null}; See the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * @param locale
+     *            Not {@code null}; See the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * @param zonelessInput
+     *            See the similar parameter of
+     *            {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}
+     * 
+     * @since 2.3.24
+     */
+    public TemplateDateFormat getTemplateDateFormat(
+            String formatString,
+            int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput)
+                    throws TemplateValueFormatException {
+        Locale currentLocale = getLocale();
+        if (locale.equals(currentLocale)) {
+            int equalCurrentTZ;
+            TimeZone currentTimeZone = getTimeZone();
+            if (timeZone.equals(currentTimeZone)) {
+                equalCurrentTZ = 1;
+            } else {
+                TimeZone currentSQLDTTimeZone = getSQLDateAndTimeTimeZone();
+                if (timeZone.equals(currentSQLDTTimeZone)) {
+                    equalCurrentTZ = 2;
+                } else {
+                    equalCurrentTZ = 0;
+                }
+            }
+            if (equalCurrentTZ != 0) {
+                return getTemplateDateFormat(formatString, dateType, equalCurrentTZ == 2, zonelessInput, true);
+            }
+            // Falls through
+        }
+        return getTemplateDateFormatWithoutCache(formatString, dateType, locale, timeZone, zonelessInput);
+    }
+    
+    TemplateDateFormat getTemplateDateFormat(TemplateDateModel tdm, ASTExpression tdmSourceExpr, boolean useTempModelExc)
+            throws TemplateException {
+        Date date = _EvalUtil.modelToDate(tdm, tdmSourceExpr);
+        
+        return getTemplateDateFormat(
+                tdm.getDateType(), date.getClass(), tdmSourceExpr,
+                useTempModelExc);
+    }
+
+    /**
+     * Same as {@link #getTemplateDateFormat(int, Class)}, but translates the exceptions to {@link TemplateException}-s.
+     */
+    TemplateDateFormat getTemplateDateFormat(
+            int dateType, Class<? extends Date> dateClass, ASTExpression blamedDateSourceExp, boolean useTempModelExc)
+                    throws TemplateException {
+        try {
+            return getTemplateDateFormat(dateType, dateClass);
+        } catch (UnknownDateTypeFormattingUnsupportedException e) {
+            throw MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e);
+        } catch (TemplateValueFormatException e) {
+            String settingName;
+            String settingValue;
+            switch (dateType) {
+            case TemplateDateModel.TIME:
+                settingName = MutableProcessingConfiguration.TIME_FORMAT_KEY;
+                settingValue = getTimeFormat();
+                break;
+            case TemplateDateModel.DATE:
+                settingName = MutableProcessingConfiguration.DATE_FORMAT_KEY;
+                settingValue = getDateFormat();
+                break;
+            case TemplateDateModel.DATETIME:
+                settingName = MutableProcessingConfiguration.DATETIME_FORMAT_KEY;
+                settingValue = getDateTimeFormat();
+                break;
+            default:
+                settingName = "???";
+                settingValue = "???";
+            }
+            
+            _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                    "The value of the \"", settingName,
+                    "\" FreeMarker configuration setting is a malformed date/time/datetime format string: ",
+                    new _DelayedJQuote(settingValue), ". Reason given: ",
+                    e.getMessage());                    
+            throw useTempModelExc ? new _TemplateModelException(e, desc) : new _MiscTemplateException(e, desc);
+        }
+    }
+
+    /**
+     * Same as {@link #getTemplateDateFormat(String, int, Class)}, but translates the exceptions to
+     * {@link TemplateException}-s.
+     */
+    TemplateDateFormat getTemplateDateFormat(
+            String formatString, int dateType, Class<? extends Date> dateClass,
+            ASTExpression blamedDateSourceExp, ASTExpression blamedFormatterExp,
+            boolean useTempModelExc)
+                    throws TemplateException {
+        try {
+            return getTemplateDateFormat(formatString, dateType, dateClass);
+        } catch (UnknownDateTypeFormattingUnsupportedException e) {
+            throw MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e);
+        } catch (TemplateValueFormatException e) {
+            _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                    "Can't invoke date/time/datetime format based on format string ",
+                    new _DelayedJQuote(formatString), ". Reason given: ",
+                    e.getMessage())
+                    .blame(blamedFormatterExp);
+            throw useTempModelExc ? new _TemplateModelException(e, desc) : new _MiscTemplateException(e, desc);
+        }
+    }
+
+    /**
+     * Used to get the {@link TemplateDateFormat} according the date/time/datetime format settings, for the current
+     * locale and time zone. See {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} for the meaning
+     * of some of the parameters.
+     */
+    private TemplateDateFormat getTemplateDateFormat(int dateType, boolean useSQLDTTZ, boolean zonelessInput)
+            throws TemplateValueFormatException {
+        if (dateType == TemplateDateModel.UNKNOWN) {
+            throw new UnknownDateTypeFormattingUnsupportedException();
+        }
+        int cacheIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTZ);
+        TemplateDateFormat[] cachedTempDateFormatArray = this.cachedTempDateFormatArray;
+        if (cachedTempDateFormatArray == null) {
+            cachedTempDateFormatArray = new TemplateDateFormat[CACHED_TDFS_LENGTH];
+            this.cachedTempDateFormatArray = cachedTempDateFormatArray;
+        }
+        TemplateDateFormat format = cachedTempDateFormatArray[cacheIdx];
+        if (format == null) {
+            final String formatString;
+            switch (dateType) {
+            case TemplateDateModel.TIME:
+                formatString = getTimeFormat();
+                break;
+            case TemplateDateModel.DATE:
+                formatString = getDateFormat();
+                break;
+            case TemplateDateModel.DATETIME:
+                formatString = getDateTimeFormat();
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid date type enum: " + Integer.valueOf(dateType));
+            }
+
+            format = getTemplateDateFormat(formatString, dateType, useSQLDTTZ, zonelessInput, false);
+            
+            cachedTempDateFormatArray[cacheIdx] = format;
+        }
+        return format;
+    }
+
+    /**
+     * Used to get the {@link TemplateDateFormat} for the specified parameters, using the {@link Environment}-level
+     * cache. As the {@link Environment}-level cache currently only stores formats for the current locale and time zone,
+     * there's no parameter to specify those.
+     * 
+     * @param cacheResult
+     *            If the results should stored in the {@link Environment}-level cache. It will still try to get the
+     *            result from the cache regardless of this parameter.
+     */
+    private TemplateDateFormat getTemplateDateFormat(
+            String formatString, int dateType, boolean useSQLDTTimeZone, boolean zonelessInput,
+            boolean cacheResult)
+                    throws TemplateValueFormatException {
+        HashMap<String, TemplateDateFormat> cachedFormatsByFormatString;
+        readFromCache: do {
+            HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray = this.cachedTempDateFormatsByFmtStrArray;
+            if (cachedTempDateFormatsByFmtStrArray == null) {
+                if (cacheResult) {
+                    cachedTempDateFormatsByFmtStrArray = new HashMap[CACHED_TDFS_LENGTH];
+                    this.cachedTempDateFormatsByFmtStrArray = cachedTempDateFormatsByFmtStrArray;
+                } else {
+                    cachedFormatsByFormatString = null;
+                    break readFromCache;
+                }
+            }
+
+            TemplateDateFormat format;
+            {
+                int cacheArrIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTimeZone);
+                cachedFormatsByFormatString = cachedTempDateFormatsByFmtStrArray[cacheArrIdx];
+                if (cachedFormatsByFormatString == null) {
+                    if (cacheResult) {
+                        cachedFormatsByFormatString = new HashMap<>(4);
+                        cachedTempDateFormatsByFmtStrArray[cacheArrIdx] = cachedFormatsByFormatString;
+                        format = null;
+                    } else {
+                        break readFromCache;
+                    }
+                } else {
+                    format = cachedFormatsByFormatString.get(formatString);
+                }
+            }
+
+            if (format != null) {
+                return format;
+            }
+            // Cache miss; falls through
+        } while (false);
+
+        TemplateDateFormat format = getTemplateDateFormatWithoutCache(
+                formatString,
+                dateType, getLocale(), useSQLDTTimeZone ? getSQLDateAndTimeTimeZone() : getTimeZone(),
+                zonelessInput);
+        if (cacheResult) {
+            // We know here that cachedFormatsByFormatString != null
+            cachedFormatsByFormatString.put(formatString, format);
+        }
+        return format;
+    }
+
+    /**
+     * Returns the {@link TemplateDateFormat} for the given parameters without using the {@link Environment}-level
+     * cache. Of course, the {@link TemplateDateFormatFactory} involved might still uses its own cache, which can be
+     * global (class-loader-level) or {@link Environment}-level.
+     * 
+     * @param formatString
+     *            See the similar parameter of {@link TemplateDateFormatFactory#get}
+     * @param dateType
+     *            See the similar parameter of {@link TemplateDateFormatFactory#get}
+     * @param zonelessInput
+     *            See the similar parameter of {@link TemplateDateFormatFactory#get}
+     */
+    private TemplateDateFormat getTemplateDateFormatWithoutCache(
+            String formatString, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput)
+                    throws TemplateValueFormatException {
+        final int formatStringLen = formatString.length();
+        final String formatParams;
+
+        TemplateDateFormatFactory formatFactory;
+        char firstChar = formatStringLen != 0 ? formatString.charAt(0) : 0;
+
+        // As of Java 8, 'x' and 'i' (lower case) are illegal date format letters, so this is backward-compatible.
+        if (firstChar == 'x'
+                && formatStringLen > 1
+                && formatString.charAt(1) == 's') {
+            formatFactory = XSTemplateDateFormatFactory.INSTANCE;
+            formatParams = formatString; // for speed, we don't remove the prefix
+        } else if (firstChar == 'i'
+                && formatStringLen > 2
+                && formatString.charAt(1) == 's'
+                && formatString.charAt(2) == 'o') {
+            formatFactory = ISOTemplateDateFormatFactory.INSTANCE;
+            formatParams = formatString; // for speed, we don't remove the prefix
+        } else if (firstChar == '@'
+                && formatStringLen > 1
+                && Character.isLetter(formatString.charAt(1))) {
+            final String name;
+            {
+                int endIdx;
+                findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) {
+                    char c = formatString.charAt(endIdx);
+                    if (c == ' ' || c == '_') {
+                        break findParamsStart;
+                    }
+                }
+                name = formatString.substring(1, endIdx);
+                formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : "";
+            }
+
+            formatFactory = getCustomDateFormat(name);
+            if (formatFactory == null) {
+                throw new UndefinedCustomFormatException(
+                        "No custom date format was defined with name " + _StringUtil.jQuote(name));
+            }
+        } else {
+            formatParams = formatString;
+            formatFactory = JavaTemplateDateFormatFactory.INSTANCE;
+        }
+
+        return formatFactory.get(formatParams, dateType, locale, timeZone,
+                zonelessInput, this);
+    }
+
+    boolean shouldUseSQLDTTZ(Class dateClass) {
+        // Attention! If you update this method, update all overloads of it!
+        return dateClass != Date.class // This pre-condition is only for speed
+                && !isSQLDateAndTimeTimeZoneSameAsNormal()
+                && isSQLDateOrTimeClass(dateClass);
+    }
+
+    private boolean shouldUseSQLDTTimeZone(boolean sqlDateOrTime) {
+        // Attention! If you update this method, update all overloads of it!
+        return sqlDateOrTime && !isSQLDateAndTimeTimeZoneSameAsNormal();
+    }
+
+    /**
+     * Tells if the given class is or is subclass of {@link java.sql.Date} or {@link java.sql.Time}.
+     */
+    private static boolean isSQLDateOrTimeClass(Class dateClass) {
+  

<TRUNCATED>


Mime
View raw message