You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/02/23 21:36:07 UTC
[40/54] [partial] incubator-freemarker git commit: Unifying the
o.a.f.core and o.a.f.core.ast
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 "<" 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 "<" 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 ">" 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 ">=" 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 <#assign ...>)
+ * <li>A variable defined globally (say, via <#global ....>)
+ * <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: <#global
+ * ...> 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><#global <i>name</i>=<i>model</i>></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><#assign <i>name</i>=<i>model</i>></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><#local <i>name</i>=<i>model</i>></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>