You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2015/09/08 00:29:34 UTC

[3/6] incubator-freemarker git commit: Added public Environment methods for getting TemplateDateFormat objects. Other formatting code clean up and JavaDoc comments.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/52dcb977/src/main/java/freemarker/core/Environment.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 5079acf..b616b5e 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -23,8 +23,9 @@ 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.DateFormat;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
@@ -75,19 +76,19 @@ import freemarker.template.utility.StringUtil;
 import freemarker.template.utility.UndeclaredThrowableException;
 
 /**
- * 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.
+ * 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>
+ * 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)}
+ * <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 {
 
@@ -98,10 +99,10 @@ public final class Environment extends Configurable {
 
     // 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));
+    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);
@@ -109,7 +110,7 @@ public final class Environment extends Configurable {
 
     private final Configuration configuration;
     private final TemplateHashModel rootDataModel;
-    private final ArrayList/*<TemplateElement>*/ instructionStack = new ArrayList();
+    private final ArrayList/* <TemplateElement> */ instructionStack = new ArrayList();
     private final ArrayList recoveredErrorStack = new ArrayList();
 
     private TemplateNumberFormat cachedTemplateNumberFormat;
@@ -117,19 +118,21 @@ public final class Environment extends Configurable {
 
     /**
      * 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}.
+     * 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>
+     * <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.
+     * <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. */
@@ -137,23 +140,24 @@ public final class Environment extends Configurable {
     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() 
+     * 
+     * @see #getISOBuiltInCalendarFactory()
      */
     private DateToISO8601CalendarFactory isoBuiltInCalendarFactory;
-    
+
     private Collator cachedCollator;
 
     private Writer out;
     private Macro.Context currentMacroContext;
-    private ArrayList localContextStack; 
+    private ArrayList localContextStack;
     private final Namespace mainNamespace;
     private Namespace currentNamespace, globalNamespace;
     private HashMap loadedLibs;
@@ -161,21 +165,21 @@ public final class Environment extends Configurable {
 
     private boolean inAttemptBlock;
     private Throwable lastThrowable;
-    
+
     private TemplateModel lastReturnValue;
     private HashMap macroToNamespaceLookup = new HashMap();
 
-    private TemplateNodeModel currentVisitorNode;    
+    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
@@ -185,7 +189,7 @@ public final class Environment extends Configurable {
     public static Environment getCurrentEnvironment() {
         return (Environment) threadEnv.get();
     }
-    
+
     static void setCurrentEnvironment(Environment env) {
         threadEnv.set(env);
     }
@@ -214,11 +218,11 @@ public final class Environment extends Configurable {
     public Template getTemplate() {
         return (Template) getParent();
     }
-    
+
     /** Returns the same value as pre-IcI 2.3.22 getTemplate() did. */
     Template getTemplate230() {
         Template legacyParent = (Template) this.legacyParent;
-        return legacyParent != null ? legacyParent : getTemplate(); 
+        return legacyParent != null ? legacyParent : getTemplate();
     }
 
     /**
@@ -232,7 +236,7 @@ public final class Environment extends Configurable {
     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
@@ -267,23 +271,22 @@ public final class Environment extends Configurable {
         }
         return null;
     }
-    
+
     /**
-     * Deletes cached values that meant to be valid only during a single
-     * template execution. 
+     * 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.
      */
@@ -308,12 +311,12 @@ public final class Environment extends Configurable {
             threadEnv.set(savedEnv);
         }
     }
-    
+
     /**
      * "Visit" the template element.
      */
     void visit(TemplateElement element)
-    throws TemplateException, IOException {
+            throws TemplateException, IOException {
         pushElement(element);
         try {
             element.accept(this);
@@ -323,16 +326,16 @@ public final class Environment extends Configurable {
             popElement();
         }
     }
-    
+
     /**
      * Instead of pushing into the element stack, we replace the top element for the time the parameter element is
      * visited, and then we restore the top element. The main purpose of this is to get rid of elements in the error
      * stack trace that from user perspective shouldn't have a stack frame. The typical example is
      * {@code [#if foo]...[@failsHere/]...[/#if]}, where the #if call shouldn't be in the stack trace. (Simply marking
-     * #if as hidden in stack traces would be wrong, because we still want to show #if when its test expression fails.)    
+     * #if as hidden in stack traces would be wrong, because we still want to show #if when its test expression fails.)
      */
     void visitByHiddingParent(TemplateElement element)
-    throws TemplateException, IOException {
+            throws TemplateException, IOException {
         TemplateElement parent = replaceTopElement(element);
         try {
             element.accept(this);
@@ -348,9 +351,9 @@ public final class Environment extends Configurable {
     }
 
     private static final TemplateModel[] NO_OUT_ARGS = new TemplateModel[0];
-    
+
     public void visit(final TemplateElement element,
-            TemplateDirectiveModel directiveModel, Map args, 
+            TemplateDirectiveModel directiveModel, Map args,
             final List bodyParameterNames) throws TemplateException, IOException {
         TemplateDirectiveBody nested;
         if (element == null) {
@@ -366,6 +369,7 @@ public final class Environment extends Configurable {
         }
         if (outArgs.length > 0) {
             pushLocalContext(new LocalContext() {
+
                 public TemplateModel getLocalVariable(String name) {
                     int index = bodyParameterNames.indexOf(name);
                     return index != -1 ? outArgs[index] : null;
@@ -384,26 +388,27 @@ public final class Environment extends Configurable {
             }
         }
     }
-    
+
     /**
-     * "Visit" the template element, passing the output
-     * through a TemplateTransformModel
-     * @param element the element to visit through a transform
-     * @param transform the transform to pass the element output
-     * through
-     * @param args optional arguments fed to the transform
+     * "Visit" the template element, passing the output through a TemplateTransformModel
+     * 
+     * @param element
+     *            the element to visit through a transform
+     * @param transform
+     *            the transform to pass the element output through
+     * @param args
+     *            optional arguments fed to the transform
      */
     void visitAndTransform(TemplateElement element,
-               TemplateTransformModel transform,
-               Map args)
-    throws TemplateException, IOException {
+            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;
+            TransformControl tc = tw instanceof TransformControl
+                    ? (TransformControl) tw
+                    : null;
 
             Writer prevOut = out;
             out = tw;
@@ -441,62 +446,61 @@ public final class Environment extends Configurable {
             handleTemplateException(te);
         }
     }
-    
+
     /**
      * Visit a block using buffering/recovery
      */
-     void visitAttemptRecover(TemplateElement attemptBlock, RecoveryBlock recoveryBlock) 
-     throws TemplateException, IOException {
-         Writer prevOut = this.out;
-         StringWriter sw = new StringWriter();
-         this.out = sw;
-         TemplateException thrownException = null;
-         boolean lastFIRE = setFastInvalidReferenceExceptions(false);
-         boolean lastInAttemptBlock = inAttemptBlock; 
-         try {
-             inAttemptBlock = true;
-             visitByHiddingParent(attemptBlock);
-         } catch (TemplateException te) {
-             thrownException = te;
-         } finally {
-             inAttemptBlock = lastInAttemptBlock;
-             setFastInvalidReferenceExceptions(lastFIRE);
-             this.out = prevOut;
-         }
-         if (thrownException != null) {
-             if (ATTEMPT_LOGGER.isDebugEnabled()) {
-                 ATTEMPT_LOGGER.debug("Error in attempt block " + 
-                         attemptBlock.getStartLocationQuoted(), thrownException);
-             }
-             try {
-                 recoveredErrorStack.add(thrownException);
-                 visit(recoveryBlock);
-             } 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;
-     }
+    void visitAttemptRecover(TemplateElement attemptBlock, RecoveryBlock recoveryBlock)
+            throws TemplateException, IOException {
+        Writer prevOut = this.out;
+        StringWriter sw = new StringWriter();
+        this.out = sw;
+        TemplateException thrownException = null;
+        boolean lastFIRE = setFastInvalidReferenceExceptions(false);
+        boolean lastInAttemptBlock = inAttemptBlock;
+        try {
+            inAttemptBlock = true;
+            visitByHiddingParent(attemptBlock);
+        } catch (TemplateException te) {
+            thrownException = te;
+        } finally {
+            inAttemptBlock = lastInAttemptBlock;
+            setFastInvalidReferenceExceptions(lastFIRE);
+            this.out = prevOut;
+        }
+        if (thrownException != null) {
+            if (ATTEMPT_LOGGER.isDebugEnabled()) {
+                ATTEMPT_LOGGER.debug("Error in attempt block " +
+                        attemptBlock.getStartLocationQuoted(), thrownException);
+            }
+            try {
+                recoveredErrorStack.add(thrownException);
+                visit(recoveryBlock);
+            } 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}.
@@ -508,7 +512,7 @@ public final class Environment extends Configurable {
         if (nestedContent != null) {
             this.currentMacroContext = invokingMacroContext.prevMacroContext;
             currentNamespace = invokingMacroContext.nestedContentNamespace;
-            
+
             final Configurable prevParent;
             final boolean parentReplacementOn = isBeforeIcI2322();
             prevParent = getParent();
@@ -517,7 +521,7 @@ public final class Environment extends Configurable {
             } else {
                 legacyParent = currentNamespace.getTemplate();
             }
-            
+
             this.localContextStack = invokingMacroContext.prevLocalContextStack;
             if (invokingMacroContext.nestedContentParameterNames != null) {
                 pushLocalContext(bodyCtx);
@@ -544,7 +548,7 @@ public final class Environment extends Configurable {
      * "visit" an IteratorBlock
      */
     boolean visitIteratorBlock(IteratorBlock.IterationContext ictxt)
-    throws TemplateException, IOException {
+            throws TemplateException, IOException {
         pushLocalContext(ictxt);
         try {
             return ictxt.accept(this);
@@ -555,12 +559,12 @@ public final class Environment extends Configurable {
             popLocalContext();
         }
     }
-    
+
     /**
      * Used for {@code #visit} and {@code #recurse}.
      */
-    void invokeNodeHandlerFor(TemplateNodeModel node, TemplateSequenceModel namespaces) 
-    throws TemplateException, IOException {
+    void invokeNodeHandlerFor(TemplateNodeModel node, TemplateSequenceModel namespaces)
+            throws TemplateException, IOException {
         if (nodeNamespaces == null) {
             SimpleSequence ss = new SimpleSequence(1);
             ss.add(currentNamespace);
@@ -580,21 +584,21 @@ public final class Environment extends Configurable {
             if (macroOrTransform instanceof Macro) {
                 invoke((Macro) macroOrTransform, null, null, null, null);
             } else if (macroOrTransform instanceof TemplateTransformModel) {
-                visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null); 
+                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());
+                        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")) {
+                    else if (!nodeType.equals("pi")
+                            && !nodeType.equals("comment")
+                            && !nodeType.equals("document_type")) {
                         throw new _MiscTemplateException(
                                 this, noNodeHandlerDefinedDescription(node, node.getNodeNamespace(), nodeType));
                     }
@@ -614,7 +618,7 @@ public final class Environment extends Configurable {
 
     private Object[] noNodeHandlerDefinedDescription(
             TemplateNodeModel node, String ns, String nodeType)
-    throws TemplateModelException {
+                    throws TemplateModelException {
         String nsPrefix;
         if (ns != null) {
             if (ns.length() > 0) {
@@ -626,44 +630,44 @@ public final class Environment extends Configurable {
             nsPrefix = "";
             ns = "";
         }
-        return new Object[] { "No macro or directive is defined for node named ",  
+        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 Macro) {
             invoke((Macro) macroOrTransform, null, null, null, null);
         } else if (macroOrTransform instanceof TemplateTransformModel) {
-            visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null); 
+            visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null);
         }
     }
-    
+
     /**
      * Calls the macro or function with the given arguments and nested block.
      */
-    void invoke(Macro macro, 
-               Map namedArgs, List positionalArgs, 
-               List bodyParameterNames, TemplateElement nestedBlock) throws TemplateException, IOException {
+    void invoke(Macro macro,
+            Map namedArgs, List positionalArgs,
+            List bodyParameterNames, TemplateElement nestedBlock) throws TemplateException, IOException {
         if (macro == Macro.DO_NOTHING_MACRO) {
             return;
         }
-        
+
         pushElement(macro);
         try {
             final Macro.Context macroCtx = macro.new Context(this, nestedBlock, bodyParameterNames);
             setMacroContextLocalsFromArguments(macroCtx, macro, namedArgs, positionalArgs);
-            
+
             final Macro.Context prevMacroCtx = currentMacroContext;
             currentMacroContext = macroCtx;
-            
+
             final ArrayList prevLocalContextStack = localContextStack;
             localContextStack = null;
-            
+
             final Namespace prevNamespace = currentNamespace;
             currentNamespace = (Namespace) macroToNamespaceLookup.get(macro);
-            
+
             try {
                 macroCtx.runMacro(this);
             } catch (ReturnInstruction.Return re) {
@@ -696,8 +700,8 @@ public final class Environment extends Configurable {
             } else {
                 catchAllParamValue = null;
             }
-            
-            for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext(); ) {
+
+            for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext();) {
                 final Map.Entry argNameAndValExp = (Map.Entry) it.next();
                 final String argName = (String) argNameAndValExp.getKey();
                 final boolean isArgNameDeclared = macro.hasArgNamed(argName);
@@ -723,11 +727,11 @@ public final class Environment extends Configurable {
             } else {
                 catchAllParamValue = null;
             }
-            
+
             String[] argNames = macro.getArgumentNamesInternal();
             final int argsCnt = positionalArgs.size();
             if (argNames.length < argsCnt && catchAllParamName == null) {
-                throw new _MiscTemplateException(this, 
+                throw new _MiscTemplateException(this,
                         (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
                         " only accepts ", new _DelayedToString(argNames.length), " parameters, but got ",
                         new _DelayedToString(argsCnt), ".");
@@ -748,7 +752,7 @@ public final class Environment extends Configurable {
             }
         }
     }
-    
+
     /**
      * Defines the given macro in the current namespace (doesn't call it).
      */
@@ -756,13 +760,13 @@ public final class Environment extends Configurable {
         macroToNamespaceLookup.put(macro, currentNamespace);
         currentNamespace.put(macro.getName(), macro);
     }
-    
+
     Namespace getMacroNamespace(Macro macro) {
         return (Namespace) macroToNamespaceLookup.get(macro);
     }
-    
+
     void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces)
-    throws TemplateException, IOException {
+            throws TemplateException, IOException {
         if (node == null) {
             node = this.getCurrentVisitorNode();
             if (node == null) {
@@ -783,9 +787,9 @@ public final class Environment extends Configurable {
     Macro.Context getCurrentMacroContext() {
         return currentMacroContext;
     }
-    
+
     private void handleTemplateException(TemplateException templateException)
-        throws TemplateException {
+            throws TemplateException {
         // Logic to prevent double-handling of the exception in
         // nested visit() calls.
         if (lastThrowable == templateException) {
@@ -814,7 +818,7 @@ public final class Environment extends Configurable {
         super.setTemplateExceptionHandler(templateExceptionHandler);
         lastThrowable = null;
     }
-    
+
     @Override
     public void setLocale(Locale locale) {
         Locale prevLocale = getLocale();
@@ -824,7 +828,7 @@ public final class Environment extends Configurable {
             if (cachedTemplateNumberFormat != null && cachedTemplateNumberFormat.isLocaleBound()) {
                 cachedTemplateNumberFormat = null;
             }
-    
+
             if (cachedTempDateFormatArray != null) {
                 for (int i = 0; i < CACHED_TDFS_LENGTH; i++) {
                     final TemplateDateFormat f = cachedTempDateFormatArray[i];
@@ -833,9 +837,9 @@ public final class Environment extends Configurable {
                     }
                 }
             }
-            
+
             cachedTempDateFormatsByFmtStrArray = null;
-            
+
             cachedCollator = null;
         }
     }
@@ -844,7 +848,7 @@ public final class Environment extends Configurable {
     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++) {
@@ -859,16 +863,16 @@ public final class Environment extends Configurable {
                     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++) {
@@ -880,14 +884,14 @@ public final class Environment extends Configurable {
             }
             if (cachedTempDateFormatsByFmtStrArray != null) {
                 for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) {
-                    cachedTempDateFormatsByFmtStrArray[i] = null; 
+                    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;
@@ -902,35 +906,32 @@ public final class Environment extends Configurable {
     boolean isSQLDateAndTimeTimeZoneSameAsNormal() {
         if (cachedSQLDateAndTimeTimeZoneSameAsNormal == null) {
             cachedSQLDateAndTimeTimeZoneSameAsNormal = Boolean.valueOf(
-                    getSQLDateAndTimeTimeZone() == null 
-                    || getSQLDateAndTimeTimeZone().equals(getTimeZone()));
+                    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.
+     * 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
-     * repeately. 
+     * 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 repeately.
      */
     String getEffectiveURLEscapingCharset() {
         if (!cachedURLEscapingCharsetSet) {
@@ -949,7 +950,7 @@ public final class Environment extends Configurable {
         }
         return cachedCollator;
     }
-    
+
     /**
      * Compares two {@link TemplateModel}-s according the rules of the FTL "==" operator.
      * 
@@ -962,8 +963,8 @@ public final class Environment extends Configurable {
 
     /**
      * 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.
+     * 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
      */
@@ -971,7 +972,7 @@ public final class Environment extends Configurable {
             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.
      * 
@@ -991,7 +992,7 @@ public final class Environment extends Configurable {
             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.
      * 
@@ -1028,18 +1029,30 @@ public final class Environment extends Configurable {
 
     /**
      * 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 formatNumber(TemplateNumberModel number, Expression exp) throws TemplateException {
         return formatNumber(number, getTemplateNumberFormat(exp), exp);
     }
 
     /**
-     * Format number with the number format specified as the parameter.
+     * 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 formatNumber(TemplateNumberModel number, String formatString, Expression exp) throws TemplateException {
         return formatNumber(number, getTemplateNumberFormat(formatString, exp), exp);
     }
 
+    /**
+     * 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 formatNumber(TemplateNumberModel number, TemplateNumberFormat format, Expression exp)
             throws TemplateModelException, _MiscTemplateException {
         try {
@@ -1051,6 +1064,12 @@ public final class Environment extends Configurable {
         }
     }
 
+    /**
+     * 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 formatNumber(Number number, BackwardCompatibleTemplateNumberFormat format, Expression exp)
             throws TemplateModelException, _MiscTemplateException {
         try {
@@ -1061,7 +1080,7 @@ public final class Environment extends Configurable {
                     e.getMessage());
         }
     }
-    
+
     /**
      * Returns the current number format ({@link #getNumberFormat()}) as {@link TemplateNumberFormat}.
      * 
@@ -1083,43 +1102,82 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * Returns the number format as {@link TemplateNumberFormat}, for the given format string and locale.
+     * 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 InvalidFormatStringException {
+        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. If {@code null} then the current locale ({@link #getLocale()}) will
-     *            be used. Note on performance (which was true at least for 2.3.24): If the locale is {@code null} or
-     *            equals to the current locale, then the {@link Environment}-level format cache will be used, otherwise
-     *            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 in the last case the number 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.
+     *            The locale of the number format; not {@code null}.
      * 
      * @since 2.3.24
      */
     public TemplateNumberFormat getTemplateNumberFormat(String formatString, Locale locale)
             throws InvalidFormatStringException {
-        if (locale == null || locale.equals(getLocale())) {
+        if (locale.equals(getLocale())) {
             getTemplateNumberFormat(formatString);
         }
-        
+
         return getTemplateNumberFormatWithoutCache(formatString, locale);
     }
 
     /**
-     * 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()}.)
-     * 
-     * @param formatString
-     *            A string that you could also use as the value of the {@code numberFormat} configuration setting.
+     * Convenience wrapper around {@link #getTemplateNumberFormat()} to be called during expression evaluation.
+     */
+    TemplateNumberFormat getTemplateNumberFormat(Expression exp) throws _MiscTemplateException {
+        TemplateNumberFormat format;
+        try {
+            format = getTemplateNumberFormat();
+        } catch (InvalidFormatStringException e) {
+            throw new _MiscTemplateException(exp, e, this,
+                    "Failed to get number format object for the current number format string, ",
+                    new _DelayedJQuote(getNumberFormat()), ": " + e.getMessage());
+        }
+        return format;
+    }
+
+    /**
+     * Convenience wrapper around {@link #getTemplateNumberFormat(String)} to be called during expression evaluation.
      * 
-     * @since 2.3.24
+     * @param exp
+     *            The blamed expression if an error occurs; it's only needed for better error messages
      */
-    public TemplateNumberFormat getTemplateNumberFormat(String formatString) throws InvalidFormatStringException {
-        return getTemplateNumberFormat(formatString, true);
+    TemplateNumberFormat getTemplateNumberFormat(String formatString, Expression exp)
+            throws _MiscTemplateException {
+        TemplateNumberFormat format;
+        try {
+            format = getTemplateNumberFormat(formatString);
+        } catch (InvalidFormatStringException e) {
+            throw new _MiscTemplateException(exp, e, this,
+                    "Failed to get number format object for the ", new _DelayedJQuote(formatString),
+                    " number format string: " + e.getMessage());
+        }
+        return format;
     }
-    
+
     /**
      * Gets the {@link TemplateNumberFormat} <em>for the current locale</em>.
      * 
@@ -1143,7 +1201,7 @@ public final class Environment extends Configurable {
         }
 
         TemplateNumberFormat format = getTemplateNumberFormatWithoutCache(formatString, getLocale());
-    
+
         if (cacheResult) {
             cachedTemplateNumberFormats.put(formatString, format);
         }
@@ -1179,13 +1237,13 @@ public final class Environment extends Configurable {
                 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 {
             if (formatStringLen > 1
@@ -1200,9 +1258,8 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * 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.
+     * 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
@@ -1213,37 +1270,6 @@ public final class Environment extends Configurable {
         return cNumberFormat;
     }
 
-    /**
-     * Convenience wrapper around {@link #getTemplateNumberFormat()} to be called during expression evaluation.
-     */
-    TemplateNumberFormat getTemplateNumberFormat(Expression exp) throws _MiscTemplateException {
-        TemplateNumberFormat format;
-        try {
-            format = getTemplateNumberFormat();
-        } catch (InvalidFormatStringException e) {
-            throw new _MiscTemplateException(exp, e, this,
-                    "Failed to get number format object for the current number format string, ",
-                    new _DelayedJQuote(getNumberFormat()), ": " + e.getMessage());
-        }
-        return format;
-    }
-
-    /**
-     * Convenience wrapper around {@link #getTemplateNumberFormat(String)} to be called during expression evaluation.
-     */
-    TemplateNumberFormat getTemplateNumberFormat(String formatString, Expression exp)
-            throws _MiscTemplateException {
-        TemplateNumberFormat format;
-        try {
-            format = getTemplateNumberFormat(formatString);
-        } catch (InvalidFormatStringException e) {
-            throw new _MiscTemplateException(exp, e, this,
-                    "Failed to get number format object for the ", new _DelayedJQuote(formatString),
-                    " number format string: " + e.getMessage());
-        }
-        return format;
-    }
-
     @Override
     public void setTimeFormat(String timeFormat) {
         String prevTimeFormat = getTimeFormat();
@@ -1286,85 +1312,239 @@ public final class Environment extends Configurable {
     public Configuration getConfiguration() {
         return configuration;
     }
-    
+
     TemplateModel getLastReturnValue() {
         return lastReturnValue;
     }
-    
+
     void setLastReturnValue(TemplateModel lastReturnValue) {
         this.lastReturnValue = lastReturnValue;
     }
-    
+
     void clearLastReturnValue() {
         this.lastReturnValue = null;
     }
 
-    String formatDate(TemplateDateModel tdm, Expression tdmSourceExpr) throws TemplateModelException {
+    /**
+     * @param tdmSourceExpr
+     *            The blamed expression if an error occurs; only used for error messages.
+     */
+    String formatDate(TemplateDateModel tdm, Expression tdmSourceExpr) throws TemplateException {
         Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr);
+        
+        TemplateDateFormat format = getTemplateDateFormat(
+                tdm.getDateType(), date.getClass(), tdmSourceExpr);
+        
         try {
-            boolean isSQLDateOrTime = isSQLDateOrTimeClass(date.getClass());
-            return getTemplateDateFormat(
-                    tdm.getDateType(), isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), tdmSourceExpr)
-                    .format(tdm);
-        } catch (UnknownDateTypeFormattingUnsupportedException e) {
-            throw MessageUtil.newCantFormatUnknownTypeDateException(tdmSourceExpr, e);
+            return format.format(tdm);
         } catch (UnformattableDateException e) {
             throw MessageUtil.newCantFormatDateException(tdmSourceExpr, e);
         }
     }
 
-    String formatDate(TemplateDateModel tdm, String formatString, Expression tdmSourceExpr)
-            throws TemplateModelException {
-        Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr);
-        boolean isSQLDateOrTime = isSQLDateOrTimeClass(date.getClass());
+    /**
+     * @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 formatDate(TemplateDateModel tdm, String formatString,
+            Expression blamedDateSourceExp, Expression blamedFormatterExp) throws TemplateException {
+        Date date = EvalUtil.modelToDate(tdm, blamedDateSourceExp);
+        
+        TemplateDateFormat format = getTemplateDateFormat(
+                formatString, tdm.getDateType(), date.getClass(),
+                blamedDateSourceExp, blamedFormatterExp);
+        
         try {
-            return getTemplateDateFormat(
-                    tdm.getDateType(), isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), formatString, true,
-                    null)
-                    .format(tdm);
-        } catch (UnknownDateTypeFormattingUnsupportedException e) {
-            throw MessageUtil.newCantFormatUnknownTypeDateException(tdmSourceExpr, e);
+            return format.format(tdm);
         } catch (UnformattableDateException e) {
-            throw MessageUtil.newCantFormatDateException(tdmSourceExpr, e);
+            throw MessageUtil.newCantFormatDateException(blamedDateSourceExp, e);
         }
     }
-    
+
     /**
-     * @param dateType The FTL date type, one of {@link TemplateDateModel#DATETIME}, {@link TemplateDateModel#TIME}
-     *          and {@link TemplateDateModel#DATE}. 
-     * @param dateClass The exact Java class of the formatted or created (via parsing) object. This matters because
-     *          the time zone is part of the returned {@link DateFormat}, and if
-     *          {@link #getSQLDateAndTimeTimeZone()} differs from {@link #getTimeZone()} then the exact class influences
-     *          the time zone.
-     * @param dateSourceExpr Used for better error messages only; may be {@code null}
+     * 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)}
      */
-    TemplateDateFormat getTemplateDateFormat(int dateType, Class/*<? extends Date>*/ dateClass, Expression dateSourceExpr)
-            throws TemplateModelException {
-        try {
-            boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
-            return getTemplateDateFormat(dateType, isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), dateSourceExpr);
-        } catch (UnknownDateTypeFormattingUnsupportedException e) {
-            throw MessageUtil.newCantFormatUnknownTypeDateException(dateSourceExpr, e);
-        }
+    public TemplateDateFormat getTemplateDateFormat(
+            int dateType, Class<? extends Date> dateClass)
+                    throws UnknownDateTypeFormattingUnsupportedException, UndefinedCustomFormatException,
+                    InvalidFormatParametersException {
+        boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
+        return getTemplateDateFormat(dateType, shouldUseSQLDTTimeZone(isSQLDateOrTime), isSQLDateOrTime);
     }
     
-    private TemplateDateFormat getTemplateDateFormat(
-            int dateType, boolean isSQLDateOrTime,
-            boolean useSQLDTTZ, Expression dateSourceExpr)
-            throws TemplateModelException, UnknownDateTypeFormattingUnsupportedException {
-        if (dateType == TemplateDateModel.UNKNOWN) {
-            throw MessageUtil.newCantFormatUnknownTypeDateException(dateSourceExpr, null);
-        }
-        int cacheIdx = getTemplateDateFormatCacheArrayIndex(dateType, isSQLDateOrTime, useSQLDTTZ);
-        TemplateDateFormat[] cachedTemplateDateFormats = this.cachedTempDateFormatArray;
-        if (cachedTemplateDateFormats == null) {
-            cachedTemplateDateFormats = new TemplateDateFormat[CACHED_TDFS_LENGTH];
-            this.cachedTempDateFormatArray = cachedTemplateDateFormats; 
+    /**
+     * 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 UnknownDateTypeFormattingUnsupportedException, UndefinedCustomFormatException,
+                    InvalidFormatParametersException {
+        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 UndefinedCustomFormatException, InvalidFormatParametersException,
+                    UnknownDateTypeFormattingUnsupportedException {
+        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 UndefinedCustomFormatException, InvalidFormatParametersException,
+                    UnknownDateTypeFormattingUnsupportedException {
+        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's
+     * 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 UndefinedCustomFormatException, InvalidFormatParametersException,
+                    UnknownDateTypeFormattingUnsupportedException {
+        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
         }
-        TemplateDateFormat format = cachedTemplateDateFormats[cacheIdx];
-        if (format == null) {
-            final String settingName;
-            final String settingValue;
+        return getTemplateDateFormatWithoutCache(formatString, dateType, locale, timeZone, zonelessInput);
+    }
+    
+    /**
+     * Same as {@link #getTemplateDateFormat(int, Class)}, but translates the exceptions to {@link TemplateException}-s.
+     */
+    TemplateDateFormat getTemplateDateFormat(
+            int dateType, Class<? extends Date> dateClass, Expression blamedDateSourceExp) throws TemplateException {
+        try {
+            return getTemplateDateFormat(dateType, dateClass);
+        } catch (UnknownDateTypeFormattingUnsupportedException e) {
+            throw MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e);
+        } catch (InvalidFormatStringException e) {
+            String settingName;
+            String settingValue;
             switch (dateType) {
             case TemplateDateModel.TIME:
                 settingName = Configurable.TIME_FORMAT_KEY;
@@ -1379,59 +1559,97 @@ public final class Environment extends Configurable {
                 settingValue = getDateTimeFormat();
                 break;
             default:
-                throw new _TemplateModelException("Invalid date type enum: ", Integer.valueOf(dateType));
-            } // switch
-
-            format = getTemplateDateFormat(
-                    dateType, isSQLDateOrTime,
-                    useSQLDTTZ, settingValue, false,
-                    settingName);
+                settingName = "???";
+                settingValue = "???";
+            }
             
-            cachedTemplateDateFormats[cacheIdx] = format;
+            throw new _MiscTemplateException(e,
+                    "The value of the \"", settingName,
+                    "\" FreeMarker configuration setting is a malformed date/time/datetime format string: ",
+                    new _DelayedJQuote(settingValue), ". Reason given: ",
+                    e.getMessage());
         }
-        return format;
     }
-    
+
     /**
-     * @param dateType {@link TemplateDateModel#UNKNOWN} is accepted or not depending on the {@code formatString}
-     *     value. When it isn't, a {@link TemplateModelException} will be thrown.
-     * @param dateClass The exact class of the date object, such as {@link java.sql.Timestamp}. 
-     * @param formatString Like "iso m" or "dd.MM.yyyy HH:mm" 
+     * Same as {@link #getTemplateDateFormat(String, int, Class)}, but translates the exceptions to
+     * {@link TemplateException}-s.
      */
     TemplateDateFormat getTemplateDateFormat(
-            int dateType, Class/*<? extends Date>*/ dateClass, String formatString, Expression dateSourceExpr)
-            throws TemplateModelException {
+            String formatString, int dateType, Class<? extends Date> dateClass,
+            Expression blamedDateSourceExp, Expression blamedFormatterExp) throws TemplateException {
         try {
-            boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
-            return getTemplateDateFormat(
-                    dateType, isSQLDateOrTime,
-                    shouldUseSQLDTTimeZone(isSQLDateOrTime), formatString, true,
-                    null);
+            return getTemplateDateFormat(formatString, dateType, dateClass);
         } catch (UnknownDateTypeFormattingUnsupportedException e) {
-            throw MessageUtil.newCantFormatUnknownTypeDateException(dateSourceExpr, e);
+            throw MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e);
+        } catch (InvalidFormatStringException e) {
+            throw new _MiscTemplateException(e,
+                    new _ErrorDescriptionBuilder(
+                            "Malformed date/time/datetime format string: ",
+                            new _DelayedJQuote(formatString), ". Reason given: ",
+                            e.getMessage())
+                            .blame(blamedFormatterExp));
         }
     }
-    
+
     /**
-     * @param dateType
-     *            See the similar parameter of {@link TemplateDateFormatFactory#get}
-     * @param zonelessInput
-     *            See the similar parameter of {@link TemplateDateFormatFactory#get}
-     * @param formatString
-     *            The string that describes the date format. See the similar parameter of
-     *            {@link TemplateDateFormatFactory#get}
-     * @param formatStringCfgSettingName
-     *            The name of the configuration setting where the {@code formatZtring} comes from, or {@code null}
-     *            if the format string was specified directly for this formatting call.
+     * 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 if the parameters.
+     */
+    private TemplateDateFormat getTemplateDateFormat(int dateType, boolean useSQLDTTZ, boolean zonelessInput)
+                    throws UnknownDateTypeFormattingUnsupportedException, UndefinedCustomFormatException,
+                    InvalidFormatParametersException {
+        if (dateType == TemplateDateModel.UNKNOWN) {
+            throw new UnknownDateTypeFormattingUnsupportedException();
+        }
+        int cacheIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTZ);
+        TemplateDateFormat[] cachedTemplateDateFormats = this.cachedTempDateFormatArray;
+        if (cachedTemplateDateFormats == null) {
+            cachedTemplateDateFormats = new TemplateDateFormat[CACHED_TDFS_LENGTH];
+            this.cachedTempDateFormatArray = cachedTemplateDateFormats;
+        }
+        TemplateDateFormat format = cachedTemplateDateFormats[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);
+            
+            cachedTemplateDateFormats[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(
-            int dateType, boolean zonelessInput, boolean useSQLDTTZ, String formatString, boolean cacheResult,
-            String formatStringCfgSettingName)
-                    throws TemplateModelException, UnknownDateTypeFormattingUnsupportedException {
+            String formatString, int dateType, boolean useSQLDTTimeZone, boolean zonelessInput,
+            boolean cacheResult)
+                    throws UnknownDateTypeFormattingUnsupportedException, UndefinedCustomFormatException,
+                    InvalidFormatParametersException {
         HashMap<String, TemplateDateFormat> cachedFormatsByFormatString;
         readFromCache: do {
-            HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray
-                    = this.cachedTempDateFormatsByFmtStrArray;
+            HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray = this.cachedTempDateFormatsByFmtStrArray;
             if (cachedTempDateFormatsByFmtStrArray == null) {
                 if (cacheResult) {
                     cachedTempDateFormatsByFmtStrArray = new HashMap[CACHED_TDFS_LENGTH];
@@ -1441,10 +1659,10 @@ public final class Environment extends Configurable {
                     break readFromCache;
                 }
             }
-            
+
             TemplateDateFormat format;
             {
-                int cacheArrIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTZ);
+                int cacheArrIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTimeZone);
                 cachedFormatsByFormatString = cachedTempDateFormatsByFmtStrArray[cacheArrIdx];
                 if (cachedFormatsByFormatString == null) {
                     if (cacheResult) {
@@ -1458,126 +1676,136 @@ public final class Environment extends Configurable {
                     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 UndefinedCustomFormatException, InvalidFormatParametersException,
+                    UnknownDateTypeFormattingUnsupportedException {
         final int formatStringLen = formatString.length();
         final String formatParams;
-        
-        // As of Java 8, 'x' and 'i' (in lower case) are illegal date format letters, so this is backward-compatible.
+
         TemplateDateFormatFactory formatFactory;
-        char firstChar = formatStringLen != 0 ? formatString.charAt(0) : 0; 
-        try {
-            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
-                    && formatString.charAt(1) != '@'
-                    && isIcI2324OrLater()) {
-                final String name;
-                {
-                    int endIdx;
-                    findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) {
-                        char c = formatString.charAt(endIdx);
-                        if (c == ' ' || c == '_') {
-                            break findParamsStart;
-                        }
+        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
+                && formatString.charAt(1) != '@'
+                && isIcI2324OrLater()) {
+            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 {
-                if (firstChar == '@'
-                        && formatStringLen > 1
-                        && formatString.charAt(1) == '@'
-                        && isIcI2324OrLater()) {
-                    // Unescape @ escaped as @@
-                    formatString = formatString.substring(1);
-                }
-                
-                formatFactory = JavaTemplateDateFormatFactory.INSTANCE;
-                formatParams = formatString;
+                name = formatString.substring(1, endIdx);
+                formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : "";
             }
 
-            TemplateDateFormat format = formatFactory.get(dateType, zonelessInput, formatParams, getLocale(),
-                    useSQLDTTZ ? getSQLDateAndTimeTimeZone() : getTimeZone(), this);
-            if (cacheResult) {
-                // We know here that cachedFormatsByFormatString != null            
-                cachedFormatsByFormatString.put(formatString, format);
+            formatFactory = getCustomDateFormat(name);
+            if (formatFactory == null) {
+                throw new UndefinedCustomFormatException(
+                        "No custom date format was defined with name " + StringUtil.jQuote(name));
             }
-            return format;
-        } catch (InvalidFormatStringException e) {
-            throw new _TemplateModelException(e,
-                    (formatStringCfgSettingName == null
-                            ? (Object) "Malformed date/time format string: "
-                            : new Object[] {
-                                    "The value of the \"", formatStringCfgSettingName,
-                                    "\" FreeMarker configuration setting is a malformed date/time format string: "
-                            }),
-                    new _DelayedJQuote(formatString), ". Reason given: ",
-                    e.getMessage());
+        } else {
+            String unescapedFormatString;
+            if (firstChar == '@'
+                    && formatStringLen > 1
+                    && formatString.charAt(1) == '@'
+                    && isIcI2324OrLater()) {
+                // Unescape @ escaped as @@
+                unescapedFormatString = formatString.substring(1);
+            } else {
+                unescapedFormatString = formatString;
+            }
+
+            formatFactory = JavaTemplateDateFormatFactory.INSTANCE;
+            formatParams = unescapedFormatString;
         }
+
+        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
+        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();
+        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 == java.sql.Time.class
-                        || (dateClass != java.sql.Timestamp.class
-                                    && ( 
-                                            java.sql.Date.class.isAssignableFrom(dateClass)
-                                            || java.sql.Time.class.isAssignableFrom(dateClass))));
+                && (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)}.
+     * 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) {
@@ -1585,7 +1813,7 @@ public final class Environment extends Configurable {
         }
         return isoBuiltInCalendarFactory;
     }
-    
+
     TemplateTransformModel getTransform(Expression exp) throws TemplateException {
         TemplateTransformModel ttm = null;
         TemplateModel tm = exp.eval(this);
@@ -1601,10 +1829,9 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * 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.)
+     * 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) {
@@ -1620,21 +1847,20 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * 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:
+     * 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>
+     * <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 {
@@ -1649,10 +1875,9 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * 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.
+     * 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);
@@ -1666,9 +1891,8 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * 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:
+     * 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) {
@@ -1676,9 +1900,8 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * 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:
+     * 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) {
@@ -1686,12 +1909,15 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * 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.
+     * 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) {
@@ -1701,40 +1927,36 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * 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.
+     * 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();
+            TemplateModelIterator rootNames = ((TemplateHashModelEx) rootDataModel).keys().iterator();
             while (rootNames.hasNext()) {
                 set.add(((TemplateScalarModel) rootNames.next()).getAsString());
             }
         }
-        
+
         // globals
-        for (TemplateModelIterator tmi = globalNamespace.keys().iterator(); tmi.hasNext(); ) {
+        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(); ) {
+        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());
@@ -1749,19 +1971,21 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * Prints the current FTL stack trace. Useful for debugging.
-     * {@link TemplateException}s incorporate this information in their stack traces.
+     * 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);
+        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 {

<TRUNCATED>