All Downloads are FREE. Search and download functionalities are using the official Maven repository.

freemarker.core.Environment Maven / Gradle / Ivy

Go to download

Google App Engine compliant variation of FreeMarker. FreeMarker is a "template engine"; a generic tool to generate text output based on templates.

There is a newer version: 2.3.33
Show newest version
/*
 * 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.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
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.ext.beans.BeansWrapper;
import freemarker.log.Logger;
import freemarker.template.Configuration;
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.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
import freemarker.template.TemplateTransformModel;
import freemarker.template.TransformControl;
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 Template.process() method, a new instance
 * of this object is created, and then discarded when process() 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.
 *
 * 

Data models that need to access the Environment * object that represents the template processing on the current thread can use * the {@link #getCurrentEnvironment()} method. * *

If you need to modify or read this object before or after the process * 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 LOGGER = Logger.getLogger("freemarker.runtime"); private static final Logger ATTEMPT_LOGGER = Logger.getLogger("freemarker.runtime.attempt"); private static final Map JAVA_NUMBER_FORMATS = new HashMap(); // 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 TemplateHashModel rootDataModel; private final ArrayList/**/ instructionStack = new ArrayList(); private final ArrayList recoveredErrorStack = new ArrayList(); private NumberFormat cachedNumberFormat; private Map cachedNumberFormats; /** * 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}. * *

The index of the array is calculated from what kind of formatter we want * (see {@link #getCachedTemplateDateFormatIndex(int, boolean, boolean)}):
* Zoned input: 0: U, 1: T, 2: D, 3: DT
* Zoneless input: 4: U, 5: T, 6: D, 7: DT
* Sys def TZ + Zoned input: 8: U, 9: T, 10: D, 11: DT
* Sys def TZ + Zoneless input: 12: U, 13: T, 14: D, 15: DT * *

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[] cachedTemplateDateFormats; private static final int CACHED_TDFS_ZONELESS_INPUT_OFFS = 4; private static final int CACHED_TDFS_DEF_SYS_TZ_OFFS = CACHED_TDFS_ZONELESS_INPUT_OFFS * 2; private static final int CACHED_TDFS_LENGTH = CACHED_TDFS_DEF_SYS_TZ_OFFS * 2; private static final int CACHED_TDFS_SQL_D_T_TZ_OFFS = CACHED_TDFS_DEF_SYS_TZ_OFFS; private XSTemplateDateFormatFactory cachedXSTemplateDateFormatFactory; private XSTemplateDateFormatFactory cachedSQLDTXSTemplateDateFormatFactory; private ISOTemplateDateFormatFactory cachedISOTemplateDateFormatFactory; private ISOTemplateDateFormatFactory cachedSQLDTISOTemplateDateFormatFactory; private JavaTemplateDateFormatFactory cachedJavaTemplateDateFormatFactory; private JavaTemplateDateFormatFactory cachedSQLDTJavaTemplateDateFormatFactory; /** 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 Macro.Context currentMacroContext; private ArrayList localContextStack; private Namespace mainNamespace, currentNamespace, globalNamespace; private HashMap 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. 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(); } public Environment(Template template, final TemplateHashModel rootDataModel, Writer out) { super(template); this.globalNamespace = new Namespace(null); this.currentNamespace = mainNamespace = new Namespace(template); this.out = out; this.rootDataModel = rootDataModel; importMacros(template); } /** * Retrieves the currently processed template. */ public Template getTemplate() { return (Template)getParent(); } /** * Deletes cached values that meant to be valid only during a single * template execution. */ private void clearCachedValues() { cachedNumberFormats = null; cachedNumberFormat = null; cachedTemplateDateFormats = null; cachedXSTemplateDateFormatFactory = cachedSQLDTXSTemplateDateFormatFactory = null; cachedISOTemplateDateFormatFactory = cachedSQLDTISOTemplateDateFormatFactory = null; cachedJavaTemplateDateFormatFactory = cachedSQLDTJavaTemplateDateFormatFactory = 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. These 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 TemplateDirectiveBody() { public void render(Writer newOut) throws TemplateException, IOException { Writer prevOut = out; out = newOut; try { Environment.this.visit(element); } finally { out = prevOut; } } }; } 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 #attempt block (but before #recover). This can be useful for * {@link TemplateExceptionHandler}-s, as then they may don't want to print the error to the output, as * #attempt will roll it back anyway. * * @since 2.3.20 */ public boolean isInAttemptBlock() { return inAttemptBlock; } void visit(BodyInstruction.Context bctxt) throws TemplateException, IOException { Macro.Context invokingMacroContext = getCurrentMacroContext(); ArrayList prevLocalContextStack = localContextStack; TemplateElement body = invokingMacroContext.body; if (body != null) { this.currentMacroContext = invokingMacroContext.prevMacroContext; currentNamespace = invokingMacroContext.bodyNamespace; Configurable prevParent = getParent(); setParent(currentNamespace.getTemplate()); this.localContextStack = invokingMacroContext.prevLocalContextStack; if (invokingMacroContext.bodyParameterNames != null) { pushLocalContext(bctxt); } try { visit(body); } finally { if (invokingMacroContext.bodyParameterNames != null) { popLocalContext(); } this.currentMacroContext = invokingMacroContext; currentNamespace = getMacroNamespace(invokingMacroContext.getMacro()); setParent(prevParent); this.localContextStack = prevLocalContextStack; } } } /** * "visit" an IteratorBlock */ void visitIteratorBlock(IteratorBlock.Context ictxt) throws TemplateException, IOException { pushLocalContext(ictxt); try { ictxt.runLoop(this); } catch (BreakInstruction.Break br) { } catch (TemplateException te) { handleTemplateException(te); } finally { popLocalContext(); } } /** * "Visit" A TemplateNodeModel */ void visit(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 Macro) { visit((Macro) 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 Macro) { visit((Macro) macroOrTransform, null, null, null, null); } else if (macroOrTransform instanceof TemplateTransformModel) { visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null); } } /** * "visit" a macro. */ void visit(Macro macro, Map namedArgs, List positionalArgs, List bodyParameterNames, TemplateElement nestedBlock) throws TemplateException, IOException { if (macro == Macro.DO_NOTHING_MACRO) { return; } pushElement(macro); try { Macro.Context previousMacroContext = currentMacroContext; Macro.Context mc = macro.new Context(this, nestedBlock, bodyParameterNames); String catchAll = macro.getCatchAll(); TemplateModel unknownVars = null; if (namedArgs != null) { if (catchAll != null) unknownVars = new SimpleHash(); for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); String varName = (String) entry.getKey(); boolean hasVar = macro.hasArgNamed(varName); if (hasVar || catchAll != null) { Expression arg = (Expression) entry.getValue(); TemplateModel value = arg.eval(this); if (hasVar) { mc.setLocalVar(varName, value); } else { ((SimpleHash)unknownVars).put(varName, value); } } else { throw new _MiscTemplateException(this, new Object[] { "Macro ", new _DelayedJQuote(macro.getName()), " has no such argument: ", varName }); } } } else if (positionalArgs != null) { if (catchAll != null) unknownVars = new SimpleSequence(); String[] argumentNames = macro.getArgumentNamesInternal(); int size = positionalArgs.size(); if (argumentNames.length < size && catchAll == null) { throw new _MiscTemplateException(this, new Object[] { "Macro " + StringUtil.jQuote(macro.getName()) + " only accepts " + argumentNames.length + " parameters." }); } for (int i = 0; i < size; i++) { Expression argExp = (Expression) positionalArgs.get(i); TemplateModel argModel = argExp.eval(this); try { if (i < argumentNames.length) { String argName = argumentNames[i]; mc.setLocalVar(argName, argModel); } else { ((SimpleSequence)unknownVars).add(argModel); } } catch (RuntimeException re) { throw new _MiscTemplateException(re, this); } } } if (catchAll != null) { mc.setLocalVar(catchAll, unknownVars); } ArrayList prevLocalContextStack = localContextStack; localContextStack = null; Namespace prevNamespace = currentNamespace; Configurable prevParent = getParent(); currentNamespace = (Namespace) macroToNamespaceLookup.get(macro); currentMacroContext = mc; try { mc.runMacro(this); } catch (ReturnInstruction.Return re) { } catch (TemplateException te) { handleTemplateException(te); } finally { currentMacroContext = previousMacroContext; localContextStack = prevLocalContextStack; currentNamespace = prevNamespace; setParent(prevParent); } } finally { popElement(); } } void visitMacroDef(Macro macro) { macroToNamespaceLookup.put(macro, currentNamespace); currentNamespace.put(macro.getName(), macro); } Namespace getMacroNamespace(Macro macro) { return (Namespace) macroToNamespaceLookup.get(macro); } void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces) throws TemplateException, IOException { 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; isetting 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. */ 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 null 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; } String formatNumber(Number number) { if(cachedNumberFormat == null) { cachedNumberFormat = getNumberFormatObject(getNumberFormat()); } return cachedNumberFormat.format(number); } public void setNumberFormat(String formatName) { super.setNumberFormat(formatName); cachedNumberFormat = null; } public void setTimeFormat(String timeFormat) { String prevTimeFormat = getTimeFormat(); super.setTimeFormat(timeFormat); if (!timeFormat.equals(prevTimeFormat)) { if (cachedTemplateDateFormats != null) { for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) { cachedTemplateDateFormats[i + TemplateDateModel.TIME] = null; } } } } public void setDateFormat(String dateFormat) { String prevDateFormat = getDateFormat(); super.setDateFormat(dateFormat); if (!dateFormat.equals(prevDateFormat)) { if (cachedTemplateDateFormats != null) { for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) { cachedTemplateDateFormats[i + TemplateDateModel.DATE] = null; } } } } public void setDateTimeFormat(String dateTimeFormat) { String prevDateTimeFormat = getDateTimeFormat(); super.setDateTimeFormat(dateTimeFormat); if (!dateTimeFormat.equals(prevDateTimeFormat)) { if (cachedTemplateDateFormats != null) { for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) { cachedTemplateDateFormats[i + TemplateDateModel.DATETIME] = null; } } } } public Configuration getConfiguration() { return getTemplate().getConfiguration(); } TemplateModel getLastReturnValue() { return lastReturnValue; } void setLastReturnValue(TemplateModel lastReturnValue) { this.lastReturnValue = lastReturnValue; } void clearLastReturnValue() { this.lastReturnValue = null; } NumberFormat getNumberFormatObject(String pattern) { if(cachedNumberFormats == null) { cachedNumberFormats = new HashMap(); } NumberFormat format = (NumberFormat) cachedNumberFormats.get(pattern); if(format != null) { return format; } // Get format from global format cache synchronized(JAVA_NUMBER_FORMATS) { Locale locale = getLocale(); NumberFormatKey fk = new NumberFormatKey(pattern, locale); format = (NumberFormat)JAVA_NUMBER_FORMATS.get(fk); if(format == null) { // Add format to global format cache. Note this is // globally done once per locale per pattern. if("number".equals(pattern)) { format = NumberFormat.getNumberInstance(locale); } else if("currency".equals(pattern)) { format = NumberFormat.getCurrencyInstance(locale); } else if("percent".equals(pattern)) { format = NumberFormat.getPercentInstance(locale); } else if ("computer".equals(pattern)) { format = getCNumberFormat(); } else { format = new DecimalFormat(pattern, new DecimalFormatSymbols(getLocale())); } JAVA_NUMBER_FORMATS.put(fk, format); } } // Clone it and store the clone in the local cache format = (NumberFormat)format.clone(); cachedNumberFormats.put(pattern, format); return format; } 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 formatDescriptor, Expression tdmSourceExpr) throws TemplateModelException { Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr); boolean isSQLDateOrTime = isSQLDateOrTimeClass(date.getClass()); try { return getTemplateDateFormat( tdm.getDateType(), isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), formatDescriptor, 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/**/ 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 = getCachedTemplateDateFormatIndex(dateType, isSQLDateOrTime, useSQLDTTZ); TemplateDateFormat[] cachedTemplateDateFormats = this.cachedTemplateDateFormats; if (cachedTemplateDateFormats == null) { cachedTemplateDateFormats = new TemplateDateFormat[CACHED_TDFS_LENGTH]; this.cachedTemplateDateFormats = cachedTemplateDateFormats; } TemplateDateFormat f = cachedTemplateDateFormats[cacheIdx]; if (f == null) { final String settingName; final String settingValue; switch (dateType) { case TemplateDateModel.TIME: settingName = Configuration.TIME_FORMAT_KEY; settingValue = getTimeFormat(); break; case TemplateDateModel.DATE: settingName = Configuration.DATE_FORMAT_KEY; settingValue = getDateFormat(); break; case TemplateDateModel.DATETIME: settingName = Configuration.DATETIME_FORMAT_KEY; settingValue = getDateTimeFormat(); break; default: throw new _TemplateModelException(new Object[] { "Invalid date type enum: ", new Integer(dateType) }); } // switch f = getTemplateDateFormat( dateType, isSQLDateOrTime, useSQLDTTZ, settingValue, settingName); cachedTemplateDateFormats[cacheIdx] = f; } return f; } /** * @param dateType {@link TemplateDateModel#UNKNOWN} is accepted or not depending on the {@code formatDescriptor} * 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 formatDescriptor Like "iso m" or "dd.MM.yyyy HH:mm" */ TemplateDateFormat getTemplateDateFormat( int dateType, Class/**/ dateClass, String formatDescriptor, Expression dateSourceExpr) throws TemplateModelException { try { boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); return getTemplateDateFormat( dateType, isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), formatDescriptor, null); } catch (UnknownDateTypeFormattingUnsupportedException e) { throw MessageUtil.newCantFormatUnknownTypeDateException(dateSourceExpr, e); } } private TemplateDateFormat getTemplateDateFormat( int dateType, boolean zonelessInput, boolean useSQLDTTZ, String formatDescriptor, String sourceCfgSetting) throws TemplateModelException, UnknownDateTypeFormattingUnsupportedException { final int formatDescriptionLen = formatDescriptor.length(); final TimeZone timeZone = useSQLDTTZ ? getSQLDateAndTimeTimeZone() : getTimeZone(); // As of Java 8, 'x' and 'i' (in lower case) are illegal date format letters, so this is backward-compatible. TemplateDateFormatFactory templateDateFormatFactory; if (formatDescriptionLen > 1 && formatDescriptor.charAt(0) == 'x' && formatDescriptor.charAt(1) == 's') { templateDateFormatFactory = useSQLDTTZ ? cachedSQLDTXSTemplateDateFormatFactory : cachedXSTemplateDateFormatFactory; if (templateDateFormatFactory == null) { templateDateFormatFactory = new XSTemplateDateFormatFactory(timeZone); if (useSQLDTTZ) { cachedSQLDTXSTemplateDateFormatFactory = (XSTemplateDateFormatFactory) templateDateFormatFactory; } else { cachedXSTemplateDateFormatFactory = (XSTemplateDateFormatFactory) templateDateFormatFactory; } } } else if (formatDescriptionLen > 2 && formatDescriptor.charAt(0) == 'i' && formatDescriptor.charAt(1) == 's' && formatDescriptor.charAt(2) == 'o') { templateDateFormatFactory = useSQLDTTZ ? cachedSQLDTISOTemplateDateFormatFactory : cachedISOTemplateDateFormatFactory; if (templateDateFormatFactory == null) { templateDateFormatFactory = new ISOTemplateDateFormatFactory(timeZone); if (useSQLDTTZ) { cachedSQLDTISOTemplateDateFormatFactory = (ISOTemplateDateFormatFactory) templateDateFormatFactory; } else { cachedISOTemplateDateFormatFactory = (ISOTemplateDateFormatFactory) templateDateFormatFactory; } } } else { templateDateFormatFactory = useSQLDTTZ ? cachedSQLDTJavaTemplateDateFormatFactory : cachedJavaTemplateDateFormatFactory; if (templateDateFormatFactory == null) { templateDateFormatFactory = new JavaTemplateDateFormatFactory(timeZone, getLocale()); if (useSQLDTTZ) { cachedSQLDTJavaTemplateDateFormatFactory = (JavaTemplateDateFormatFactory) templateDateFormatFactory; } else { cachedJavaTemplateDateFormatFactory = (JavaTemplateDateFormatFactory) templateDateFormatFactory; } } } try { return templateDateFormatFactory.get(dateType, zonelessInput, formatDescriptor); } catch (ParseException e) { throw new _TemplateModelException(e.getCause(), new Object[] { (sourceCfgSetting == null ? (Object) "Malformed date/time format descriptor: " : new Object[] { "The value of the \"", sourceCfgSetting, "\" FreeMarker configuration setting is a malformed date/time format descriptor: " }), new _DelayedJQuote(formatDescriptor), ". 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 getCachedTemplateDateFormatIndex(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; } /** * Returns the {@link NumberFormat} used for the c built-in. * This is always US English "0.################", 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; } 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 = getConfiguration().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. * This is the correspondent to an FTL top-level variable reading expression. * That is, it tries to find the the variable in this order: *

    *
  1. An loop variable (if we're in a loop or user defined directive body) such as foo_has_next *
  2. A local variable (if we're in a macro) *
  3. A variable defined in the current namespace (say, via <#assign ...>) *
  4. A variable defined globally (say, via <#global ....>) *
  5. Variable in the data model: *
      *
    1. A variable in the root hash that was exposed to this rendering environment in the Template.process(...) call *
    2. A shared variable set in the configuration via a call to Configuration.setSharedVariable(...) *
    *
  6. *
*/ 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 .globals.name. * 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 = getConfiguration().getSharedVariable(name); } return result; } /** * Sets a variable that is visible globally. * This is correspondent to FTL <#global name=model>. * 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 <#assign name=model>. * 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 <#local name=model>. * @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 = getConfiguration().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) { LOGGER.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) { StringBuffer sb = new StringBuffer(); appendInstructionStackItem(stackEl, sb); return sb.toString(); } static void appendInstructionStackItem(TemplateElement stackEl, StringBuffer 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.getTemplate(), stackEl.beginLine, stackEl.beginColumn)); } sb.append("]"); } static private Macro getEnclosingMacro(TemplateElement stackEl) { while (stackEl != null) { if (stackEl instanceof Macro) return (Macro) stackEl; stackEl = stackEl.getParent(); } 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 import 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 name-space. * This is correspondent of FTL .main hash. */ public Namespace getMainNamespace() { return mainNamespace; } /** * Returns the main name-space. * This is correspondent of FTL .namespace hash. */ public Namespace getCurrentNamespace() { return currentNamespace; } /** * Returns a fictitious name-space that contains the globally visible variables * that were created in the template, but not the variables of the data-model. * There is no such thing in FTL; this strange method was added because of the * JSP taglib support, since this imaginary name-space contains the page-scope * attributes. */ public Namespace getGlobalNamespace() { return globalNamespace; } 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 = getConfiguration().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 .globals hash. * That is, you see the variables created with * <#global ...>, 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 = getConfiguration().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. .current_node */ 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; i0) { result = ns.get(prefix + ":" + localName); if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) { result = null; } } else { if (nsURI.length() == 0) { result = ns.get(Template.NO_NS_PREFIX + ":" + localName); if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) { result = null; } } if (nsURI.equals(template.getDefaultNS())) { result = ns.get(Template.DEFAULT_NAMESPACE_PREFIX + ":" + localName); if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) { result = null; } } if (result == null) { result = ns.get(localName); if (!(result instanceof Macro) && !(result instanceof TemplateTransformModel)) { result = null; } } } } return result; } /** * Emulates include directive, except that name must be tempate * root relative. * *

It's the same as include(getTemplateForInclusion(name, encoding, parse)). * 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 with {@link #include(Template includedTemplate)}. * The advantage over simply using config.getTemplate(...) is that it chooses * the default encoding exactly as the include 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 parse 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 parse, boolean ignoreMissing) throws IOException { if (encoding == null) { // This branch shouldn't exist... but we have to keep BC. encoding = getTemplate().getEncoding(); } if (encoding == null) { encoding = getConfiguration().getEncoding(this.getLocale()); } return getConfiguration().getTemplate(name, getLocale(), encoding, parse, ignoreMissing); } /** * Processes a Template in the context of this Environment, including its * output in the Environment's Writer. * * @param includedTemplate the template to process. Note that it does not need * to be a template returned by * {@link #getTemplateForInclusion(String name, String encoding, boolean parse)}. */ public void include(Template includedTemplate) throws TemplateException, IOException { Template prevTemplate = getTemplate(); setParent(includedTemplate); importMacros(includedTemplate); try { visit(includedTemplate.getRootTreeNode()); } finally { setParent(prevTemplate); } } /** * Emulates import directive, except that name must be tempate * root relative. * *

It's the same as importLib(getTemplateForImporting(name), namespace). * 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 config.getTemplate(...) is that it chooses the encoding * as the import 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 import directive. * * @param loadedTemplate the template to import. Note that it does not 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); } 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; } } void importMacros(Template template) { for (Iterator it = template.getMacros().values().iterator(); it.hasNext();) { visitMacroDef((Macro) it.next()); } } /** * @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 static final class NumberFormatKey { private final String pattern; private final Locale locale; NumberFormatKey(String pattern, Locale locale) { this.pattern = pattern; this.locale = locale; } public boolean equals(Object o) { if(o instanceof NumberFormatKey) { NumberFormatKey fk = (NumberFormatKey)o; return fk.pattern.equals(pattern) && fk.locale.equals(locale); } return false; } public int hashCode() { return pattern.hashCode() ^ locale.hashCode(); } } public class Namespace extends SimpleHash { private 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; } } private static final Writer EMPTY_BODY_WRITER = new Writer() { public void write(char[] cbuf, int off, int len) throws IOException { if (len > 0) { throw new IOException( "This transform does not allow nested content."); } } public void flush() { } public void close() { } }; /** * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy