http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/Environment.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/Environment.java 
b/src/main/java/org/apache/freemarker/core/Environment.java
new file mode 100644
index 0000000..36eccb9
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/Environment.java
@@ -0,0 +1,2977 @@
+/*
+ * 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.StringWriter;
+import java.io.Writer;
+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.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.model.impl.SimpleSequence;
+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.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 Configurable {
+    
+    private static final ThreadLocal threadEnv = new ThreadLocal();
+
+    private static final Logger LOG = _CoreLogs.RUNTIME;
+    private static final Logger LOG_ATTEMPT = _CoreLogs.ATTEMPT;
+
+    // Do not use this object directly; clone 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;
+
+    /**
+     * 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 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 String 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 (Environment) threadEnv.get();
+    }
+
+    static void setCurrentEnvironment(Environment env) {
+        threadEnv.set(env);
+    }
+
+    public Environment(Template template, final TemplateHashModel 
rootDataModel, Writer out) {
+        super(template);
+        configuration = template.getConfiguration();
+        globalNamespace = new Namespace(null);
+        currentNamespace = mainNamespace = new Namespace(template);
+        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 mainNamespace.getTemplate();
+    }
+
+    /**
+     * 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();
+    }
+
+    /**
+     * 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 {
+        Object savedEnv = threadEnv.get();
+        threadEnv.set(this);
+        try {
+            // Cached values from a previous execution are possibly outdated.
+            clearCachedValues();
+            try {
+                doAutoImportsAndIncludes(this);
+                visit(getMainTemplate().getRootTreeNode());
+                // 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 {
+            threadEnv.set(savedEnv);
+        }
+    }
+
+    /**
+     * "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) {
+            SimpleSequence ss = new SimpleSequence(1);
+            ss.add(currentNamespace);
+            nodeNamespaces = ss;
+        }
+        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, _MiscTemplateException {
+        String catchAllParamName = macro.getCatchAll();
+        if (namedArgs != null) {
+            final SimpleHash catchAllParamValue;
+            if (catchAllParamName != null) {
+                catchAllParamValue = new SimpleHash((ObjectWrapper) null);
+                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 SimpleSequence catchAllParamValue;
+            if (catchAllParamName != null) {
+                catchAllParamValue = new SimpleSequence((ObjectWrapper) null);
+                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
+    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
+    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
+    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;
+        }
+    }
+
+    // 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(String urlEscapingCharset) {
+        cachedURLEscapingCharsetSet = false;
+        super.setURLEscapingCharset(urlEscapingCharset);
+    }
+
+    /*
+     * 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(String outputEncoding) {
+        cachedURLEscapingCharsetSet = false;
+        super.setOutputEncoding(outputEncoding);
+    }
+
+    /**
+     * 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.
+     */
+    String 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;
+    }
+
+    /**
+     * 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);
+        }
+    }
+
+    /**
+     * 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(Number number, 
BackwardCompatibleTemplateNumberFormat format, ASTExpression exp)
+            throws TemplateModelException, _MiscTemplateException {
+        try {
+            return format.format(number);
+        } catch (UnformattableValueException e) {
+            throw new _MiscTemplateException(exp, e, this,
+                    "Failed to format number with ", new 
_DelayedJQuote(format.getDescription()), ": ",
+                    e.getMessage());
+        }
+    }
+
+    /**
+     * 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
+    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
+    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;
+                }
+            }
+        }
+    }
+
+    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 TemplateModelException, TemplateException {
+        Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr);
+        
+        TemplateDateFormat format = getTemplateDateFormat(
+                tdm.getDateType(), date.getClass(), tdmSourceExpr,
+                useTempModelExc);
+        return format;
+    }
+
+    /**
+     * 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 = Configurable.TIME_FORMAT_KEY;
+                settingValue = getTimeFormat();
+                break;
+            case TemplateDateModel.DATE:
+                settingName = Configurable.DATE_FORMAT_KEY;
+                settingValue = getDateFormat();
+                break;
+            case TemplateDateModel.DATETIME:
+                settingName = Configurable.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 create 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) {
+        // We do shortcuts for the most common cases.
+        return dateClass != java.util.Date.class
+                && (dateClass == java.sql.Date.class || dateClass == Time.class
+                        || (dateClass != Timestamp.class
+                                && 
(java.sql.Date.class.isAssignableFrom(dateClass)
+                                        || 
Time.class.isAssignableFrom(dateClass))));
+    }
+
+    private int getTemplateDateFormatCacheArrayIndex(int dateType, boolean 
zonelessInput, boolean sqlDTTZ) {
+        return dateType
+                + (zonelessInput ? CACHED_TDFS_ZONELESS_INPUT_OFFS : 0)
+                + (sqlDTTZ ? CACHED_TDFS_SQL_D_T_TZ_OFFS : 0);
+    }
+
+    /**
+     * Returns the {@link DateToISO8601CalendarFactory} used by the the "iso_" 
built-ins. Be careful when using this; it
+     * should only by used with
+     * {@link _DateUtil#dateToISO8601String(Date, boolean, boolean, boolean, 
int, TimeZone, DateToISO8601CalendarFactory)}
+     * and {@link _DateUtil#dateToXSString(Date, boolean, boolean, boolean, 
int, TimeZone, DateToISO8601CalendarFactory)}
+     * .
+     */
+    DateToISO8601CalendarFactory getISOBuiltInCalendarFactory() {
+        if (isoBuiltInCalendarFactory == null) {
+            isoBuiltInCalendarFactory = new 
_DateUtil.TrivialDateToISO8601CalendarFactory();
+        }
+        return isoBuiltInCalendarFactory;
+    }
+
+    TemplateTransformModel getTransform(ASTExpression exp) throws 
TemplateException {
+        TemplateTransformModel ttm = null;
+        TemplateModel tm = exp.eval(this);
+        if (tm instanceof TemplateTransformModel) {
+            ttm = (TemplateTransformModel) tm;
+        } else if (exp instanceof ASTExpVariable) {
+            tm = configuration.getSharedVariable(exp.toString());
+            if (tm instanceof TemplateTransformModel) {
+                ttm = (TemplateTransformModel) tm;
+            }
+        }
+        return ttm;
+    }
+
+    /**
+     * Returns the loop or macro local variable corresponding to this variable 
name. Possibly null. (Note that the
+     * misnomer is kept for backward compatibility: loop variables are not 
local variables according to our
+     * terminology.)
+     */
+    public TemplateModel getLocalVariable(String name) throws 
TemplateModelException {
+        if (localContextStack != null) {
+            for (int i = localContextStack.size() - 1; i >= 0; i--) {
+                LocalContext lc = localContextStack.get(i);
+                TemplateModel tm = lc.getLocalVariable(name);
+                if (tm != null) {
+                    return tm;
+                }
+            }
+        }
+        return currentMacroContext == null ? null : 
currentMacroContext.getLocalVariable(name);
+    }
+
+    /**
+     * Returns the variable that is visible in this context, or {@code null} 
if the variable is not found. This is the
+     * correspondent to an FTL top-level variable reading expression. That is, 
it tries to find the the variable in this
+     * order:
+     * <ol>
+     * <li>An loop variable (if we're in a loop or user defined directive 
body) such as foo_has_next
+     * <li>A local variable (if we're in a macro)
+     * <li>A variable defined in the current namespace (say, via &lt;#assign 
...&gt;)
+     * <li>A variable defined globally (say, via &lt;#global ....&gt;)
+     * <li>Variable in the data model:
+     * <ol>
+     * <li>A variable in the root hash that was exposed to this rendering 
environment in the Template.process(...) call
+     * <li>A shared variable set in the configuration via a call to 
Configuration.setSharedVariable(...)
+     * </ol>
+     * </li>
+     * </ol>
+     */
+    public TemplateModel getVariable(String name) throws 
TemplateModelException {
+        TemplateModel result = getLocalVariable(name);
+        if (result == null) {
+            result = currentNamespace.get(name);
+        }
+        if (result == null) {
+            result = getGlobalVariable(name);
+        }
+        return result;
+    }
+
+    /**
+     * Returns the globally visible variable of the given name (or null). This 
is correspondent to FTL
+     * <code>.globals.<i>name</i></code>. This will first look at variables 
that were assigned globally via: &lt;#global
+     * ...&gt; and then at the data model exposed to the template.
+     */
+    public TemplateModel getGlobalVariable(String name) throws 
TemplateModelException {
+        TemplateModel result = globalNamespace.get(name);
+        if (result == null) {
+            result = rootDataModel.get(name);
+        }
+        if (result == null) {
+            result = configuration.getSharedVariable(name);
+        }
+        return result;
+    }
+
+    /**
+     * Sets a variable that is visible globally. This is correspondent to FTL
+     * <code>&lt;#global <i>name</i>=<i>model</i>&gt;</code>. This can be 
considered a convenient shorthand for:
+     * getGlobalNamespace().put(name, model)
+     */
+    public void setGlobalVariable(String name, TemplateModel model) {
+        globalNamespace.put(name, model);
+    }
+
+    /**
+     * Sets a variable in the current namespace. This is correspondent to FTL
+     * <code>&lt;#assign <i>name</i>=<i>model</i>&gt;</code>. This can be 
considered a convenient shorthand for:
+     * getCurrentNamespace().put(name, model)
+     */
+    public void setVariable(String name, TemplateModel model) {
+        currentNamespace.put(name, model);
+    }
+
+    /**
+     * Sets a local variable (one effective only during a macro invocation). 
This is correspondent to FTL
+     * <code>&lt;#local <i>name</i>=<i>model</i>&gt;</code>.
+     * 
+     * @param name
+     *            the identifier of the variable
+     * @param model
+     *            the value of the variable.
+     * @throws IllegalStateException
+     *             if the environment is not executing a macro body.
+     */
+    public void setLocalVariable(String name, TemplateModel model) {
+        if (currentMacroContext == null) {
+            throw new IllegalStateException("Not executing macro body");
+        }
+        currentMacroContext.setLocalVar(name, model);
+    }
+
+    /**
+     * Returns a set of variable names that are known at the time of call. 
This includes names of all shared variables
+     * in the {@link Configuration}, names of all global variables that were 
assigned during the template processing,
+     * names of all variables in the current name-space, names of all local 
variables and loop variables. If the passed
+     * root data model implements the {@link TemplateHashModelEx} interface, 
then all names it retrieves through a call
+     * to {@link TemplateHashModelEx#keys()} method are returned as well. The 
method returns a new Set object on each
+     * call that is completely disconnected from the Environment. That is, 
modifying the set will have no effect on the
+     * Environment object.
+     */
+    public Set getKnownVariableNames() throws TemplateModelException {
+        // shared vars.
+        Set set = configuration.getSharedVariableNames();
+
+        // root hash
+        if (rootDataModel instanceof TemplateHashModelEx) {
+            TemplateModelIterator rootNames = ((TemplateHashModelEx) 
rootDataModel).keys().iterator();
+            while (rootNames.hasNext()) {
+                set.add(((TemplateScalarModel) 
rootNames.next()).getAsString());
+            }
+        }
+
+        // globals
+        for (TemplateModelIterator tmi = globalNamespace.keys().iterator(); 
tmi.hasNext();) {
+            set.add(((TemplateScalarModel) tmi.next()).getAsString());
+        }
+
+        // current name-space
+        for (TemplateModelIterator tmi = currentNamespace.keys().iterator(); 
tmi.hasNext();) {
+            set.add(((TemplateScalarModel) tmi.next()).getAsString());
+        }
+
+        // locals and loop vars
+        if (currentMacroContext != null) {
+            set.addAll(currentMacroContext.getLocalVariableNames());
+        }
+        if (localContextStack != null) {
+            for (int i = localContextStack.size() - 1; i >= 0; i--) {
+                LocalContext lc = localContextStack.get(i);
+                set.addAll(lc.getLocalVariableNames());
+            }
+        }
+        return set;
+    }
+
+    /**
+     * Prints the current FTL stack trace. Useful for debugging. {@link 
TemplateException}s incorporate this information
+     * in their stack traces.
+     */
+    public void outputInstructionStack(PrintWriter pw) {
+        outputInstructionStack(getInstructionStackSnapshot(), false, pw);
+        pw.flush();
+    }
+
+    private static final int TERSE_MODE_INSTRUCTION_STACK_TRACE_LIMIT = 10;
+
+    /**
+     * Prints an FTL stack trace based on a stack trace snapshot.
+     * 
+     * @param w
+     *            If it's a {@link PrintWriter}, {@link PrintWriter#println()} 
will be used for line-breaks.
+     * @see #getInstructionStackSnapshot()
+     * @since 2.3.21
+     */
+    static void outputInstructionStack(
+            _ASTElement[] instructionStackSnapshot, boolean terseMode, Writer 
w) {
+        final PrintWriter pw = (PrintWriter) (w instanceof PrintWriter ? w : 
null);
+        try {
+            if (instructionStackSnapshot != null) {
+                final int totalFrames = instructionStackSnapshot.length;
+                int framesToPrint = terseMode
+                        ? (totalFrames <= 
TERSE_MODE_INSTRUCTION_STACK_TRACE_LIMIT
+                                ? totalFrames
+                                : TERSE_MODE_INSTRUCTION_STACK_TRACE_LIMIT - 1)
+                        : totalFrames;
+                boolean hideNestringRelatedFrames = terseMode && framesToPrint 
< totalFrames;
+                int nestingRelatedFramesHidden = 0;
+                int trailingFramesHidden = 0;
+                int framesPrinted = 0;
+                for (int frameIdx = 0; frameIdx < totalFrames; frameIdx++) {
+                    _ASTElement stackEl = instructionStackSnapshot[frameIdx];
+                    final boolean nestingRelatedElement = (frameIdx > 0 && 
stackEl instanceof ASTDirNested)
+                            || (frameIdx > 1 && 
instructionStackSnapshot[frameIdx - 1] instanceof ASTDirNested);
+                    if (framesPrinted < framesToPrint) {
+                        if (!nestingRelatedElement || 
!hideNestringRelatedFrames) {
+                            w.write(frameIdx == 0
+                                    ? "\t- Failed at: "
+                                    : (nestingRelatedElement
+                                            ? "\t~ Reached through: "
+                                            : "\t- Reached through: "));
+                            w.write(instructionStackItemToString(stackEl));
+                            if (pw != null) pw.println();
+                            else
+                                w.write('\n');
+                            framesPrinted++;
+                        } else {
+                            nestingRelatedFramesHidden++;
+      

<TRUNCATED>

Reply via email to