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>
