| /* |
| * Copyright (c) 2003 The Visigoth Software Society. All rights |
| * reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in |
| * the documentation and/or other materials provided with the |
| * distribution. |
| * |
| * 3. The end-user documentation included with the redistribution, if |
| * any, must include the following acknowledgement: |
| * "This product includes software developed by the |
| * Visigoth Software Society (http://www.visigoths.org/)." |
| * Alternately, this acknowledgement may appear in the software itself, |
| * if and wherever such third-party acknowledgements normally appear. |
| * |
| * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the |
| * project contributors may be used to endorse or promote products derived |
| * from this software without prior written permission. For written |
| * permission, please contact visigoths@visigoths.org. |
| * |
| * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth" |
| * nor may "FreeMarker" or "Visigoth" appear in their names |
| * without prior written permission of the Visigoth Software Society. |
| * |
| * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR |
| * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
| * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT |
| * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
| * SUCH DAMAGE. |
| * ==================================================================== |
| * |
| * This software consists of voluntary contributions made by many |
| * individuals on behalf of the Visigoth Software Society. For more |
| * information on the Visigoth Software Society, please see |
| * http://www.visigoths.org/ |
| */ |
| |
| 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.SimpleDateFormat; |
| 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.StringTokenizer; |
| 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 <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)} |
| * |
| * @author <a href="mailto:jon@revusky.com">Jonathan Revusky</a> |
| * @author Attila Szegedi |
| */ |
| public final class Environment extends Configurable { |
| |
| static final String STACK_SECTION_SEPARATOR = "----------"; |
| |
| private static final ThreadLocal threadEnv = new ThreadLocal(); |
| |
| private static final Logger logger = Logger.getLogger("freemarker.runtime"); |
| private static final Logger attemptLogger = Logger.getLogger("freemarker.runtime.attempt"); |
| |
| private static final Map localizedNumberFormats = new HashMap(); |
| private static final Map localizedDateFormats = 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/*<TemplateElement>*/ instructionStack = new ArrayList(); |
| private final ArrayList recoveredErrorStack = new ArrayList(); |
| |
| private NumberFormat numberFormat; |
| private Map numberFormats; |
| |
| private DateFormat timeFormat, dateFormat, dateTimeFormat; |
| private Map[] dateFormats; |
| private NumberFormat cNumberFormat; |
| |
| /** |
| * Used by the "iso_" built-ins to accelerate formatting. |
| * @see #getISOBuiltInCalendar() |
| */ |
| private DateToISO8601CalendarFactory isoBuiltInCalendarFactory; |
| |
| private Collator collator; |
| |
| 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 urlEscapingCharsetCached; |
| |
| 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() { |
| numberFormats = null; |
| numberFormat = null; |
| dateFormats = null; |
| collator = null; |
| cachedURLEscapingCharset = null; |
| urlEscapingCharsetCached = false; |
| } |
| |
| /** |
| * Processes the template to which this environment belongs. |
| */ |
| 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 (attemptLogger.isDebugEnabled()) { |
| attemptLogger.debug("Error in attempt block " + |
| attemptBlock.getStartLocationQuoted(), thrownException); |
| } |
| try { |
| recoveredErrorStack.add(thrownException); |
| visit(recoveryBlock); |
| } finally { |
| recoveredErrorStack.remove(recoveredErrorStack.size() -1); |
| } |
| } else { |
| out.write(sw.toString()); |
| } |
| } |
| |
| String getCurrentRecoveredErrorMessage() throws TemplateException { |
| if(recoveredErrorStack.isEmpty()) { |
| throw new _MiscTemplateException(this, ".error is not available outside of a #recover block"); |
| } |
| return ((Throwable) recoveredErrorStack.get(recoveredErrorStack.size() -1)).getMessage(); |
| } |
| |
| /** |
| * Tells if we are inside an <tt>#attempt</tt> block (but before <tt>#recover</tt>). This can be useful for |
| * {@link TemplateExceptionHandler}-s, as then they may don't want to print the error to the output, as |
| * <tt>#attempt</tt> will roll it back anyway. |
| * |
| * @since 2.3.20 |
| */ |
| public boolean isInAttemptBlock() { |
| return inAttemptBlock; |
| } |
| |
| |
| void 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; i<children.size(); i++) { |
| TemplateNodeModel child = (TemplateNodeModel) children.get(i); |
| if (child != null) { |
| visit(child, namespaces); |
| } |
| } |
| } |
| |
| Macro.Context getCurrentMacroContext() { |
| return currentMacroContext; |
| } |
| |
| private void handleTemplateException(TemplateException te) |
| throws TemplateException |
| { |
| // Logic to prevent double-handling of the exception in |
| // nested visit() calls. |
| if(lastThrowable == te) { |
| throw te; |
| } |
| lastThrowable = te; |
| |
| // Log the exception |
| if(logger.isErrorEnabled()) { |
| logger.error("Error executing FreeMarker template", te); |
| } |
| |
| // Stop exception is not passed to the handler, but |
| // explicitly rethrown. |
| if(te instanceof StopException) { |
| throw te; |
| } |
| |
| // Finally, pass the exception to the handler |
| getTemplateExceptionHandler().handleTemplateException(te, this, out); |
| } |
| |
| public void setTemplateExceptionHandler(TemplateExceptionHandler templateExceptionHandler) { |
| super.setTemplateExceptionHandler(templateExceptionHandler); |
| lastThrowable = null; |
| } |
| |
| public void setLocale(Locale locale) { |
| super.setLocale(locale); |
| // Clear local format cache |
| numberFormats = null; |
| numberFormat = null; |
| |
| dateFormats = null; |
| timeFormat = dateFormat = dateTimeFormat = null; |
| |
| collator = null; |
| } |
| |
| public void setTimeZone(TimeZone timeZone) { |
| super.setTimeZone(timeZone); |
| // Clear local date format cache |
| dateFormats = null; |
| timeFormat = dateFormat = dateTimeFormat = null; |
| } |
| |
| public void setURLEscapingCharset(String urlEscapingCharset) { |
| urlEscapingCharsetCached = 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. |
| */ |
| public void setOutputEncoding(String outputEncoding) { |
| urlEscapingCharsetCached = 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 (!urlEscapingCharsetCached) { |
| cachedURLEscapingCharset = getURLEscapingCharset(); |
| if (cachedURLEscapingCharset == null) { |
| cachedURLEscapingCharset = getOutputEncoding(); |
| } |
| urlEscapingCharsetCached = true; |
| } |
| return cachedURLEscapingCharset; |
| } |
| |
| Collator getCollator() { |
| if(collator == null) { |
| collator = Collator.getInstance(getLocale()); |
| } |
| return collator; |
| } |
| |
| /** |
| * 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(numberFormat == null) { |
| numberFormat = getNumberFormatObject(getNumberFormat()); |
| } |
| return numberFormat.format(number); |
| } |
| |
| public void setNumberFormat(String formatName) { |
| super.setNumberFormat(formatName); |
| numberFormat = null; |
| } |
| |
| String formatDate(Date date, int type) throws TemplateModelException { |
| DateFormat df = getDateFormatObject(type); |
| if(df == null) { |
| throw new _TemplateModelException(new _ErrorDescriptionBuilder( |
| "Can't convert the date to string, because it's not known which parts of the date variable are " |
| + "in use.") |
| .tips(MessageUtil.UNKNOWN_DATE_TYPE_ERROR_TIPS)); |
| } |
| return df.format(date); |
| } |
| |
| public void setTimeFormat(String formatName) { |
| super.setTimeFormat(formatName); |
| timeFormat = null; |
| } |
| |
| public void setDateFormat(String formatName) { |
| super.setDateFormat(formatName); |
| dateFormat = null; |
| } |
| |
| public void setDateTimeFormat(String formatName) { |
| super.setDateTimeFormat(formatName); |
| dateTimeFormat = 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(numberFormats == null) { |
| numberFormats = new HashMap(); |
| } |
| |
| NumberFormat format = (NumberFormat) numberFormats.get(pattern); |
| if(format != null) |
| { |
| return format; |
| } |
| |
| // Get format from global format cache |
| synchronized(localizedNumberFormats) |
| { |
| Locale locale = getLocale(); |
| NumberFormatKey fk = new NumberFormatKey(pattern, locale); |
| format = (NumberFormat)localizedNumberFormats.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())); |
| } |
| localizedNumberFormats.put(fk, format); |
| } |
| } |
| |
| // Clone it and store the clone in the local cache |
| format = (NumberFormat)format.clone(); |
| numberFormats.put(pattern, format); |
| return format; |
| } |
| |
| DateFormat getDateFormatObject(int dateType) |
| throws |
| TemplateModelException |
| { |
| switch(dateType) { |
| case TemplateDateModel.UNKNOWN: { |
| return null; |
| } |
| case TemplateDateModel.TIME: { |
| if(timeFormat == null) { |
| timeFormat = getDateFormatObject(dateType, getTimeFormat()); |
| } |
| return timeFormat; |
| } |
| case TemplateDateModel.DATE: { |
| if(dateFormat == null) { |
| dateFormat = getDateFormatObject(dateType, getDateFormat()); |
| } |
| return dateFormat; |
| } |
| case TemplateDateModel.DATETIME: { |
| if(dateTimeFormat == null) { |
| dateTimeFormat = getDateFormatObject(dateType, getDateTimeFormat()); |
| } |
| return dateTimeFormat; |
| } |
| default: { |
| throw new _TemplateModelException(new Object[] { |
| "Unrecognized date type: ", new Integer(dateType) }); |
| } |
| } |
| } |
| |
| DateFormat getDateFormatObject(int dateType, String pattern) |
| throws |
| TemplateModelException |
| { |
| if(dateFormats == null) { |
| dateFormats = new Map[4]; |
| dateFormats[TemplateDateModel.UNKNOWN] = new HashMap(); |
| dateFormats[TemplateDateModel.TIME] = new HashMap(); |
| dateFormats[TemplateDateModel.DATE] = new HashMap(); |
| dateFormats[TemplateDateModel.DATETIME] = new HashMap(); |
| } |
| Map typedDateFormat = dateFormats[dateType]; |
| |
| DateFormat format = (DateFormat) typedDateFormat.get(pattern); |
| if(format != null) { |
| return format; |
| } |
| |
| // Get format from global format cache |
| synchronized(localizedDateFormats) { |
| Locale locale = getLocale(); |
| TimeZone timeZone = getTimeZone(); |
| DateFormatKey fk = new DateFormatKey(dateType, pattern, locale, timeZone); |
| format = (DateFormat)localizedDateFormats.get(fk); |
| if(format == null) { |
| // Add format to global format cache. Note this is |
| // globally done once per locale per pattern. |
| StringTokenizer tok = new StringTokenizer(pattern, "_"); |
| int dateStyle = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : DateFormat.DEFAULT; |
| if(dateStyle != -1) { |
| switch(dateType) { |
| case TemplateDateModel.UNKNOWN: { |
| throw new _TemplateModelException(new _ErrorDescriptionBuilder( |
| "Can't convert the date to string using a built-in format because it's not known " |
| + "which parts of the date are in use.") |
| .tips(MessageUtil.UNKNOWN_DATE_TO_STRING_TIPS)); |
| } |
| case TemplateDateModel.TIME: { |
| format = DateFormat.getTimeInstance(dateStyle, locale); |
| break; |
| } |
| case TemplateDateModel.DATE: { |
| format = DateFormat.getDateInstance(dateStyle, locale); |
| break; |
| } |
| case TemplateDateModel.DATETIME: { |
| int timeStyle = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : dateStyle; |
| if(timeStyle != -1) { |
| format = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale); |
| } |
| break; |
| } |
| } |
| } |
| if(format == null) { |
| try { |
| format = new SimpleDateFormat(pattern, locale); |
| } |
| catch(IllegalArgumentException e) { |
| throw new _TemplateModelException(e, new Object[] { |
| "Can't parse ", new _DelayedJQuote(pattern), " to a date format, because:\n", e }); |
| } |
| } |
| format.setTimeZone(timeZone); |
| localizedDateFormats.put(fk, format); |
| } |
| } |
| |
| // Clone it and store the clone in the local cache |
| format = (DateFormat)format.clone(); |
| typedDateFormat.put(pattern, format); |
| return format; |
| } |
| |
| int parseDateStyleToken(String token) { |
| if("short".equals(token)) { |
| return DateFormat.SHORT; |
| } |
| if("medium".equals(token)) { |
| return DateFormat.MEDIUM; |
| } |
| if("long".equals(token)) { |
| return DateFormat.LONG; |
| } |
| if("full".equals(token)) { |
| return DateFormat.FULL; |
| } |
| return -1; |
| } |
| |
| |
| /** |
| * 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)}. |
| */ |
| DateToISO8601CalendarFactory getISOBuiltInCalendar() { |
| if (isoBuiltInCalendarFactory == null) { |
| isoBuiltInCalendarFactory = new DateUtil.TrivialDateToISO8601CalendarFactory(); |
| } |
| return isoBuiltInCalendarFactory; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| 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: |
| * <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 = getConfiguration().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 = 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(), pw); |
| pw.flush(); |
| } |
| |
| /** |
| * Prints an FTL stack trace based on a stack trace snapshot. |
| * @see #getInstructionStackSnapshot() |
| * @since 2.3.20 |
| */ |
| static void outputInstructionStack( |
| TemplateElement[] instructionStackSnapshot, PrintWriter pw) { |
| pw.println(STACK_SECTION_SEPARATOR); |
| if (instructionStackSnapshot != null) { |
| for (int i = 0; i < instructionStackSnapshot.length; i++) { |
| TemplateElement stackEl = instructionStackSnapshot[i]; |
| pw.print(i == 0 ? "==> " : " "); |
| pw.println(instructionStackItemToString(stackEl)); |
| } |
| } else { |
| pw.println("[the stack was empty]"); |
| } |
| pw.println(STACK_SECTION_SEPARATOR); |
| } |
| |
| /** |
| * 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(); |
| 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("]"); |
| |
| return sb.toString(); |
| } |
| |
| 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 <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 name-space. |
| * This is correspondent of FTL <code>.main</code> hash. |
| */ |
| public Namespace getMainNamespace() { |
| return mainNamespace; |
| } |
| |
| /** |
| * Returns the main name-space. |
| * This is correspondent of FTL <code>.namespace</code> 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 <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 = getConfiguration().getSharedVariable(key); |
| } |
| return result; |
| } |
| }; |
| } |
| |
| private void pushElement(TemplateElement element) { |
| instructionStack.add(element); |
| } |
| |
| private void popElement() { |
| instructionStack.remove(instructionStack.size() - 1); |
| } |
| |
| void replaceElemetStackTop(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 Macro) && !(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 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 <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)); |
| } |
| |
| /** |
| * Gets a template for inclusion; used with {@link #include(Template includedTemplate)}. |
| * The advantage over simply using <code>config.getTemplate(...)</code> is that it chooses |
| * the default encoding as the <code>include</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.) |
| * @param encoding the encoding of the obtained template. If null, |
| * the encoding of the Template that is currently being processed in this |
| * Environment is used. |
| * @param parse whether to process a parsed template or just include the |
| * unparsed template source. |
| */ |
| public Template getTemplateForInclusion(String name, String encoding, boolean parse) |
| throws IOException |
| { |
| if (encoding == null) { |
| encoding = getTemplate().getEncoding(); |
| } |
| if (encoding == null) { |
| encoding = getConfiguration().getEncoding(this.getLocale()); |
| } |
| return getConfiguration().getTemplate(name, getLocale(), encoding, parse); |
| } |
| |
| /** |
| * 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 |
| { |
| Template prevTemplate = getTemplate(); |
| setParent(includedTemplate); |
| importMacros(includedTemplate); |
| try { |
| visit(includedTemplate.getRootTreeNode()); |
| } |
| finally { |
| setParent(prevTemplate); |
| } |
| } |
| |
| /** |
| * 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); |
| } |
| |
| 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(); |
| } |
| } |
| |
| private static final class DateFormatKey |
| { |
| private final int dateType; |
| private final String pattern; |
| private final Locale locale; |
| private final TimeZone timeZone; |
| |
| DateFormatKey(int dateType, String pattern, Locale locale, TimeZone timeZone) |
| { |
| this.dateType = dateType; |
| this.pattern = pattern; |
| this.locale = locale; |
| this.timeZone = timeZone; |
| } |
| |
| public boolean equals(Object o) |
| { |
| if(o instanceof DateFormatKey) |
| { |
| DateFormatKey fk = (DateFormatKey)o; |
| return dateType == fk.dateType && fk.pattern.equals(pattern) && fk.locale.equals(locale) && fk.timeZone.equals(timeZone); |
| } |
| return false; |
| } |
| |
| public int hashCode() |
| { |
| return dateType ^ pattern.hashCode() ^ locale.hashCode() ^ timeZone.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; |
| } |
| |
| } |