/*
* Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
*
* Licensed 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 freemarker.core;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.text.Collator;
import java.text.DateFormat;
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.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import freemarker.cache.TemplateNameFormat;
import freemarker.cache._CacheAPI;
import freemarker.ext.beans.BeansWrapper;
import freemarker.log.Logger;
import freemarker.template.Configuration;
import freemarker.template.MalformedTemplateNameException;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleHash;
import freemarker.template.SimpleSequence;
import freemarker.template.Template;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateNodeModel;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
import freemarker.template.TemplateTransformModel;
import freemarker.template.TransformControl;
import freemarker.template._TemplateAPI;
import freemarker.template.utility.DateUtil;
import freemarker.template.utility.DateUtil.DateToISO8601CalendarFactory;
import freemarker.template.utility.NullWriter;
import freemarker.template.utility.StringUtil;
import freemarker.template.utility.UndeclaredThrowableException;
/**
* Object that represents the runtime environment during template processing.
* For every invocation of a <tt>Template.process()</tt> method, a new instance
* of this object is created, and then discarded when <tt>process()</tt> returns.
* This object stores the set of temporary variables created by the template,
* the value of settings set by the template, the reference to the data model root,
* etc. Everything that is needed to fulfill the template processing job.
*
* <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 = Logger.getLogger("freemarker.runtime");
private static final Logger ATTEMPT_LOGGER = Logger.getLogger("freemarker.runtime.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 final ArrayList/*<TemplateElement>*/ instructionStack = new ArrayList();
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 Template currentTemplate;
private Namespace currentNamespace;
private CallableInvocationContext currentMacroContext;
private Writer out;
private ArrayList localContextStack;
private final Namespace mainNamespace;
private Namespace globalNamespace;
private HashMap loadedLibs;
private Configurable legacyParent;
private boolean inAttemptBlock;
private Throwable lastThrowable;
private TemplateModel lastReturnValue;
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();
this.globalNamespace = new Namespace(null);
this.currentNamespace = mainNamespace = new Namespace(template);
this.currentTemplate = getMainTemplate();
this.out = out;
this.rootDataModel = rootDataModel;
predefineCallables(template);
}
/**
* Despite its name it just returns {@link #getParent()}. If {@link Configuration#getIncompatibleImprovements()} is
* at least 2.3.22, then that will be the same as {@link #getMainTemplate()}. Otherwise the returned value follows
* the {@link Environment} parent switchings that occur at {@code #include}/{@code #import} and {@code #nested}
* directive calls, that is, it's not very meaningful outside FreeMarker internals.
*
* @deprecated Use {@link #getMainTemplate()} or {@link #getCurrentTemplate()} (also relevant,
* {@link #getCurrentNamespace()} and then {@link Namespace#getTemplate()}); the value returned by this
* method is often not what you expect when it comes to macro/function invocations.
*/
@Deprecated
public Template getTemplate() {
return (Template) getParent();
}
/** Returns the same value as pre-IcI 2.3.22 getTemplate() did. */
Template getTemplate230() {
Template legacyParent = (Template) this.legacyParent;
return legacyParent != null ? legacyParent : getTemplate();
}
/**
* 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
*/
public Template getCurrentTemplate() {
return currentTemplate;
}
/**
* Gets the currently executing <em>custom</em> directive's call place information, or {@code null} if there's no
* executing custom directive. This currently only works for calls made from templates with the {@code <@...>}
* syntax. This should only be called from the {@link TemplateDirectiveModel} that was invoked with {@code <@...>},
* otherwise its return value is not defined by this API (it's usually {@code null}).
*
* @since 2.3.22
*/
public DirectiveCallPlace getCurrentDirectiveCallPlace() {
int ln = instructionStack.size();
if (ln == 0) return null;
TemplateElement te = (TemplateElement) instructionStack.get(ln - 1);
if (te instanceof UnifiedCall) return (UnifiedCall) te;
if (te instanceof Macro && ln > 1 && instructionStack.get(ln - 2) instanceof UnifiedCall) {
return (UnifiedCall) instructionStack.get(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(getTemplate().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(TemplateElement element)
throws TemplateException, IOException {
pushElement(element);
try {
element.accept(this);
} catch (TemplateException te) {
handleTemplateException(te);
} finally {
popElement();
}
}
/**
* Instead of pushing into the element stack, we replace the top element for the time the parameter element is
* visited, and then we restore the top element. The main purpose of this is to get rid of elements in the error
* stack trace that from user perspective shouldn't have a stack frame. The typical example is
* {@code [#if foo]...[@failsHere/]...[/#if]}, where the #if call shouldn't be in the stack trace. (Simply marking
* #if as hidden in stack traces would be wrong, because we still want to show #if when its test expression fails.)
*/
void visitByHiddingParent(TemplateElement element)
throws TemplateException, IOException {
TemplateElement parent = replaceTopElement(element);
try {
element.accept(this);
} catch (TemplateException te) {
handleTemplateException(te);
} finally {
replaceTopElement(parent);
}
}
private TemplateElement replaceTopElement(TemplateElement element) {
return (TemplateElement) instructionStack.set(instructionStack.size() - 1, element);
}
private static final TemplateModel[] NO_OUT_ARGS = new TemplateModel[0];
public void visit(final TemplateElement element,
TemplateDirectiveModel directiveModel, Map args,
final List bodyParameterNames) throws TemplateException, IOException {
TemplateDirectiveBody nested;
if (element == null) {
nested = null;
} else {
nested = new NestedElementTemplateDirectiveBody(element);
}
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() {
public TemplateModel getLocalVariable(String name) {
int index = bodyParameterNames.indexOf(name);
return index != -1 ? outArgs[index] : null;
}
public Collection getLocalVariableNames() {
return bodyParameterNames;
}
});
}
try {
directiveModel.execute(this, args, outArgs, nested);
} finally {
if (outArgs.length > 0) {
popLocalContext();
}
}
}
/**
* "Visit" the template element, passing the output
* through a TemplateTransformModel
* @param element the element to visit through a transform
* @param transform the transform to pass the element output
* through
* @param args optional arguments fed to the transform
*/
void visitAndTransform(TemplateElement element,
TemplateTransformModel transform,
Map args)
throws TemplateException, IOException {
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 {
if (element != null) {
visitByHiddingParent(element);
}
} 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(TemplateElement attemptBlock, RecoveryBlock recoveryBlock)
throws TemplateException, IOException {
Writer prevOut = this.out;
StringWriter sw = new StringWriter();
this.out = sw;
TemplateException thrownException = null;
boolean lastFIRE = setFastInvalidReferenceExceptions(false);
boolean lastInAttemptBlock = inAttemptBlock;
try {
inAttemptBlock = true;
visitByHiddingParent(attemptBlock);
} catch (TemplateException te) {
thrownException = te;
} finally {
inAttemptBlock = lastInAttemptBlock;
setFastInvalidReferenceExceptions(lastFIRE);
this.out = prevOut;
}
if (thrownException != null) {
if (ATTEMPT_LOGGER.isDebugEnabled()) {
ATTEMPT_LOGGER.debug("Error in attempt block " +
attemptBlock.getStartLocationQuoted(), thrownException);
}
try {
recoveredErrorStack.add(thrownException);
visit(recoveryBlock);
} finally {
recoveredErrorStack.remove(recoveredErrorStack.size() - 1);
}
} else {
out.write(sw.toString());
}
}
String getCurrentRecoveredErrorMessage() throws TemplateException {
if (recoveredErrorStack.isEmpty()) {
throw new _MiscTemplateException(this, ".error is not available outside of a #recover block");
}
return ((Throwable) recoveredErrorStack.get(recoveredErrorStack.size() - 1)).getMessage();
}
/**
* Tells if we are inside an <tt>#attempt</tt> block (but before <tt>#recover</tt>). This can be useful for
* {@link TemplateExceptionHandler}-s, as then they may don't want to print the error to the output, as
* <tt>#attempt</tt> will roll it back anyway.
*
* @since 2.3.20
*/
public boolean isInAttemptBlock() {
return inAttemptBlock;
}
/**
* Used for {@code #nested}.
*/
void invokeNestedContent(BodyInstruction.Context bodyCtx) throws TemplateException, IOException {
CallableInvocationContext invokingMacroContext = getCurrentMacroContext();
ArrayList prevLocalContextStack = localContextStack;
TemplateElement nestedContent = invokingMacroContext.nestedContent;
if (nestedContent != null) {
this.currentMacroContext = invokingMacroContext.prevMacroContext;
final Namespace prevCurrentNamespace = currentNamespace;
currentNamespace = invokingMacroContext.nestedContentNamespace;
final Template prevCurrentTemplate = currentTemplate;
currentTemplate = invokingMacroContext.nestedContentTemplate;
final Configurable prevParent;
final boolean parentReplacementOn = isBeforeIcI2322();
prevParent = getParent();
if (parentReplacementOn) {
setParent(currentNamespace.getTemplate());
} else {
legacyParent = currentNamespace.getTemplate();
}
this.localContextStack = invokingMacroContext.prevLocalContextStack;
if (invokingMacroContext.nestedContentParameterNames != null) {
pushLocalContext(bodyCtx);
}
try {
visit(nestedContent);
} finally {
if (invokingMacroContext.nestedContentParameterNames != null) {
popLocalContext();
}
this.currentMacroContext = invokingMacroContext;
currentNamespace = prevCurrentNamespace;
currentTemplate = prevCurrentTemplate;
if (parentReplacementOn) {
setParent(prevParent);
} else {
legacyParent = prevParent;
}
this.localContextStack = prevLocalContextStack;
}
}
}
/**
* "visit" an IteratorBlock
*/
boolean visitIteratorBlock(IteratorBlock.IterationContext ictxt)
throws TemplateException, IOException {
pushLocalContext(ictxt);
try {
return ictxt.accept(this);
} catch (TemplateException te) {
handleTemplateException(te);
return true;
} finally {
popLocalContext();
}
}
/**
* 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 = this.nodeNamespaceIndex;
String prevNodeName = this.currentNodeName;
String prevNodeNS = this.currentNodeNS;
TemplateSequenceModel prevNodeNamespaces = nodeNamespaces;
TemplateNodeModel prevVisitorNode = currentVisitorNode;
currentVisitorNode = node;
if (namespaces != null) {
this.nodeNamespaces = namespaces;
}
try {
TemplateModel macroOrTransform = getNodeProcessor(node);
if (macroOrTransform instanceof BoundCallable) {
invoke((BoundCallable) 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 {
this.currentVisitorNode = prevVisitorNode;
this.nodeNamespaceIndex = prevNodeNamespaceIndex;
this.currentNodeName = prevNodeName;
this.currentNodeNS = prevNodeNS;
this.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 BoundCallable) {
invoke((BoundCallable) 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(BoundCallable boundCallable,
Map namedArgs, List positionalArgs,
List bodyParameterNames, TemplateElement nestedBlock) throws TemplateException, IOException {
UnboundCallable unboundCallable = boundCallable.getUnboundCallable();
if (unboundCallable == UnboundCallable.NO_OP_MACRO) {
return;
}
pushElement(unboundCallable);
try {
final CallableInvocationContext macroCtx = new CallableInvocationContext(unboundCallable, this, nestedBlock, bodyParameterNames);
setMacroContextLocalsFromArguments(macroCtx, unboundCallable, namedArgs, positionalArgs);
final CallableInvocationContext prevMacroCtx = currentMacroContext;
currentMacroContext = macroCtx;
final ArrayList prevLocalContextStack = localContextStack;
localContextStack = null;
final Namespace prevCurrentNamespace = currentNamespace;
currentNamespace = boundCallable.getNamespace();
final Template prevCurrentTemplate = currentTemplate;
currentTemplate = boundCallable.getTemplate();
try {
macroCtx.invoce(this);
} catch (ReturnInstruction.Return re) {
// Not an error, just a <#return>
} catch (TemplateException te) {
handleTemplateException(te);
} finally {
currentMacroContext = prevMacroCtx;
localContextStack = prevLocalContextStack;
currentNamespace = prevCurrentNamespace;
currentTemplate = prevCurrentTemplate;
}
} finally {
popElement();
}
}
/**
* Sets the local variables corresponding to the macro call arguments in the macro context.
*/
private void setMacroContextLocalsFromArguments(
final CallableInvocationContext macroCtx,
final UnboundCallable unboundCallable,
final Map namedArgs, final List positionalArgs) throws TemplateException, _MiscTemplateException {
String catchAllParamName = unboundCallable.getCatchAll();
if (namedArgs != null) {
final SimpleHash catchAllParamValue;
if (catchAllParamName != null) {
catchAllParamValue = new SimpleHash((ObjectWrapper) null);
macroCtx.setLocalVar(catchAllParamName, catchAllParamValue);
} else {
catchAllParamValue = null;
}
for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext(); ) {
final Map.Entry argNameAndValExp = (Map.Entry) it.next();
final String argName = (String) argNameAndValExp.getKey();
final boolean isArgNameDeclared = unboundCallable.hasArgNamed(argName);
if (isArgNameDeclared || catchAllParamName != null) {
Expression argValueExp = (Expression) argNameAndValExp.getValue();
TemplateModel argValue = argValueExp.eval(this);
if (isArgNameDeclared) {
macroCtx.setLocalVar(argName, argValue);
} else {
catchAllParamValue.put(argName, argValue);
}
} else {
throw new _MiscTemplateException(this,
(unboundCallable.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(unboundCallable.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 = unboundCallable.getArgumentNamesInternal();
final int argsCnt = positionalArgs.size();
if (argNames.length < argsCnt && catchAllParamName == null) {
throw new _MiscTemplateException(this,
(unboundCallable.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(unboundCallable.getName()),
" only accepts ", new _DelayedToString(argNames.length), " parameters, but got ",
new _DelayedToString(argsCnt), ".");
}
for (int i = 0; i < argsCnt; i++) {
Expression argValueExp = (Expression) 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 visitCallableDefinition(UnboundCallable unboundCallable) {
currentNamespace.put(
unboundCallable.getName(),
new BoundCallable(unboundCallable, currentTemplate, currentNamespace));
}
void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces)
throws TemplateException, IOException {
if (node == null) {
node = this.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);
}
}
}
CallableInvocationContext 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
* repeately.
*/
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.
*/
String formatNumber(TemplateNumberModel number, Expression exp) throws TemplateException {
return formatNumber(number, getTemplateNumberFormat(exp), exp);
}
/**
* Format number with the number format specified as the parameter.
*/
String formatNumber(TemplateNumberModel number, String formatString, Expression exp) throws TemplateException {
return formatNumber(number, getTemplateNumberFormat(formatString, exp), exp);
}
String formatNumber(TemplateNumberModel number, TemplateNumberFormat format, Expression exp)
throws TemplateModelException, _MiscTemplateException {
try {
return format.format(number);
} catch (UnformattableNumberException e) {
throw new _MiscTemplateException(exp, e, this,
"Failed to format number with format ", new _DelayedJQuote(format.getDescription()), ": ",
e.getMessage());
}
}
String formatNumber(Number number, BackwardCompatibleTemplateNumberFormat format, Expression exp)
throws TemplateModelException, _MiscTemplateException {
try {
return format.format(number);
} catch (UnformattableNumberException e) {
throw new _MiscTemplateException(exp, e, this,
"Failed to format number with ", new _DelayedJQuote(format.getDescription()), ": ",
e.getMessage());
}
}
/**
* Returns the current number format as {@link TemplateNumberFormat}.
*
* @since 2.3.24
*/
public TemplateNumberFormat getTemplateNumberFormat() throws InvalidFormatStringException {
TemplateNumberFormat format = cachedTemplateNumberFormat;
if (format == null) {
format = getTemplateNumberFormat(getNumberFormat(), false, null);
cachedTemplateNumberFormat = format;
}
return format;
}
/**
* Returns the number format for the given format string as {@link TemplateNumberFormat}.
*
* @param formatString
* A string that you could also use as the value of the {@code numberFormat} configuration setting.
* @param locale
* Can be {@code null}, in which case the current locale will be used. Note that the current locale
* can change over time, and the format returned for a {@code null} parameter won't follow that change.
* Note that if the specified locale differs from the current locale, as of this writing, the
* {@link Environment}-level format cache won't be used.
*
* @since 2.3.24
*/
public TemplateNumberFormat getTemplateNumberFormat(String formatString, Locale locale)
throws InvalidFormatStringException {
return getTemplateNumberFormat(formatString, true, locale);
}
/**
* Same as {@link #getTemplateNumberFormat(String, Locale)} with {@code null} {@code locale} parameter.
*
* @since 2.3.24
*/
public TemplateNumberFormat getTemplateNumberFormat(String formatString) throws InvalidFormatStringException {
return getTemplateNumberFormat(formatString, (Locale) null);
}
/**
* @param locale
* Can be {@code null}, in which case the current locale will be used.
*/
private TemplateNumberFormat getTemplateNumberFormat(String formatString, boolean cacheResult, Locale locale)
throws InvalidFormatStringException {
boolean usesEnvLocale = locale == null || locale.equals(getLocale());
if (usesEnvLocale) {
if (cachedTemplateNumberFormats == null) {
if (cacheResult) {
cachedTemplateNumberFormats = new HashMap<String, TemplateNumberFormat>();
}
} else {
TemplateNumberFormat format = cachedTemplateNumberFormats.get(formatString);
if (format != null) {
return format;
}
}
}
Locale actualLocale = locale == null ? getLocale() : locale;
TemplateNumberFormat format;
int formatStringLen = formatString.length();
if (formatStringLen > 1
&& formatString.charAt(0) == '@'
&& formatString.charAt(1) != '@'
&& isIcI2324OrLater()) {
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));
}
format = formatFactory.get(params, actualLocale, this);
} else {
if (formatStringLen > 1
&& formatString.charAt(0) == '@'
&& formatString.charAt(1) == '@'
&& isIcI2324OrLater()) {
// Unescape @ escaped as @@
formatString = formatString.substring(1);
}
format = JavaTemplateNumberFormatFactory.INSTANCE.get(formatString, actualLocale, this);
}
if (cacheResult && usesEnvLocale) {
cachedTemplateNumberFormats.put(formatString, format);
}
return format;
}
/**
* 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;
}
TemplateNumberFormat getTemplateNumberFormat(Expression exp)
throws _MiscTemplateException {
TemplateNumberFormat format;
try {
format = getTemplateNumberFormat();
} catch (InvalidFormatStringException e) {
throw new _MiscTemplateException(exp, e, this,
"Failed to get number format object for the current number format string, ",
new _DelayedJQuote(getNumberFormat()), ": " + e.getMessage());
}
return format;
}
TemplateNumberFormat getTemplateNumberFormat(String formatString, Expression exp)
throws _MiscTemplateException {
TemplateNumberFormat format;
try {
format = getTemplateNumberFormat(formatString);
} catch (InvalidFormatStringException e) {
throw new _MiscTemplateException(exp, e, this,
"Failed to get number format object for the ", new _DelayedJQuote(formatString),
" number format string: " + e.getMessage());
}
return format;
}
@Override
public void setTimeFormat(String timeFormat) {
String prevTimeFormat = getTimeFormat();
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() {
this.lastReturnValue = null;
}
String formatDate(TemplateDateModel tdm, Expression tdmSourceExpr) throws TemplateModelException {
Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr);
try {
boolean isSQLDateOrTime = isSQLDateOrTimeClass(date.getClass());
return getTemplateDateFormat(
tdm.getDateType(), isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), tdmSourceExpr)
.format(tdm);
} catch (UnknownDateTypeFormattingUnsupportedException e) {
throw MessageUtil.newCantFormatUnknownTypeDateException(tdmSourceExpr, e);
} catch (UnformattableDateException e) {
throw MessageUtil.newCantFormatDateException(tdmSourceExpr, e);
}
}
String formatDate(TemplateDateModel tdm, String formatString, Expression tdmSourceExpr)
throws TemplateModelException {
Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr);
boolean isSQLDateOrTime = isSQLDateOrTimeClass(date.getClass());
try {
return getTemplateDateFormat(
tdm.getDateType(), isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), formatString, true,
null)
.format(tdm);
} catch (UnknownDateTypeFormattingUnsupportedException e) {
throw MessageUtil.newCantFormatUnknownTypeDateException(tdmSourceExpr, e);
} catch (UnformattableDateException e) {
throw MessageUtil.newCantFormatDateException(tdmSourceExpr, e);
}
}
/**
* @param dateType The FTL date type, one of {@link TemplateDateModel#DATETIME}, {@link TemplateDateModel#TIME}
* and {@link TemplateDateModel#DATE}.
* @param dateClass The exact Java class of the formatted or created (via parsing) object. This matters because
* the time zone is part of the returned {@link DateFormat}, and if
* {@link #getSQLDateAndTimeTimeZone()} differs from {@link #getTimeZone()} then the exact class influences
* the time zone.
* @param dateSourceExpr Used for better error messages only; may be {@code null}
*/
TemplateDateFormat getTemplateDateFormat(int dateType, Class/*<? extends Date>*/ dateClass, Expression dateSourceExpr)
throws TemplateModelException {
try {
boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
return getTemplateDateFormat(dateType, isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), dateSourceExpr);
} catch (UnknownDateTypeFormattingUnsupportedException e) {
throw MessageUtil.newCantFormatUnknownTypeDateException(dateSourceExpr, e);
}
}
private TemplateDateFormat getTemplateDateFormat(
int dateType, boolean isSQLDateOrTime,
boolean useSQLDTTZ, Expression dateSourceExpr)
throws TemplateModelException, UnknownDateTypeFormattingUnsupportedException {
if (dateType == TemplateDateModel.UNKNOWN) {
throw MessageUtil.newCantFormatUnknownTypeDateException(dateSourceExpr, null);
}
int cacheIdx = getTemplateDateFormatCacheArrayIndex(dateType, isSQLDateOrTime, useSQLDTTZ);
TemplateDateFormat[] cachedTemplateDateFormats = this.cachedTempDateFormatArray;
if (cachedTemplateDateFormats == null) {
cachedTemplateDateFormats = new TemplateDateFormat[CACHED_TDFS_LENGTH];
this.cachedTempDateFormatArray = cachedTemplateDateFormats;
}
TemplateDateFormat format = cachedTemplateDateFormats[cacheIdx];
if (format == null) {
final String settingName;
final 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:
throw new _TemplateModelException("Invalid date type enum: ", Integer.valueOf(dateType));
} // switch
format = getTemplateDateFormat(
dateType, isSQLDateOrTime,
useSQLDTTZ, settingValue, false,
settingName);
cachedTemplateDateFormats[cacheIdx] = format;
}
return format;
}
/**
* @param dateType {@link TemplateDateModel#UNKNOWN} is accepted or not depending on the {@code formatString}
* value. When it isn't, a {@link TemplateModelException} will be thrown.
* @param dateClass The exact class of the date object, such as {@link java.sql.Timestamp}.
* @param formatString Like "iso m" or "dd.MM.yyyy HH:mm"
*/
TemplateDateFormat getTemplateDateFormat(
int dateType, Class/*<? extends Date>*/ dateClass, String formatString, Expression dateSourceExpr)
throws TemplateModelException {
try {
boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass);
return getTemplateDateFormat(
dateType, isSQLDateOrTime,
shouldUseSQLDTTimeZone(isSQLDateOrTime), formatString, true,
null);
} catch (UnknownDateTypeFormattingUnsupportedException e) {
throw MessageUtil.newCantFormatUnknownTypeDateException(dateSourceExpr, e);
}
}
/**
* @param dateType
* See the similar parameter of {@link TemplateDateFormatFactory#get}
* @param zonelessInput
* See the similar parameter of {@link TemplateDateFormatFactory#get}
* @param formatString
* The string that describes the date format. See the similar parameter of
* {@link TemplateDateFormatFactory#get}
* @param formatStringCfgSettingName
* The name of the configuration setting where the {@code formatZtring} comes from, or {@code null}
* if the format string was specified directly for this formatting call.
*/
private TemplateDateFormat getTemplateDateFormat(
int dateType, boolean zonelessInput, boolean useSQLDTTZ, String formatString, boolean cacheResult,
String formatStringCfgSettingName)
throws TemplateModelException, UnknownDateTypeFormattingUnsupportedException {
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, useSQLDTTZ);
cachedFormatsByFormatString = cachedTempDateFormatsByFmtStrArray[cacheArrIdx];
if (cachedFormatsByFormatString == null) {
if (cacheResult) {
cachedFormatsByFormatString = new HashMap<String, TemplateDateFormat>(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);
final int formatStringLen = formatString.length();
final String formatParams;
// As of Java 8, 'x' and 'i' (in lower case) are illegal date format letters, so this is backward-compatible.
TemplateDateFormatFactory formatFactory;
char firstChar = formatStringLen != 0 ? formatString.charAt(0) : 0;
try {
if (
firstChar == 'x'
&& formatStringLen > 1
&& formatString.charAt(1) == 's') {
formatFactory = XSTemplateDateFormatFactory.INSTANCE;
formatParams = formatString; // for speed, we don't remove the prefix
} else if (
firstChar == 'i'
&& formatStringLen > 2
&& formatString.charAt(1) == 's'
&& formatString.charAt(2) == 'o') {
formatFactory = ISOTemplateDateFormatFactory.INSTANCE;
formatParams = formatString; // for speed, we don't remove the prefix
} else if (firstChar == '@'
&& formatStringLen > 1
&& formatString.charAt(1) != '@'
&& isIcI2324OrLater()) {
final String name;
{
int endIdx;
findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) {
char c = formatString.charAt(endIdx);
if (c == ' ' || c == '_') {
break findParamsStart;
}
}
name = formatString.substring(1, endIdx);
formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : "";
}
formatFactory = getCustomDateFormat(name);
if (formatFactory == null) {
throw new UndefinedCustomFormatException(
"No custom date format was defined with name " + StringUtil.jQuote(name));
}
} else {
if (firstChar == '@'
&& formatStringLen > 1
&& formatString.charAt(1) == '@'
&& isIcI2324OrLater()) {
// Unescape @ escaped as @@
formatString = formatString.substring(1);
}
formatFactory = JavaTemplateDateFormatFactory.INSTANCE;
formatParams = formatString;
}
TemplateDateFormat format = formatFactory.get(dateType, zonelessInput, formatParams, getLocale(),
useSQLDTTZ ? getSQLDateAndTimeTimeZone() : getTimeZone(), this);
if (cacheResult) {
// We know here that cachedFormatsByFormatString != null
cachedFormatsByFormatString.put(formatString, format);
}
return format;
} catch (InvalidFormatStringException e) {
throw new _TemplateModelException(e,
(formatStringCfgSettingName == null
? (Object) "Malformed date/time format string: "
: new Object[] {
"The value of the \"", formatStringCfgSettingName,
"\" FreeMarker configuration setting is a malformed date/time format string: "
}),
new _DelayedJQuote(formatString), ". Reason given: ",
e.getMessage());
}
}
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 == java.sql.Time.class
|| (dateClass != java.sql.Timestamp.class
&& (
java.sql.Date.class.isAssignableFrom(dateClass)
|| java.sql.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(Expression exp) throws TemplateException {
TemplateTransformModel ttm = null;
TemplateModel tm = exp.eval(this);
if (tm instanceof TemplateTransformModel) {
ttm = (TemplateTransformModel) tm;
} else if (exp instanceof Identifier) {
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 = (LocalContext) 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 = (LocalContext) 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(
TemplateElement[] 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++) {
TemplateElement stackEl = instructionStackSnapshot[frameIdx];
final boolean nestingRelatedElement = (frameIdx > 0 && stackEl instanceof BodyInstruction)
|| (frameIdx > 1 && instructionStackSnapshot[frameIdx - 1] instanceof BodyInstruction);
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++;
}
} else {
trailingFramesHidden++;
}
}
boolean hadClosingNotes = false;
if (trailingFramesHidden > 0) {
w.write("\t... (Had ");
w.write(String.valueOf(trailingFramesHidden + nestingRelatedFramesHidden));
w.write(" more, hidden for tersenes)");
hadClosingNotes = true;
}
if (nestingRelatedFramesHidden > 0) {
if (hadClosingNotes) {
w.write(' ');
} else {
w.write('\t');
}
w.write("(Hidden " + nestingRelatedFramesHidden + " \"~\" lines for terseness)");
if (pw != null) pw.println(); else w.write('\n');
hadClosingNotes = true;
}
if (hadClosingNotes) {
if (pw != null) pw.println(); else w.write('\n');
}
} else {
w.write("(The stack was empty)");
if (pw != null) pw.println(); else w.write('\n');
}
} catch (IOException e) {
LOG.error("Failed to print FTL stack trace", e);
}
}
/**
* Returns the snapshot of what would be printed as FTL stack trace.
* @since 2.3.20
*/
TemplateElement[] getInstructionStackSnapshot() {
int requiredLength = 0;
int ln = instructionStack.size();
for (int i = 0; i < ln; i++) {
TemplateElement stackEl = (TemplateElement) instructionStack.get(i);
if (i == ln || stackEl.isShownInStackTrace()) {
requiredLength++;
}
}
if (requiredLength == 0) return null;
TemplateElement[] result = new TemplateElement[requiredLength];
int dstIdx = requiredLength - 1;
for (int i = 0; i < ln; i++) {
TemplateElement stackEl = (TemplateElement) instructionStack.get(i);
if (i == ln || stackEl.isShownInStackTrace()) {
result[dstIdx--] = stackEl;
}
}
return result;
}
static String instructionStackItemToString(TemplateElement stackEl) {
StringBuilder sb = new StringBuilder();
appendInstructionStackItem(stackEl, sb);
return sb.toString();
}
static void appendInstructionStackItem(TemplateElement stackEl, StringBuilder sb) {
sb.append(MessageUtil.shorten(stackEl.getDescription(), 40));
sb.append(" [");
Macro enclosingMacro = getEnclosingMacro(stackEl);
if (enclosingMacro != null) {
sb.append(MessageUtil.formatLocationForEvaluationError(
enclosingMacro, stackEl.beginLine, stackEl.beginColumn));
} else {
sb.append(MessageUtil.formatLocationForEvaluationError(
stackEl.getUnboundTemplate(), stackEl.beginLine, stackEl.beginColumn));
}
sb.append("]");
}
static private Macro getEnclosingMacro(TemplateElement stackEl) {
while (stackEl != null) {
if (stackEl instanceof Macro) return (Macro) stackEl;
stackEl = stackEl.getParentElement();
}
return null;
}
private void pushLocalContext(LocalContext localContext) {
if (localContextStack == null) {
localContextStack = new ArrayList();
}
localContextStack.add(localContext);
}
private void popLocalContext() {
localContextStack.remove(localContextStack.size() - 1);
}
ArrayList getLocalContextStack() {
return localContextStack;
}
/**
* Returns the name-space for the name if exists, or null.
* @param name the template path that you have used with the <code>import</code> directive
* or {@link #importLib(String, String)} call, in normalized form. That is, the path must be an absolute
* path, and it must not contain "/../" or "/./". The leading "/" is optional.
*/
public Namespace getNamespace(String name) {
if (name.startsWith("/")) name = name.substring(1);
if (loadedLibs != null) {
return (Namespace) loadedLibs.get(name);
} else {
return null;
}
}
/**
* Returns the main namespace.
* This corresponds to the FTL {@code .main} hash.
*/
public Namespace getMainNamespace() {
return mainNamespace;
}
/**
* Returns the current namespace.
* This corresponds to the FTL {@code .namespace} hash.
* Initially, the current name space is the main namespace, but when inside an {@code #import}-ed template, it will
* change to the namespace of that import. Note that {@code #include} doesn't affect the namespace, so if you are
* in an {@code #import}-ed template and then from there do an {@code #include}, the current namespace will remain
* the namespace of the {@code #import}.
*/
public Namespace getCurrentNamespace() {
return currentNamespace;
}
/**
* Returns the name-space that contains the globally visible non-data-model variables
* (usually created with {@code <#global ...>}).
*/
public Namespace getGlobalNamespace() {
return globalNamespace;
}
/**
* Returns the data-model (also known as the template context in some other template engines).
*/
public TemplateHashModel getDataModel() {
final TemplateHashModel result = new TemplateHashModel() {
public boolean isEmpty() {
return false;
}
public TemplateModel get(String key) throws TemplateModelException {
TemplateModel value = rootDataModel.get(key);
if (value == null) {
value = configuration.getSharedVariable(key);
}
return value;
}
};
if (rootDataModel instanceof TemplateHashModelEx) {
return new TemplateHashModelEx() {
public boolean isEmpty() throws TemplateModelException {
return result.isEmpty();
}
public TemplateModel get(String key) throws TemplateModelException {
return result.get(key);
}
//NB: The methods below do not take into account
// configuration shared variables even though
// the hash will return them, if only for BWC reasons
public TemplateCollectionModel values() throws TemplateModelException {
return ((TemplateHashModelEx) rootDataModel).values();
}
public TemplateCollectionModel keys() throws TemplateModelException {
return ((TemplateHashModelEx) rootDataModel).keys();
}
public int size() throws TemplateModelException {
return ((TemplateHashModelEx) rootDataModel).size();
}
};
}
return result;
}
/**
* Returns the read-only hash of globally visible variables.
* This is the correspondent of FTL <code>.globals</code> hash.
* That is, you see the variables created with
* <code><#global ...></code>, and the variables of the data-model.
* To create new global variables, use {@link #setGlobalVariable setGlobalVariable}.
*/
public TemplateHashModel getGlobalVariables() {
return new TemplateHashModel() {
public boolean isEmpty() {
return false;
}
public TemplateModel get(String key) throws TemplateModelException {
TemplateModel result = globalNamespace.get(key);
if (result == null) {
result = rootDataModel.get(key);
}
if (result == null) {
result = configuration.getSharedVariable(key);
}
return result;
}
};
}
private void pushElement(TemplateElement element) {
instructionStack.add(element);
}
private void popElement() {
instructionStack.remove(instructionStack.size() - 1);
}
void replaceElementStackTop(TemplateElement instr) {
instructionStack.set(instructionStack.size() - 1, instr);
}
public TemplateNodeModel getCurrentVisitorNode() {
return currentVisitorNode;
}
/**
* sets TemplateNodeModel as the current visitor node. <tt>.current_node</tt>
*/
public void setCurrentVisitorNode(TemplateNodeModel node) {
currentVisitorNode = node;
}
TemplateModel getNodeProcessor(TemplateNodeModel node) throws TemplateException {
String nodeName = node.getNodeName();
if (nodeName == null) {
throw new _MiscTemplateException(this, "Node name is null.");
}
TemplateModel result = getNodeProcessor(nodeName, node.getNodeNamespace(), 0);
if (result == null) {
String type = node.getNodeType();
/* DD: Original version: */
if (type == null) {
type = "default";
}
result = getNodeProcessor("@" + type, null, 0);
/* DD: Jonathan's non-BC version and IMHO otherwise wrong version:
if (type != null) {
result = getNodeProcessor("@" + type, null, 0);
}
if (result == null) {
result = getNodeProcessor("@default", null, 0);
}
*/
}
return result;
}
private TemplateModel getNodeProcessor(final String nodeName, final String nsURI, int startIndex)
throws TemplateException {
TemplateModel result = null;
int i;
for (i = startIndex; i < nodeNamespaces.size(); i++) {
Namespace ns = null;
try {
ns = (Namespace) nodeNamespaces.get(i);
} catch (ClassCastException cce) {
throw new _MiscTemplateException(this,
"A \"using\" clause should contain a sequence of namespaces or strings that indicate the "
+ "location of importable macro libraries.");
}
result = getNodeProcessor(ns, nodeName, nsURI);
if (result != null)
break;
}
if (result != null) {
this.nodeNamespaceIndex = i + 1;
this.currentNodeName = nodeName;
this.currentNodeNS = nsURI;
}
return result;
}
private TemplateModel getNodeProcessor(Namespace ns, String localName, String nsURI) throws TemplateException {
TemplateModel result = null;
if (nsURI == null) {
result = ns.get(localName);
if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
} else {
Template template = ns.getTemplate();
String prefix = template.getPrefixForNamespace(nsURI);
if (prefix == null) {
// The other template cannot handle this node
// since it has no prefix registered for the namespace
return null;
}
if (prefix.length() > 0) {
result = ns.get(prefix + ":" + localName);
if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
} else {
if (nsURI.length() == 0) {
result = ns.get(Template.NO_NS_PREFIX + ":" + localName);
if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
}
if (nsURI.equals(template.getDefaultNS())) {
result = ns.get(Template.DEFAULT_NAMESPACE_PREFIX + ":" + localName);
if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
}
if (result == null) {
result = ns.get(localName);
if (!(result instanceof BoundCallable) && !(result instanceof TemplateTransformModel)) {
result = null;
}
}
}
}
return result;
}
/**
* Emulates <code>include</code> directive, except that <code>name</code> must be tempate
* root relative.
*
* <p>It's the same as <code>include(getTemplateForInclusion(name, encoding, parse))</code>.
* But, you may want to separately call these two methods, so you can determine the source of
* exceptions more precisely, and thus achieve more intelligent error handling.
*
* @see #getTemplateForInclusion(String name, String encoding, boolean parse)
* @see #include(Template includedTemplate)
*/
public void include(String name, String encoding, boolean parse)
throws IOException, TemplateException {
include(getTemplateForInclusion(name, encoding, parse));
}
/**
* Same as {@link #getTemplateForInclusion(String, String, boolean, boolean)} with {@code false}
* {@code ignoreMissign} argument.
*/
public Template getTemplateForInclusion(String name, String encoding, boolean parse)
throws IOException {
return getTemplateForInclusion(name, encoding, parse, false);
}
/**
* Gets a template for inclusion; used for implementing {@link #include(Template includedTemplate)}. The advantage
* over simply using <code>config.getTemplate(...)</code> is that it chooses the default encoding exactly as the
* <code>include</code> directive does, although that encoding selection mechanism is a historical baggage and
* considered to be harmful.
*
* @param name
* the name of the template, relatively to the template root directory (not the to the directory of the
* currently executing template file). (Note that you can use
* {@link freemarker.cache.TemplateCache#getFullTemplatePath} to convert paths to template root relative
* paths.) For more details see the identical parameter of
* {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)}
*
* @param encoding
* the charset of the obtained template. If {@code null}, the encoding of the top template that is
* currently being processed in this {@link Environment} is used, which can lead to odd situations, so
* using {@code null} is not recommended. In most applications, the value of
* {@link Configuration#getEncoding(Locale)} (or {@link Configuration#getDefaultEncoding()}) should be
* used here.
*
* @param parseAsFTL
* See identical parameter of {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)}
*
* @param ignoreMissing
* See identical parameter of {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)}
*
* @return Same as {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)}
* @throws IOException
* Same as exceptions thrown by
* {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)}
*
* @since 2.3.21
*/
public Template getTemplateForInclusion(String name, String encoding, boolean parseAsFTL, boolean ignoreMissing)
throws IOException {
final Template inheritedTemplate = getTemplate();
if (encoding == null) {
// This branch shouldn't exist, as it doesn't make much sense to inherit encoding. But we have to keep BC.
encoding = inheritedTemplate.getEncoding();
if (encoding == null) {
encoding = configuration.getEncoding(this.getLocale());
}
}
Object customLookupCondition = inheritedTemplate.getCustomLookupCondition();
return configuration.getTemplate(
name, getLocale(), customLookupCondition,
encoding, parseAsFTL,
ignoreMissing);
}
/**
* Processes a Template in the context of this <code>Environment</code>, including its
* output in the <code>Environment</code>'s Writer.
*
* @param includedTemplate the template to process. Note that it does <em>not</em> need
* to be a template returned by
* {@link #getTemplateForInclusion(String name, String encoding, boolean parse)}.
*/
public void include(Template includedTemplate)
throws TemplateException, IOException {
final Template prevTemplate;
final boolean parentReplacementOn = isBeforeIcI2322();
prevTemplate = getTemplate();
if (parentReplacementOn) {
setParent(includedTemplate);
} else {
legacyParent = includedTemplate;
}
final Template prevCurrentTemplate = currentTemplate;
try {
currentTemplate = includedTemplate;
predefineCallables(includedTemplate);
try {
visit(includedTemplate.getRootTreeNode());
} finally {
if (parentReplacementOn) {
setParent(prevTemplate);
} else {
legacyParent = prevTemplate;
}
}
} finally {
currentTemplate = prevCurrentTemplate;
}
}
/**
* Emulates <code>import</code> directive, except that <code>name</code> must be tempate
* root relative.
*
* <p>It's the same as <code>importLib(getTemplateForImporting(name), namespace)</code>.
* But, you may want to separately call these two methods, so you can determine the source of
* exceptions more precisely, and thus achieve more intelligent error handling.
*
* @see #getTemplateForImporting(String name)
* @see #importLib(Template includedTemplate, String namespace)
*/
public Namespace importLib(String name, String namespace)
throws IOException, TemplateException {
return importLib(getTemplateForImporting(name), namespace);
}
/**
* Gets a template for importing; used with
* {@link #importLib(Template importedTemplate, String namespace)}. The advantage
* over simply using <code>config.getTemplate(...)</code> is that it chooses the encoding
* as the <code>import</code> directive does.
*
* @param name the name of the template, relatively to the template root directory
* (not the to the directory of the currently executing template file!).
* (Note that you can use {@link freemarker.cache.TemplateCache#getFullTemplatePath}
* to convert paths to template root relative paths.)
*/
public Template getTemplateForImporting(String name) throws IOException {
return getTemplateForInclusion(name, null, true);
}
/**
* Emulates <code>import</code> directive.
*
* @param loadedTemplate the template to import. Note that it does <em>not</em> need
* to be a template returned by {@link #getTemplateForImporting(String name)}.
*/
public Namespace importLib(Template loadedTemplate, String namespace)
throws IOException, TemplateException {
if (loadedLibs == null) {
loadedLibs = new HashMap();
}
String templateName = loadedTemplate.getName();
Namespace existingNamespace = (Namespace) loadedLibs.get(templateName);
if (existingNamespace != null) {
if (namespace != null) {
setVariable(namespace, existingNamespace);
}
} else {
Namespace newNamespace = new Namespace(loadedTemplate);
if (namespace != null) {
currentNamespace.put(namespace, newNamespace);
if (currentNamespace == mainNamespace) {
globalNamespace.put(namespace, newNamespace);
}
}
Namespace prevNamespace = this.currentNamespace;
this.currentNamespace = newNamespace;
loadedLibs.put(templateName, currentNamespace);
Writer prevOut = out;
this.out = NullWriter.INSTANCE;
try {
include(loadedTemplate);
} finally {
this.out = prevOut;
this.currentNamespace = prevNamespace;
}
}
return (Namespace) loadedLibs.get(templateName);
}
/**
* Resolves a reference to a template (like the one used in {@code #include} or {@code #import}), assuming a base
* name. This gives a full (that is, absolute), even if non-normalized template name, that could be used for
* {@link Configuration#getTemplate(String)}. This is mostly used when a template refers to another template.
*
* @param baseName
* The name to which relative {@code targetName}-s are relative to. Maybe {@code null}, which usually
* means that the base is the root "directory". Assuming {@link TemplateNameFormat#DEFAULT_2_3_0} or
* {@link TemplateNameFormat#DEFAULT_2_4_0}, the rules are as follows. If you want to specify a base
* directory here, it must end with {@code "/"}. If it doesn't end with {@code "/"}, it's parent
* directory will be used as the base path. Might starts with a scheme part (like {@code "foo://"}, or
* with {@link TemplateNameFormat#DEFAULT_2_4_0} even just {@code "foo:"}).
* @param targetName
* The name of the template, which is either a relative or absolute name. Assuming
* {@link TemplateNameFormat#DEFAULT_2_3_0} or {@link TemplateNameFormat#DEFAULT_2_4_0}, the rules are as
* follows. If it starts with {@code "/"} or contains a scheme part separator ({@code "://"}, also, with
* {@link TemplateNameFormat#DEFAULT_2_4_0} a {@code ":"} with no {@code "/"} anywhere before it) then
* it's an absolute name, otherwise it's a relative path. Relative paths are interpreted relatively to
* the {@code baseName}. Absolute names are simply returned as is, ignoring the {@code baseName}, except,
* when the {@code baseName} has scheme part while the {@code targetName} doesn't have, then the schema
* of the {@code baseName} is prepended to the {@code targetName}.
*
* @since 2.3.22
*/
public String toFullTemplateName(String baseName, String targetName)
throws MalformedTemplateNameException {
if (isClassicCompatible()) {
// Early FM only had absolute names.
return targetName;
}
return _CacheAPI.toAbsoluteName(configuration.getTemplateNameFormat(), baseName, targetName);
}
String renderElementToString(TemplateElement te) throws IOException, TemplateException {
Writer prevOut = out;
try {
StringWriter sw = new StringWriter();
this.out = sw;
visit(te);
return sw.toString();
} finally {
this.out = prevOut;
}
}
/**
* Used for creating the callables that are defined by #macro/#function elements that weren't executed yet.
*/
void predefineCallables(Template template) {
final Map<String, UnboundCallable> unboundCallables
= _CoreAPI.getUnboundCallables(template.getUnboundTemplate());
if (unboundCallables != null) {
for (UnboundCallable unboundCallable : unboundCallables.values()) {
visitCallableDefinition(unboundCallable);
}
}
}
/**
* @return the namespace URI registered for this prefix, or null.
* This is based on the mappings registered in the current namespace.
*/
public String getNamespaceForPrefix(String prefix) {
return currentNamespace.getTemplate().getNamespaceForPrefix(prefix);
}
public String getPrefixForNamespace(String nsURI) {
return currentNamespace.getTemplate().getPrefixForNamespace(nsURI);
}
/**
* @return the default node namespace for the current FTL namespace
*/
public String getDefaultNS() {
return currentNamespace.getTemplate().getDefaultNS();
}
/**
* A hook that Jython uses.
*/
public Object __getitem__(String key) throws TemplateModelException {
return BeansWrapper.getDefaultInstance().unwrap(getVariable(key));
}
/**
* A hook that Jython uses.
*/
public void __setitem__(String key, Object o) throws TemplateException {
setGlobalVariable(key, getObjectWrapper().wrap(o));
}
private IdentityHashMap<Object, Object> customStateVariables;
/**
* Returns the value of a custom state variable, or {@code null} if it's missing; see
* {@link #setCustomState(Object, Object)} for more.
*
* @since 2.3.24
*/
public Object getCustomState(Object identityKey) {
if (customStateVariables == null) {
return null;
}
return customStateVariables.get(identityKey);
}
/**
* Sets the value of a custom state variable. Custom state variables meant to be used by
* {@link TemplateNumberFormatFactory}-es, {@link TemplateDateFormatFactory}-es, and similar user-implementable,
* pluggable objects, which want to maintain an {@link Environment}-scoped state (such as a cache).
*
* @param identityKey
* The key that identifies the variable, by its object identity (not by {@link Object#equals(Object)}).
* This should be something like a {@code private static final Object CUSTOM_STATE_KEY = new Object();}
* in the class that needs this state variable.
* @param value
* The value of the variable. Can be anything, even {@code null}.
*
* @return The previous value of the variable, or {@code null} if the variable didn't exist.
*
* @since 2.3.24
*/
public Object setCustomState(Object identityKey, Object value) {
IdentityHashMap<Object, Object> customStateVariables = this.customStateVariables;
if (customStateVariables == null) {
customStateVariables = new IdentityHashMap<Object, Object>();
this.customStateVariables = customStateVariables;
}
return customStateVariables.put(identityKey, value);
}
final class NestedElementTemplateDirectiveBody implements TemplateDirectiveBody {
private final TemplateElement element;
private NestedElementTemplateDirectiveBody(TemplateElement element) {
this.element = element;
}
public void render(Writer newOut) throws TemplateException, IOException {
Writer prevOut = out;
out = newOut;
try {
Environment.this.visit(element);
} finally {
out = prevOut;
}
}
public TemplateElement getElement() {
return element;
}
}
public class Namespace extends SimpleHash {
private final Template template;
Namespace() {
this.template = Environment.this.getTemplate();
}
Namespace(Template template) {
this.template = template;
}
/**
* @return the Template object with which this Namespace is associated.
*/
public Template getTemplate() {
return template == null ? Environment.this.getTemplate() : template;
}
@Override
public String toString() {
return StringUtil.jQuote(template.getName()) + super.toString();
}
}
private static final Writer EMPTY_BODY_WRITER = new Writer() {
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
if (len > 0) {
throw new IOException(
"This transform does not allow nested content.");
}
}
@Override
public void flush() {
}
@Override
public void close() {
}
};
private boolean isBeforeIcI2322() {
return configuration.getIncompatibleImprovements().intValue() < _TemplateAPI.VERSION_INT_2_3_22;
}
private boolean isIcI2324OrLater() {
return configuration.getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_3_24;
}
/**
* See {@link #setFastInvalidReferenceExceptions(boolean)}.
*/
boolean getFastInvalidReferenceExceptions() {
return fastInvalidReferenceExceptions;
}
/**
* Sets if for invalid references {@link InvalidReferenceException#FAST_INSTANCE} should be thrown, or a new
* {@link InvalidReferenceException}. The "fast" instance is used if we know that the error will be handled
* so that its message will not be logged or shown anywhere.
*/
boolean setFastInvalidReferenceExceptions(boolean b) {
boolean res = fastInvalidReferenceExceptions;
fastInvalidReferenceExceptions = b;
return res;
}
}