| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package freemarker.core; |
| |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.io.Writer; |
| import java.sql.Time; |
| import java.sql.Timestamp; |
| import java.text.Collator; |
| import java.text.NumberFormat; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TimeZone; |
| |
| import freemarker.cache.TemplateNameFormat; |
| import freemarker.cache._CacheAPI; |
| import freemarker.ext.beans.BeansWrapper; |
| import freemarker.log.Logger; |
| import freemarker.template.Configuration; |
| import freemarker.template.MalformedTemplateNameException; |
| import freemarker.template.ObjectWrapper; |
| import freemarker.template.SimpleHash; |
| import freemarker.template.SimpleSequence; |
| import freemarker.template.Template; |
| import freemarker.template.TemplateCollectionModel; |
| import freemarker.template.TemplateDateModel; |
| import freemarker.template.TemplateDirectiveBody; |
| import freemarker.template.TemplateDirectiveModel; |
| import freemarker.template.TemplateException; |
| import freemarker.template.TemplateExceptionHandler; |
| import freemarker.template.TemplateHashModel; |
| import freemarker.template.TemplateHashModelEx; |
| import freemarker.template.TemplateHashModelEx2; |
| import freemarker.template.TemplateModel; |
| import freemarker.template.TemplateModelException; |
| import freemarker.template.TemplateModelIterator; |
| import freemarker.template.TemplateNodeModel; |
| import freemarker.template.TemplateNumberModel; |
| import freemarker.template.TemplateScalarModel; |
| import freemarker.template.TemplateSequenceModel; |
| import freemarker.template.TemplateTransformModel; |
| import freemarker.template.TransformControl; |
| import freemarker.template.Version; |
| import freemarker.template._ObjectWrappers; |
| import freemarker.template._VersionInts; |
| 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.TemplateModelUtils; |
| import freemarker.template.utility.UndeclaredThrowableException; |
| |
| /** |
| * Object that represents the runtime environment during template processing. For every invocation of a |
| * {@code Template.process()} method, a new instance of this object is created, and then discarded when |
| * {@code 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. |
| * |
| * <p> |
| * Data models that need to access the {@code Environment} 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 {@code 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 LOG = Logger.getLogger("freemarker.runtime"); |
| private static final Logger ATTEMPT_LOGGER = Logger.getLogger("freemarker.runtime.attempt"); |
| |
| private final Configuration configuration; |
| private final boolean incompatibleImprovementsGE2328; |
| private final TemplateHashModel rootDataModel; |
| private TemplateElement[] instructionStack = new TemplateElement[16]; |
| private int instructionStackSize = 0; |
| private final ArrayList recoveredErrorStack = new ArrayList(); |
| |
| private TemplateNumberFormat cachedTemplateNumberFormat; |
| private Map<String, TemplateNumberFormat> cachedTemplateNumberFormats; |
| |
| /** |
| * Stores the date/time/date-time formatters that are used when no format is explicitly given at the place of |
| * formatting. That is, in situations like ${lastModified} or even ${lastModified?date}, but not in situations like |
| * ${lastModified?string.iso}. |
| * |
| * <p> |
| * The index of the array is calculated from what kind of formatter we want (see |
| * {@link #getTemplateDateFormatCacheArrayIndex(int, boolean, boolean)}):<br> |
| * Zoned input: 0: U, 1: T, 2: D, 3: DT<br> |
| * Zoneless input: 4: U, 5: T, 6: D, 7: DT<br> |
| * SQL D T TZ + Zoned input: 8: U, 9: T, 10: D, 11: DT<br> |
| * SQL D T TZ + Zoneless input: 12: U, 13: T, 14: D, 15: DT |
| * |
| * <p> |
| * This is a lazily filled cache. It starts out as {@code null}, then when first needed the array will be created. |
| * The array elements also start out as {@code null}-s, and they are filled as the particular kind of formatter is |
| * first needed. |
| */ |
| private TemplateDateFormat[] cachedTempDateFormatArray; |
| /** Similar to {@link #cachedTempDateFormatArray}, but used when a formatting string was specified. */ |
| private HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray; |
| private static final int CACHED_TDFS_ZONELESS_INPUT_OFFS = 4; |
| private static final int CACHED_TDFS_SQL_D_T_TZ_OFFS = CACHED_TDFS_ZONELESS_INPUT_OFFS * 2; |
| private static final int CACHED_TDFS_LENGTH = CACHED_TDFS_SQL_D_T_TZ_OFFS * 2; |
| |
| /** Caches the result of {@link #isSQLDateAndTimeTimeZoneSameAsNormal()}. */ |
| private Boolean cachedSQLDateAndTimeTimeZoneSameAsNormal; |
| |
| @Deprecated |
| private NumberFormat cNumberFormat; |
| private TemplateNumberFormat cTemplateNumberFormat; |
| private TemplateNumberFormat cTemplateNumberFormatWithPre2331IcIBug; |
| |
| /** |
| * Should be a boolean "trueAndFalseStringsCached", but with Incompatible Improvements less than 2.3.22 the |
| * effective value of {@code boolean_format} could change because of {@code #import} and {@code #include}, |
| * as those changed the parent template. So we need this cache invalidation trick. |
| */ |
| private Configurable trueAndFalseStringsCachedForParent; |
| private String cachedTrueString; |
| private String cachedFalseString; |
| |
| /** |
| * 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 LocalContextStack localContextStack; |
| private final Namespace mainNamespace; |
| private Namespace currentNamespace, globalNamespace; |
| private HashMap<String, Namespace> loadedLibs; |
| private Configurable legacyParent; |
| |
| private boolean inAttemptBlock; |
| private Throwable lastThrowable; |
| |
| private TemplateModel lastReturnValue; |
| private Map<Object, Namespace> macroToNamespaceLookup = new IdentityHashMap<>(); |
| |
| 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; |
| |
| private TemplateProcessingTracer templateProcessingTracer; |
| |
| /** |
| * Retrieves the environment object associated with the current thread, or {@code null} if there's no template |
| * processing going on in this thread. Data model implementations that need access to the environment can call this |
| * method to obtain the environment object that represents the template processing that is currently running on the |
| * current thread. |
| */ |
| public static Environment getCurrentEnvironment() { |
| return (Environment) threadEnv.get(); |
| } |
| |
| static void setCurrentEnvironment(Environment env) { |
| threadEnv.set(env); |
| } |
| |
| /** |
| * Creates an environment with the given main (top-level) template that it intends to {@linkplain #process()} later; |
| * typically, it's better to use {@link Template#createProcessingEnvironment(Object, Writer)} instead of this. |
| * |
| * @param template Not {@code null} |
| * @param rootDataModel Not {@code null} |
| * @param out Not {@code null} |
| */ |
| public Environment(Template template, final TemplateHashModel rootDataModel, Writer out) { |
| super(template); |
| configuration = template.getConfiguration(); |
| incompatibleImprovementsGE2328 = configuration.getIncompatibleImprovements().intValue() >= _VersionInts.V_2_3_28; |
| this.globalNamespace = new Namespace(null); |
| this.currentNamespace = mainNamespace = new Namespace(template); |
| this.out = out; |
| this.rootDataModel = rootDataModel; |
| importMacros(template); |
| } |
| |
| /** |
| * Despite its name it just returns {@link #getParent()}. If {@link Configuration#getIncompatibleImprovements()} is |
| * at least 2.3.22, then that will be the same as {@link #getMainTemplate()}. Otherwise the returned value follows |
| * the {@link Environment} parent switchings that occur at {@code #include}/{@code #import} and {@code #nested} |
| * directive calls, that is, it's not very meaningful outside FreeMarker internals. |
| * |
| * @deprecated Use {@link #getMainTemplate()} instead (or {@link #getCurrentNamespace()} and then |
| * {@link Namespace#getTemplate()}); the value returned by this method is often not what you expect when |
| * it comes to macro/function invocations. |
| */ |
| @Deprecated |
| public Template getTemplate() { |
| return (Template) getParent(); |
| } |
| |
| /** Returns the same value as pre-IcI 2.3.22 getTemplate() did. */ |
| Template getTemplate230() { |
| Template legacyParent = (Template) this.legacyParent; |
| return legacyParent != null ? legacyParent : getTemplate(); |
| } |
| |
| /** |
| * Returns the topmost {@link Template}, with other words, the one for which this {@link Environment} was created. |
| * That template will never change, like {@code #include} or macro calls don't change it. This method never returns |
| * {@code null}. |
| * |
| * @see #getCurrentNamespace() |
| * |
| * @since 2.3.22 |
| */ |
| public Template getMainTemplate() { |
| return mainNamespace.getTemplate(); |
| } |
| |
| /** |
| * Returns the {@link Template} that we are "lexically" inside at the moment. This template will change when |
| * entering an {@code #include} or calling a macro or function in another template, or returning to yet another |
| * template with {@code #nested}. When you are calling a directive that's implemented in Java or a Java method |
| * from a template, the current template will be the last current template, not {@code null}. This method never |
| * returns {@code null}. |
| * |
| * @see #getMainTemplate() |
| * @see #getCurrentNamespace() |
| * |
| * @since 2.3.23 |
| */ |
| @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm") |
| public Template getCurrentTemplate() { |
| int ln = instructionStackSize; |
| return ln == 0 ? getMainTemplate() : instructionStack[ln - 1].getTemplate(); |
| } |
| |
| /** |
| * Gets the currently executing <em>custom</em> directive's call place information, or {@code null} if there's no |
| * executing custom directive. This currently only works for calls made from templates with the {@code <@...>} |
| * syntax. This should only be called from the {@link TemplateDirectiveModel} that was invoked with {@code <@...>}, |
| * otherwise its return value is not defined by this API (it's usually {@code null}). |
| * |
| * @since 2.3.22 |
| */ |
| @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm") |
| public DirectiveCallPlace getCurrentDirectiveCallPlace() { |
| int ln = instructionStackSize; |
| if (ln == 0) return null; |
| TemplateElement te = instructionStack[ln - 1]; |
| if (te instanceof UnifiedCall) return (UnifiedCall) te; |
| if (te instanceof Macro && ln > 1 && instructionStack[ln - 2] instanceof UnifiedCall) { |
| return (UnifiedCall) instructionStack[ln - 2]; |
| } |
| return null; |
| } |
| |
| /** |
| * Deletes cached values that meant to be valid only during a single template execution. |
| */ |
| private void clearCachedValues() { |
| cachedTemplateNumberFormats = null; |
| cachedTemplateNumberFormat = null; |
| |
| cachedTempDateFormatArray = null; |
| cachedTempDateFormatsByFmtStrArray = null; |
| |
| cachedCollator = null; |
| cachedURLEscapingCharset = null; |
| cachedURLEscapingCharsetSet = false; |
| } |
| |
| /** |
| * Processes the template to which this environment belongs to. |
| */ |
| public void process() throws TemplateException, IOException { |
| Object savedEnv = threadEnv.get(); |
| threadEnv.set(this); |
| try { |
| // Cached values from a previous execution are possibly outdated. |
| clearCachedValues(); |
| try { |
| doAutoImportsAndIncludes(this); |
| visit(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 IOException, TemplateException { |
| // ATTENTION: This method body is manually "inlined" into visit(TemplateElement[]); keep them in sync! |
| pushElement(element); |
| try { |
| TemplateElement[] templateElementsToVisit = element.accept(this); |
| if (templateElementsToVisit != null) { |
| for (TemplateElement el : templateElementsToVisit) { |
| if (el == null) { |
| break; // Skip unused trailing buffer capacity |
| } |
| visit(el); |
| } |
| } |
| } catch (TemplateException te) { |
| handleTemplateException(te); |
| } finally { |
| popElement(); |
| } |
| // ATTENTION: This method body above is manually "inlined" into visit(TemplateElement[]); keep them in sync! |
| } |
| |
| /** |
| * @param elementBuffer |
| * The elements to visit; might contains trailing {@code null}-s. Can be {@code null}. |
| * |
| * @since 2.3.24 |
| */ |
| final void visit(TemplateElement[] elementBuffer) throws IOException, TemplateException { |
| if (elementBuffer == null) { |
| return; |
| } |
| for (TemplateElement element : elementBuffer) { |
| if (element == null) { |
| break; // Skip unused trailing buffer capacity |
| } |
| |
| // ATTENTION: This part is the manually "inlining" of visit(TemplateElement[]); keep them in sync! |
| // We don't just let Hotspot to do it, as we want a hard guarantee regarding maximum stack usage. |
| pushElement(element); |
| try { |
| TemplateElement[] templateElementsToVisit = element.accept(this); |
| if (templateElementsToVisit != null) { |
| for (TemplateElement el : templateElementsToVisit) { |
| if (el == null) { |
| break; // Skip unused trailing buffer capacity |
| } |
| visit(el); |
| } |
| } |
| } catch (TemplateException te) { |
| handleTemplateException(te); |
| } finally { |
| popElement(); |
| } |
| // ATTENTION: This part above is the manually "inlining" of visit(TemplateElement[]); keep them in sync! |
| } |
| } |
| |
| /** |
| * Visits the elements while temporarily using the parameter output {@link Writer}. |
| * |
| * @since 2.3.27 |
| */ |
| final void visit(TemplateElement[] elementBuffer, Writer out) throws IOException, TemplateException { |
| Writer prevOut = this.out; |
| this.out = out; |
| try { |
| visit(elementBuffer); |
| } finally { |
| this.out = prevOut; |
| } |
| } |
| |
| @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "Not called when stack is empty") |
| private TemplateElement replaceTopElement(TemplateElement element) { |
| return instructionStack[instructionStackSize - 1] = element; |
| } |
| |
| private static final TemplateModel[] NO_OUT_ARGS = new TemplateModel[0]; |
| |
| /** |
| * @deprecated Should be internal API |
| */ |
| @Deprecated |
| public void visit(final TemplateElement element, |
| TemplateDirectiveModel directiveModel, Map args, |
| final List bodyParameterNames) throws TemplateException, IOException { |
| visit(new TemplateElement[] { element }, directiveModel, args, bodyParameterNames); |
| } |
| |
| void visit(final TemplateElement[] childBuffer, |
| TemplateDirectiveModel directiveModel, Map args, |
| final List bodyParameterNames) throws TemplateException, IOException { |
| TemplateDirectiveBody nested; |
| if (childBuffer == null) { |
| nested = null; |
| } else { |
| nested = new NestedElementTemplateDirectiveBody(childBuffer); |
| } |
| final TemplateModel[] outArgs; |
| if (bodyParameterNames == null || bodyParameterNames.isEmpty()) { |
| outArgs = NO_OUT_ARGS; |
| } else { |
| outArgs = new TemplateModel[bodyParameterNames.size()]; |
| } |
| if (outArgs.length > 0) { |
| pushLocalContext(new LocalContext() { |
| |
| @Override |
| public TemplateModel getLocalVariable(String name) { |
| int index = bodyParameterNames.indexOf(name); |
| return index != -1 ? outArgs[index] : null; |
| } |
| |
| @Override |
| public Collection getLocalVariableNames() { |
| return bodyParameterNames; |
| } |
| }); |
| } |
| try { |
| directiveModel.execute(this, args, outArgs, nested); |
| } catch (FlowControlException e) { |
| throw e; |
| } catch (TemplateException e) { |
| throw e; |
| } catch (IOException e) { |
| // For backward compatibility, we assume that this is because the output Writer has thrown it. |
| throw e; |
| } catch (Exception e) { |
| if (EvalUtil.shouldWrapUncheckedException(e, this)) { |
| throw new _MiscTemplateException( |
| e, this, "Directive has thrown an unchecked exception; see the cause exception."); |
| } else if (e instanceof RuntimeException) { |
| throw (RuntimeException) e; |
| } else { |
| throw new UndeclaredThrowableException(e); |
| } |
| } finally { |
| if (outArgs.length > 0) { |
| localContextStack.pop(); |
| } |
| } |
| } |
| |
| /** |
| * "Visit" the template element, passing the output through a TemplateTransformModel |
| * |
| * @param elementBuffer |
| * the element to visit through a transform; might contains trailing {@code null}-s |
| * @param transform |
| * the transform to pass the element output through |
| * @param args |
| * optional arguments fed to the transform |
| */ |
| void visitAndTransform(TemplateElement[] elementBuffer, |
| TemplateTransformModel transform, |
| Map args) |
| throws TemplateException, IOException { |
| try { |
| Writer tw = transform.getWriter(out, args); |
| if (tw == null) tw = EMPTY_BODY_WRITER; |
| TransformControl tc = tw instanceof TransformControl |
| ? (TransformControl) tw |
| : null; |
| |
| Writer prevOut = out; |
| out = tw; |
| try { |
| if (tc == null || tc.onStart() != TransformControl.SKIP_BODY) { |
| do { |
| visit(elementBuffer); |
| } while (tc != null && tc.afterBody() == TransformControl.REPEAT_EVALUATION); |
| } |
| } catch (Throwable t) { |
| try { |
| if (tc != null |
| && !(t instanceof FlowControlException |
| && getConfiguration().getIncompatibleImprovements().intValue() |
| >= _VersionInts.V_2_3_27)) { |
| tc.onError(t); |
| } else { |
| throw t; |
| } |
| } catch (TemplateException | IOException | Error e) { |
| throw e; |
| } catch (Throwable e) { |
| if (EvalUtil.shouldWrapUncheckedException(e, this)) { |
| throw new _MiscTemplateException( |
| e, this, "Transform has thrown an unchecked exception; see the cause exception."); |
| } else if (e instanceof RuntimeException) { |
| throw (RuntimeException) e; |
| } else { |
| throw new UndeclaredThrowableException(e); |
| } |
| } |
| } finally { |
| out = prevOut; |
| if (prevOut != tw) { |
| tw.close(); |
| } |
| } |
| } catch (TemplateException te) { |
| handleTemplateException(te); |
| } |
| } |
| |
| /** |
| * Visit a block using buffering/recovery |
| */ |
| void visitAttemptRecover( |
| AttemptBlock attemptBlock, TemplateElement attemptedSection, RecoveryBlock recoverySection) |
| 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; |
| visit(attemptedSection); |
| } 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(recoverySection); |
| } finally { |
| recoveredErrorStack.remove(recoveredErrorStack.size() - 1); |
| } |
| } else { |
| out.write(sw.toString()); |
| } |
| } |
| |
| String getCurrentRecoveredErrorMessage() throws TemplateException { |
| if (recoveredErrorStack.isEmpty()) { |
| throw new _MiscTemplateException(this, ".error is not available outside of a #recover block"); |
| } |
| return ((Throwable) recoveredErrorStack.get(recoveredErrorStack.size() - 1)).getMessage(); |
| } |
| |
| /** |
| * Tells if we are inside an {@code #attempt} block (but before {@code #recover}). This can be useful for |
| * {@link TemplateExceptionHandler}-s, as then they may don't want to print the error to the output, as |
| * {@code #attempt} will roll it back anyway. |
| * |
| * @since 2.3.20 |
| */ |
| public boolean isInAttemptBlock() { |
| return inAttemptBlock; |
| } |
| |
| /** |
| * Used for {@code #nested}. |
| */ |
| void invokeNestedContent(BodyInstruction.Context bodyCtx) throws TemplateException, IOException { |
| Macro.Context invokingMacroContext = getCurrentMacroContext(); |
| LocalContextStack prevLocalContextStack = localContextStack; |
| TemplateObject callPlace = invokingMacroContext.callPlace; |
| TemplateElement[] nestedContentBuffer = callPlace instanceof TemplateElement |
| ? ((TemplateElement) callPlace).getChildBuffer() : null; |
| if (nestedContentBuffer != null) { |
| this.currentMacroContext = invokingMacroContext.prevMacroContext; |
| currentNamespace = invokingMacroContext.nestedContentNamespace; |
| |
| final Configurable prevParent; |
| final boolean parentReplacementOn = isBeforeIcI2322(); |
| prevParent = getParent(); |
| if (parentReplacementOn) { |
| setParent(currentNamespace.getTemplate()); |
| } else { |
| legacyParent = currentNamespace.getTemplate(); |
| } |
| |
| this.localContextStack = invokingMacroContext.prevLocalContextStack; |
| if (invokingMacroContext.nestedContentParameterNames != null) { |
| pushLocalContext(bodyCtx); |
| } |
| try { |
| visit(nestedContentBuffer); |
| } finally { |
| if (invokingMacroContext.nestedContentParameterNames != null) { |
| localContextStack.pop(); |
| } |
| this.currentMacroContext = invokingMacroContext; |
| currentNamespace = getMacroNamespace(invokingMacroContext.getMacro()); |
| if (parentReplacementOn) { |
| setParent(prevParent); |
| } else { |
| legacyParent = prevParent; |
| } |
| this.localContextStack = prevLocalContextStack; |
| } |
| } |
| } |
| |
| /** |
| * "visit" an IteratorBlock |
| */ |
| boolean visitIteratorBlock(IteratorBlock.IterationContext ictxt) |
| throws TemplateException, IOException { |
| pushLocalContext(ictxt); |
| try { |
| return ictxt.accept(this); |
| } catch (TemplateException te) { |
| handleTemplateException(te); |
| return true; |
| } finally { |
| localContextStack.pop(); |
| } |
| } |
| |
| /** |
| * @param loopVarName |
| * Then name of the loop variable that's also visible in FTL at the moment, whose context we are looking |
| * for. |
| * @return The matching context or {@code null} if no such context exists. |
| */ |
| IteratorBlock.IterationContext findEnclosingIterationContextWithVisibleVariable(String loopVarName) { |
| return findEnclosingIterationContext(loopVarName); |
| } |
| |
| /** |
| * @return The matching context or {@code null} if no such context exists. |
| */ |
| IteratorBlock.IterationContext findClosestEnclosingIterationContext() { |
| return findEnclosingIterationContext(null); |
| } |
| |
| private IteratorBlock.IterationContext findEnclosingIterationContext(String visibleLoopVarName) { |
| LocalContextStack ctxStack = getLocalContextStack(); |
| if (ctxStack != null) { |
| for (int i = ctxStack.size() - 1; i >= 0; i--) { |
| Object ctx = ctxStack.get(i); |
| if (ctx instanceof IteratorBlock.IterationContext |
| && (visibleLoopVarName == null |
| || ((IteratorBlock.IterationContext) ctx) |
| .hasVisibleLoopVar(visibleLoopVarName))) { |
| return (IteratorBlock.IterationContext) ctx; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Evaluate expression with shadowing a single variable with a new local variable. |
| */ |
| TemplateModel evaluateWithNewLocal(Expression exp, String lambdaArgName, TemplateModel lamdaArgValue) |
| throws TemplateException { |
| pushLocalContext(new LocalContextWithNewLocal(lambdaArgName, lamdaArgValue)); |
| try { |
| return exp.eval(this); |
| } finally { |
| localContextStack.pop(); |
| } |
| } |
| |
| /** |
| * Specialization for 1 local variables. |
| */ |
| private static class LocalContextWithNewLocal implements LocalContext { |
| private final String lambdaArgName; |
| private final TemplateModel lambdaArgValue; |
| |
| public LocalContextWithNewLocal(String lambdaArgName, TemplateModel lambdaArgValue) { |
| this.lambdaArgName = lambdaArgName; |
| this.lambdaArgValue = lambdaArgValue; |
| } |
| |
| @Override |
| public TemplateModel getLocalVariable(String name) throws TemplateModelException { |
| return name.equals(lambdaArgName) ? lambdaArgValue : null; |
| } |
| |
| @Override |
| public Collection getLocalVariableNames() throws TemplateModelException { |
| return Collections.singleton(lambdaArgName); |
| } |
| } |
| |
| /** |
| * Used for {@code #visit} and {@code #recurse}. |
| */ |
| void invokeNodeHandlerFor(TemplateNodeModel node, TemplateSequenceModel namespaces) |
| throws TemplateException, IOException { |
| if (nodeNamespaces == null) { |
| SimpleSequence ss = new SimpleSequence(1, _ObjectWrappers.SAFE_OBJECT_WRAPPER); |
| 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) { |
| invokeMacro((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) { |
| invokeMacro((Macro) macroOrTransform, null, null, null, null); |
| } else if (macroOrTransform instanceof TemplateTransformModel) { |
| visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null); |
| } |
| } |
| |
| /** |
| * Calls a macro with the given arguments and nested block. |
| */ |
| void invokeMacro(Macro macro, |
| Map<String, ? extends Expression> namedArgs, List<? extends Expression> positionalArgs, |
| List<String> bodyParameterNames, TemplateObject callPlace) throws TemplateException, IOException { |
| invokeMacroOrFunctionCommonPart(macro, namedArgs, positionalArgs, bodyParameterNames, callPlace); |
| } |
| |
| /** |
| * Calls an FTL function, and returns its return value. |
| */ |
| TemplateModel invokeFunction( |
| Environment env, Macro func, List<? extends Expression> argumentExps, TemplateObject callPlace) |
| throws TemplateException { |
| env.setLastReturnValue(null); |
| if (!func.isFunction()) { |
| throw new _MiscTemplateException(env, "A macro cannot be called in an expression. (Functions can be.)"); |
| } |
| Writer prevOut = env.getOut(); |
| try { |
| env.setOut(NullWriter.INSTANCE); |
| env.invokeMacro(func, null, argumentExps, null, callPlace); |
| } catch (IOException e) { |
| // Should not occur |
| throw new TemplateException("Unexpected exception during function execution", e, env); |
| } finally { |
| env.setOut(prevOut); |
| } |
| return env.getLastReturnValue(); |
| } |
| |
| private void invokeMacroOrFunctionCommonPart(Macro macroOrFunction, |
| Map<String, ? extends Expression> namedArgs, List<? extends Expression> positionalArgs, |
| List<String> bodyParameterNames, TemplateObject callPlace) throws TemplateException, |
| IOException { |
| if (macroOrFunction == Macro.DO_NOTHING_MACRO) { |
| return; |
| } |
| |
| boolean elementPushed; |
| if (!incompatibleImprovementsGE2328) { |
| // Doing this so early is wrong, as now the arguments will be evaluated while the called macro/function is |
| // in the element stack. Thus .current_template_name will be wrong for example. |
| pushElement(macroOrFunction); |
| elementPushed = true; |
| } else { |
| elementPushed = false; |
| } |
| try { |
| final Macro.Context macroCtx = macroOrFunction.new Context(this, callPlace, bodyParameterNames); |
| // Causes the evaluation of argument expressions: |
| setMacroContextLocalsFromArguments(macroCtx, macroOrFunction, namedArgs, positionalArgs); |
| |
| if (!elementPushed) { // When incompatibleImprovements >= 2.3.28 |
| pushElement(macroOrFunction); |
| elementPushed = true; |
| } |
| |
| final Macro.Context prevMacroCtx = currentMacroContext; |
| currentMacroContext = macroCtx; |
| |
| final LocalContextStack prevLocalContextStack = localContextStack; |
| localContextStack = null; |
| |
| final Namespace prevNamespace = currentNamespace; |
| currentNamespace = getMacroNamespace(macroOrFunction); |
| |
| try { |
| macroCtx.checkParamsSetAndApplyDefaults(this); |
| visit(macroOrFunction.getChildBuffer()); |
| } catch (ReturnInstruction.Return re) { |
| // Not an error, just a <#return> |
| } catch (TemplateException te) { |
| handleTemplateException(te); |
| } finally { |
| currentMacroContext = prevMacroCtx; |
| localContextStack = prevLocalContextStack; |
| currentNamespace = prevNamespace; |
| } |
| } finally { |
| if (elementPushed) { |
| popElement(); |
| } |
| } |
| } |
| |
| /** |
| * Sets the local variables corresponding to the macro call arguments in the macro context. |
| */ |
| private void setMacroContextLocalsFromArguments( |
| final Macro.Context macroCtx, |
| final Macro macro, |
| final Map<String, ? extends Expression> namedArgs, final List<? extends Expression> positionalArgs) |
| throws TemplateException { |
| String catchAllParamName = macro.getCatchAll(); |
| SimpleHash namedCatchAllParamValue = null; |
| SimpleSequence positionalCatchAllParamValue = null; |
| int nextPositionalArgToAssignIdx = 0; |
| |
| // Used for ?with_args(...): |
| WithArgsState withArgsState = getWithArgState(macro); |
| if (withArgsState != null) { |
| TemplateHashModelEx byNameWithArgs = withArgsState.byName; |
| TemplateSequenceModel byPositionWithArgs = withArgsState.byPosition; |
| |
| if (byNameWithArgs != null) { |
| TemplateHashModelEx2.KeyValuePairIterator withArgsKVPIter |
| = TemplateModelUtils.getKeyValuePairIterator(byNameWithArgs); |
| while (withArgsKVPIter.hasNext()) { |
| TemplateHashModelEx2.KeyValuePair withArgKVP = withArgsKVPIter.next(); |
| |
| String argName; |
| { |
| TemplateModel argNameTM = withArgKVP.getKey(); |
| if (!(argNameTM instanceof TemplateScalarModel)) { |
| throw new _TemplateModelException( |
| "Expected string keys in the \"with args\" hash, but one of the keys was ", |
| new _DelayedAOrAn(new _DelayedFTLTypeDescription(argNameTM)), "."); |
| } |
| argName = EvalUtil.modelToString((TemplateScalarModel) argNameTM, null, null); |
| } |
| |
| TemplateModel argValue = withArgKVP.getValue(); |
| // What if argValue is null? It still has to occur in the named catch-all parameter, to be similar |
| // to <@macroWithCatchAll a=null b=null />, which will also add the keys to the catch-all hash. |
| // Similarly, we also still fail if the name is not declared. |
| final boolean isArgNameDeclared = macro.hasArgNamed(argName); |
| if (isArgNameDeclared) { |
| macroCtx.setLocalVar(argName, argValue); |
| } else if (catchAllParamName != null) { |
| if (namedCatchAllParamValue == null) { |
| namedCatchAllParamValue = initNamedCatchAllParameter(macroCtx, catchAllParamName); |
| } |
| if (!withArgsState.orderLast) { |
| namedCatchAllParamValue.put(argName, argValue); |
| } else { |
| List<NameValuePair> orderLastByNameCatchAll = withArgsState.orderLastByNameCatchAll; |
| if (orderLastByNameCatchAll == null) { |
| orderLastByNameCatchAll = new ArrayList<>(); |
| withArgsState.orderLastByNameCatchAll = orderLastByNameCatchAll; |
| } |
| orderLastByNameCatchAll.add(new NameValuePair(argName, argValue)); |
| } |
| } else { |
| throw newUndeclaredParamNameException(macro, argName); |
| } |
| } // while (withArgsKVPIter.hasNext()) |
| } else if (byPositionWithArgs != null) { |
| if (!withArgsState.orderLast) { // ?withArgs |
| String[] argNames = macro.getArgumentNamesNoCopy(); |
| final int argsCnt = byPositionWithArgs.size(); |
| if (argNames.length < argsCnt && catchAllParamName == null) { |
| throw newTooManyArgumentsException(macro, argNames, argsCnt); |
| } |
| for (int argIdx = 0; argIdx < argsCnt; argIdx++) { |
| TemplateModel argValue = byPositionWithArgs.get(argIdx); |
| try { |
| if (nextPositionalArgToAssignIdx < argNames.length) { |
| String argName = argNames[nextPositionalArgToAssignIdx++]; |
| macroCtx.setLocalVar(argName, argValue); |
| } else { |
| if (positionalCatchAllParamValue == null) { |
| positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName); |
| } |
| positionalCatchAllParamValue.add(argValue); |
| } |
| } catch (RuntimeException re) { |
| throw new _MiscTemplateException(re, this); |
| } |
| } |
| } else { // ?withArgsLast |
| if (namedArgs != null && !namedArgs.isEmpty() && byPositionWithArgs.size() != 0) { |
| // Unlike with ?withArgs, here we can't know in general which argument byPositionWithArgs[0] |
| // meant to refer to, as the named arguments have already taken some indexes. |
| throw new _MiscTemplateException("Call can't pass parameters by name, as there's " + |
| "\"with args last\" in effect that specifies parameters by position."); |
| } |
| if (catchAllParamName == null) { |
| // To fail before Expression-s for some normal arguments are evaluated: |
| int totalPositionalArgCnt = |
| (positionalArgs != null ? positionalArgs.size() : 0) + byPositionWithArgs.size(); |
| if (totalPositionalArgCnt > macro.getArgumentNamesNoCopy().length) { |
| throw newTooManyArgumentsException(macro, macro.getArgumentNamesNoCopy(), totalPositionalArgCnt); |
| } |
| } |
| } |
| } |
| } // if (withArgsState != null) |
| |
| if (namedArgs != null) { |
| if (catchAllParamName != null && namedCatchAllParamValue == null && positionalCatchAllParamValue == null) { |
| // If a macro call has no argument (like <@m />), before 2.3.30 we assumed it's a by-name call. But now |
| // if we have ?with_args(args), its argument type decides if the call is by-name or by-position. |
| if (namedArgs.isEmpty() && withArgsState != null && withArgsState.byPosition != null) { |
| positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName); |
| } else { |
| namedCatchAllParamValue = initNamedCatchAllParameter(macroCtx, catchAllParamName); |
| } |
| } |
| |
| for (Map.Entry<String, ? extends Expression> argNameAndValExp : namedArgs.entrySet()) { |
| final String argName = argNameAndValExp.getKey(); |
| final boolean isArgNameDeclared = macro.hasArgNamed(argName); |
| if (isArgNameDeclared || namedCatchAllParamValue != null) { |
| final Expression argValueExp = argNameAndValExp.getValue(); |
| TemplateModel argValue = argValueExp.eval(this); |
| if (isArgNameDeclared) { |
| macroCtx.setLocalVar(argName, argValue); |
| } else { |
| namedCatchAllParamValue.put(argName, argValue); |
| } |
| } else { |
| if (positionalCatchAllParamValue != null) { |
| throw newBothNamedAndPositionalCatchAllParamsException(macro); |
| } else { |
| throw newUndeclaredParamNameException(macro, argName); |
| } |
| } |
| } |
| } else if (positionalArgs != null) { |
| if (catchAllParamName != null && positionalCatchAllParamValue == null && namedCatchAllParamValue == null) { |
| if (positionalArgs.isEmpty() && withArgsState != null && withArgsState.byName != null) { |
| namedCatchAllParamValue = initNamedCatchAllParameter(macroCtx, catchAllParamName); |
| } else { |
| positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName); |
| } |
| } |
| |
| String[] argNames = macro.getArgumentNamesNoCopy(); |
| final int argsCnt = positionalArgs.size(); |
| final int argsWithWithArgsCnt = argsCnt + nextPositionalArgToAssignIdx; |
| if (argNames.length < argsWithWithArgsCnt && positionalCatchAllParamValue == null) { |
| if (namedCatchAllParamValue != null) { |
| throw newBothNamedAndPositionalCatchAllParamsException(macro); |
| } else { |
| throw newTooManyArgumentsException(macro, argNames, argsWithWithArgsCnt); |
| } |
| } |
| for (int srcPosArgIdx = 0; srcPosArgIdx < argsCnt; srcPosArgIdx++) { |
| Expression argValueExp = positionalArgs.get(srcPosArgIdx); |
| TemplateModel argValue; |
| try { |
| argValue = argValueExp.eval(this); |
| } catch (RuntimeException e) { |
| throw new _MiscTemplateException(e, this); |
| } |
| if (nextPositionalArgToAssignIdx < argNames.length) { |
| String argName = argNames[nextPositionalArgToAssignIdx++]; |
| macroCtx.setLocalVar(argName, argValue); |
| } else { |
| positionalCatchAllParamValue.add(argValue); |
| } |
| } |
| } // else if (positionalArgs != null) |
| |
| if (withArgsState != null && withArgsState.orderLast) { |
| if (withArgsState.orderLastByNameCatchAll != null) { |
| for (NameValuePair nameValuePair : withArgsState.orderLastByNameCatchAll) { |
| if (!namedCatchAllParamValue.containsKey(nameValuePair.name)) { |
| namedCatchAllParamValue.put(nameValuePair.name, nameValuePair.value); |
| } |
| } |
| } else if (withArgsState.byPosition != null) { |
| TemplateSequenceModel byPosition = withArgsState.byPosition; |
| int withArgCnt = byPosition.size(); |
| String[] argNames = macro.getArgumentNamesNoCopy(); |
| for (int withArgIdx = 0; withArgIdx < withArgCnt; withArgIdx++) { |
| TemplateModel withArgValue = byPosition.get(withArgIdx); |
| if (nextPositionalArgToAssignIdx < argNames.length) { |
| String argName = argNames[nextPositionalArgToAssignIdx++]; |
| macroCtx.setLocalVar(argName, withArgValue); |
| } else { |
| // It was checked much earlier that we don't have too many arguments, so this must work: |
| positionalCatchAllParamValue.add(withArgValue); |
| } |
| } |
| } |
| } |
| } |
| |
| private static WithArgsState getWithArgState(Macro macro) { |
| Macro.WithArgs withArgs = macro.getWithArgs(); |
| return withArgs == null ? null : new WithArgsState(withArgs.getByName(), withArgs.getByPosition(), |
| withArgs.isOrderLast()); |
| } |
| |
| private static final class WithArgsState { |
| private final TemplateHashModelEx byName; |
| private final TemplateSequenceModel byPosition; |
| private final boolean orderLast; |
| private List<NameValuePair> orderLastByNameCatchAll; |
| |
| public WithArgsState(TemplateHashModelEx byName, TemplateSequenceModel byPosition, boolean orderLast) { |
| this.byName = byName; |
| this.byPosition = byPosition; |
| this.orderLast = orderLast; |
| } |
| } |
| |
| private static final class NameValuePair { |
| private final String name; |
| private final TemplateModel value; |
| |
| public NameValuePair(String name, TemplateModel value) { |
| this.name = name; |
| this.value = value; |
| } |
| } |
| |
| private _MiscTemplateException newTooManyArgumentsException(Macro macro, String[] argNames, int argsCnt) { |
| return new _MiscTemplateException(this, |
| (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()), |
| " only accepts ", new _DelayedToString(argNames.length), " parameters, but got ", |
| new _DelayedToString(argsCnt), "."); |
| } |
| |
| private static SimpleSequence initPositionalCatchAllParameter(Macro.Context macroCtx, String catchAllParamName) { |
| SimpleSequence positionalCatchAllParamValue; |
| positionalCatchAllParamValue = new SimpleSequence(_ObjectWrappers.SAFE_OBJECT_WRAPPER); |
| macroCtx.setLocalVar(catchAllParamName, positionalCatchAllParamValue); |
| return positionalCatchAllParamValue; |
| } |
| |
| private static SimpleHash initNamedCatchAllParameter(Macro.Context macroCtx, String catchAllParamName) { |
| SimpleHash namedCatchAllParamValue; |
| namedCatchAllParamValue = new SimpleHash( |
| new LinkedHashMap<String, Object>(), _ObjectWrappers.SAFE_OBJECT_WRAPPER, 0); |
| macroCtx.setLocalVar(catchAllParamName, namedCatchAllParamValue); |
| return namedCatchAllParamValue; |
| } |
| |
| private _MiscTemplateException newUndeclaredParamNameException(Macro macro, String argName) { |
| return new _MiscTemplateException(this, |
| (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()), |
| " has no parameter with name ", new _DelayedJQuote(argName), ". Valid parameter names are: " |
| , new _DelayedJoinWithComma(macro.getArgumentNamesNoCopy())); |
| } |
| |
| private _MiscTemplateException newBothNamedAndPositionalCatchAllParamsException(Macro macro) { |
| return new _MiscTemplateException(this, |
| (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()), |
| " call can't have both named and positional arguments that has to go into catch-all parameter."); |
| } |
| |
| /** |
| * Defines the given macro in the current namespace (doesn't call it). |
| */ |
| void visitMacroDef(Macro macro) { |
| macroToNamespaceLookup.put(macro.getNamespaceLookupKey(), currentNamespace); |
| currentNamespace.put(macro.getName(), macro); |
| } |
| |
| Namespace getMacroNamespace(Macro macro) { |
| return macroToNamespaceLookup.get(macro.getNamespaceLookupKey()); |
| } |
| |
| 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; |
| } |
| int size = children.size(); |
| for (int i = 0; i < size; i++) { |
| TemplateNodeModel child = (TemplateNodeModel) children.get(i); |
| if (child != null) { |
| invokeNodeHandlerFor(child, namespaces); |
| } |
| } |
| } |
| |
| Macro.Context getCurrentMacroContext() { |
| return currentMacroContext; |
| } |
| |
| private void handleTemplateException(TemplateException templateException) |
| throws TemplateException { |
| if (templateException instanceof TemplateModelException |
| && ((TemplateModelException) templateException).getReplaceWithCause() |
| && templateException.getCause() instanceof TemplateException) { |
| templateException = (TemplateException) templateException.getCause(); |
| } |
| |
| // Logic to prevent double-handling of the exception in |
| // nested visit() calls. |
| if (lastThrowable == templateException) { |
| throw templateException; |
| } |
| lastThrowable = templateException; |
| |
| if (getLogTemplateExceptions() && LOG.isErrorEnabled() |
| && !isInAttemptBlock() /* because then the AttemptExceptionReporter will report this */) { |
| LOG.error("Error executing FreeMarker template", templateException); |
| } |
| |
| try { |
| // Stop exception is not passed to the handler, but |
| // explicitly rethrown. |
| if (templateException instanceof StopException) { |
| throw templateException; |
| } |
| |
| // Finally, pass the exception to the handler |
| getTemplateExceptionHandler().handleTemplateException(templateException, this, out); |
| } catch (TemplateException e) { |
| // Note that if the TemplateExceptionHandler doesn't rethrow the exception, we don't get in there. |
| if (isInAttemptBlock()) { |
| this.getAttemptExceptionReporter().report(templateException, this); |
| } |
| throw e; |
| } |
| } |
| |
| @Override |
| public void setTemplateExceptionHandler(TemplateExceptionHandler templateExceptionHandler) { |
| super.setTemplateExceptionHandler(templateExceptionHandler); |
| lastThrowable = null; |
| } |
| |
| @Override |
| public void setLocale(Locale locale) { |
| Locale prevLocale = getLocale(); |
| super.setLocale(locale); |
| if (!locale.equals(prevLocale)) { |
| cachedTemplateNumberFormats = null; |
| if (cachedTemplateNumberFormat != null && cachedTemplateNumberFormat.isLocaleBound()) { |
| cachedTemplateNumberFormat = null; |
| } |
| |
| if (cachedTempDateFormatArray != null) { |
| for (int i = 0; i < CACHED_TDFS_LENGTH; i++) { |
| final TemplateDateFormat f = cachedTempDateFormatArray[i]; |
| if (f != null && f.isLocaleBound()) { |
| cachedTempDateFormatArray[i] = null; |
| } |
| } |
| } |
| |
| cachedTempDateFormatsByFmtStrArray = null; |
| |
| cachedCollator = null; |
| } |
| } |
| |
| @Override |
| public void setTimeZone(TimeZone timeZone) { |
| TimeZone prevTimeZone = getTimeZone(); |
| super.setTimeZone(timeZone); |
| |
| if (!timeZone.equals(prevTimeZone)) { |
| if (cachedTempDateFormatArray != null) { |
| for (int i = 0; i < CACHED_TDFS_SQL_D_T_TZ_OFFS; i++) { |
| TemplateDateFormat f = cachedTempDateFormatArray[i]; |
| if (f != null && f.isTimeZoneBound()) { |
| cachedTempDateFormatArray[i] = null; |
| } |
| } |
| } |
| if (cachedTempDateFormatsByFmtStrArray != null) { |
| for (int i = 0; i < CACHED_TDFS_SQL_D_T_TZ_OFFS; i++) { |
| cachedTempDateFormatsByFmtStrArray[i] = null; |
| } |
| } |
| |
| cachedSQLDateAndTimeTimeZoneSameAsNormal = null; |
| } |
| } |
| |
| @Override |
| public void setSQLDateAndTimeTimeZone(TimeZone timeZone) { |
| TimeZone prevTimeZone = getSQLDateAndTimeTimeZone(); |
| super.setSQLDateAndTimeTimeZone(timeZone); |
| |
| if (!nullSafeEquals(timeZone, prevTimeZone)) { |
| if (cachedTempDateFormatArray != null) { |
| for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) { |
| TemplateDateFormat format = cachedTempDateFormatArray[i]; |
| if (format != null && format.isTimeZoneBound()) { |
| cachedTempDateFormatArray[i] = null; |
| } |
| } |
| } |
| if (cachedTempDateFormatsByFmtStrArray != null) { |
| for (int i = CACHED_TDFS_SQL_D_T_TZ_OFFS; i < CACHED_TDFS_LENGTH; i++) { |
| cachedTempDateFormatsByFmtStrArray[i] = null; |
| } |
| } |
| |
| cachedSQLDateAndTimeTimeZoneSameAsNormal = null; |
| } |
| } |
| |
| // Replace with Objects.equals in Java 7 |
| private static boolean nullSafeEquals(Object o1, Object o2) { |
| if (o1 == o2) return true; |
| if (o1 == null || o2 == null) return false; |
| return o1.equals(o2); |
| } |
| |
| /** |
| * Tells if the same concrete time zone is used for SQL date-only and time-only values as for other |
| * date/time/date-time values. |
| */ |
| boolean isSQLDateAndTimeTimeZoneSameAsNormal() { |
| if (cachedSQLDateAndTimeTimeZoneSameAsNormal == null) { |
| cachedSQLDateAndTimeTimeZoneSameAsNormal = Boolean.valueOf( |
| getSQLDateAndTimeTimeZone() == null |
| || getSQLDateAndTimeTimeZone().equals(getTimeZone())); |
| } |
| return cachedSQLDateAndTimeTimeZoneSameAsNormal.booleanValue(); |
| } |
| |
| @Override |
| public void setURLEscapingCharset(String urlEscapingCharset) { |
| cachedURLEscapingCharsetSet = false; |
| super.setURLEscapingCharset(urlEscapingCharset); |
| } |
| |
| /* |
| * Note that altough it's not allowed to set this setting with the {@code setting} directive, it still must be |
| * allowed to set it from Java code while the template executes, since some frameworks allow templates to actually |
| * change the output encoding on-the-fly. |
| */ |
| @Override |
| public void setOutputEncoding(String outputEncoding) { |
| cachedURLEscapingCharsetSet = false; |
| super.setOutputEncoding(outputEncoding); |
| } |
| |
| /** |
| * Returns the name of the charset that should be used for URL encoding. This will be <code>null</code> if the |
| * information is not available. The function caches the return value, so it's quick to call it repeatedly. |
| */ |
| String getEffectiveURLEscapingCharset() { |
| if (!cachedURLEscapingCharsetSet) { |
| cachedURLEscapingCharset = getURLEscapingCharset(); |
| if (cachedURLEscapingCharset == null) { |
| cachedURLEscapingCharset = getOutputEncoding(); |
| } |
| cachedURLEscapingCharsetSet = true; |
| } |
| return cachedURLEscapingCharset; |
| } |
| |
| Collator getCollator() { |
| if (cachedCollator == null) { |
| cachedCollator = Collator.getInstance(getLocale()); |
| } |
| return cachedCollator; |
| } |
| |
| /** |
| * Compares two {@link TemplateModel}-s according the rules of the FTL "==" operator. |
| * |
| * @since 2.3.20 |
| */ |
| public boolean applyEqualsOperator(TemplateModel leftValue, TemplateModel rightValue) |
| throws TemplateException { |
| return EvalUtil.compare(leftValue, EvalUtil.CMP_OP_EQUALS, rightValue, this); |
| } |
| |
| /** |
| * Compares two {@link TemplateModel}-s according the rules of the FTL "==" operator, except that if the two types |
| * are incompatible, they are treated as non-equal instead of throwing an exception. Comparing dates of different |
| * types (date-only VS time-only VS date-time) will still throw an exception, however. |
| * |
| * @since 2.3.20 |
| */ |
| public boolean applyEqualsOperatorLenient(TemplateModel leftValue, TemplateModel rightValue) |
| throws TemplateException { |
| return EvalUtil.compareLenient(leftValue, EvalUtil.CMP_OP_EQUALS, rightValue, this); |
| } |
| |
| /** |
| * Compares two {@link TemplateModel}-s according the rules of the FTL "<" operator. |
| * |
| * @since 2.3.20 |
| */ |
| public boolean applyLessThanOperator(TemplateModel leftValue, TemplateModel rightValue) |
| throws TemplateException { |
| return EvalUtil.compare(leftValue, EvalUtil.CMP_OP_LESS_THAN, rightValue, this); |
| } |
| |
| /** |
| * Compares two {@link TemplateModel}-s according the rules of the FTL "<" operator. |
| * |
| * @since 2.3.20 |
| */ |
| public boolean applyLessThanOrEqualsOperator(TemplateModel leftValue, TemplateModel rightValue) |
| throws TemplateException { |
| return EvalUtil.compare(leftValue, EvalUtil.CMP_OP_LESS_THAN_EQUALS, rightValue, this); |
| } |
| |
| /** |
| * Compares two {@link TemplateModel}-s according the rules of the FTL ">" operator. |
| * |
| * @since 2.3.20 |
| */ |
| public boolean applyGreaterThanOperator(TemplateModel leftValue, TemplateModel rightValue) |
| throws TemplateException { |
| return EvalUtil.compare(leftValue, EvalUtil.CMP_OP_GREATER_THAN, rightValue, this); |
| } |
| |
| /** |
| * Compares two {@link TemplateModel}-s according the rules of the FTL ">=" operator. |
| * |
| * @since 2.3.20 |
| */ |
| public boolean applyWithGreaterThanOrEqualsOperator(TemplateModel leftValue, TemplateModel rightValue) |
| throws TemplateException { |
| return EvalUtil.compare(leftValue, EvalUtil.CMP_OP_GREATER_THAN_EQUALS, rightValue, this); |
| } |
| |
| public void setOut(Writer out) { |
| this.out = out; |
| } |
| |
| public Writer getOut() { |
| return out; |
| } |
| |
| @Override |
| public void setNumberFormat(String formatName) { |
| super.setNumberFormat(formatName); |
| cachedTemplateNumberFormat = null; |
| } |
| |
| /** |
| * Format number with the default number format. |
| * |
| * @param exp |
| * The blamed expression if an error occurs; it's only needed for better error messages |
| */ |
| String formatNumberToPlainText(TemplateNumberModel number, Expression exp, boolean useTempModelExc) |
| throws TemplateException { |
| return formatNumberToPlainText(number, getTemplateNumberFormat(exp, useTempModelExc), exp, useTempModelExc); |
| } |
| |
| /** |
| * Format number with the number format specified as the parameter, with the current locale. |
| * |
| * @param exp |
| * The blamed expression if an error occurs; it's only needed for better error messages |
| */ |
| String formatNumberToPlainText( |
| TemplateNumberModel number, TemplateNumberFormat format, Expression exp, |
| boolean useTempModelExc) |
| throws TemplateException { |
| try { |
| return EvalUtil.assertFormatResultNotNull(format.formatToPlainText(number)); |
| } catch (TemplateValueFormatException e) { |
| throw _MessageUtil.newCantFormatNumberException(format, exp, e, useTempModelExc); |
| } |
| } |
| |
| /** |
| * Format number with the number format specified as the parameter, with the current locale. |
| * |
| * @param exp |
| * The blamed expression if an error occurs; it's only needed for better error messages |
| */ |
| String formatNumberToPlainText(Number number, BackwardCompatibleTemplateNumberFormat format, Expression exp) |
| throws TemplateModelException, _MiscTemplateException { |
| try { |
| return format.format(number); |
| } catch (UnformattableValueException e) { |
| throw new _MiscTemplateException(exp, e, this, |
| "Failed to format number with ", new _DelayedJQuote(format.getDescription()), ": ", |
| e.getMessage()); |
| } |
| } |
| |
| /** |
| * Returns the current number format ({@link #getNumberFormat()}) as {@link TemplateNumberFormat}. |
| * |
| * <p> |
| * Performance notes: The result is stored for reuse, so calling this method frequently is usually not a problem. |
| * However, at least as of this writing (2.3.24), changing the current locale {@link #setLocale(Locale)} or changing |
| * the current number format ({@link #setNumberFormat(String)}) will drop the stored value, so it will have to be |
| * recalculated. |
| * |
| * @since 2.3.24 |
| */ |
| public TemplateNumberFormat getTemplateNumberFormat() throws TemplateValueFormatException { |
| TemplateNumberFormat format = cachedTemplateNumberFormat; |
| if (format == null) { |
| format = getTemplateNumberFormat(getNumberFormat(), false); |
| cachedTemplateNumberFormat = format; |
| } |
| return format; |
| } |
| |
| /** |
| * Returns the number format as {@link TemplateNumberFormat} for the given format string and the current locale. |
| * (The current locale is the locale returned by {@link #getLocale()}.) Note that the result will be cached in the |
| * {@link Environment} instance (though at least in 2.3.24 the cache will be flushed if the current locale of the |
| * {@link Environment} is changed). |
| * |
| * @param formatString |
| * A string that you could also use as the value of the {@code numberFormat} configuration setting. Can't |
| * be {@code null}. |
| * |
| * @since 2.3.24 |
| */ |
| public TemplateNumberFormat getTemplateNumberFormat(String formatString) throws TemplateValueFormatException { |
| return getTemplateNumberFormat(formatString, true); |
| } |
| |
| /** |
| * Returns the number format as {@link TemplateNumberFormat}, for the given format string and locale. To get a |
| * number format for the current locale, use {@link #getTemplateNumberFormat(String)} instead. |
| * |
| * <p> |
| * Note on performance (which was true at least for 2.3.24): Unless the locale happens to be equal to the current |
| * locale, the {@link Environment}-level format cache can't be used, so the format string has to be parsed and the |
| * matching factory has to be get an invoked, which is much more expensive than getting the format from the cache. |
| * Thus the returned format should be stored by the caller for later reuse (but only within the current thread and |
| * in relation to the current {@link Environment}), if it will be needed frequently. |
| * |
| * @param formatString |
| * A string that you could also use as the value of the {@code numberFormat} configuration setting. |
| * @param locale |
| * The locale of the number format; not {@code null}. |
| * |
| * @since 2.3.24 |
| */ |
| public TemplateNumberFormat getTemplateNumberFormat(String formatString, Locale locale) |
| throws TemplateValueFormatException { |
| if (locale.equals(getLocale())) { |
| getTemplateNumberFormat(formatString); |
| } |
| |
| return getTemplateNumberFormatWithoutCache(formatString, locale); |
| } |
| |
| /** |
| * Convenience wrapper around {@link #getTemplateNumberFormat()} to be called during expression evaluation. |
| */ |
| TemplateNumberFormat getTemplateNumberFormat(Expression exp, boolean useTempModelExc) throws TemplateException { |
| TemplateNumberFormat format; |
| try { |
| format = getTemplateNumberFormat(); |
| } catch (TemplateValueFormatException e) { |
| _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( |
| "Failed to get number format object for the current number format string, ", |
| new _DelayedJQuote(getNumberFormat()), ": ", e.getMessage()) |
| .blame(exp); |
| throw useTempModelExc |
| ? new _TemplateModelException(e, this, desc) : new _MiscTemplateException(e, this, desc); |
| } |
| return format; |
| } |
| |
| /** |
| * Convenience wrapper around {@link #getTemplateNumberFormat(String)} to be called during expression evaluation. |
| * |
| * @param exp |
| * The blamed expression if an error occurs; it's only needed for better error messages |
| */ |
| TemplateNumberFormat getTemplateNumberFormat(String formatString, Expression exp, boolean useTempModelExc) |
| throws TemplateException { |
| TemplateNumberFormat format; |
| try { |
| format = getTemplateNumberFormat(formatString); |
| } catch (TemplateValueFormatException e) { |
| _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( |
| "Failed to get number format object for the ", new _DelayedJQuote(formatString), |
| " number format string: ", e.getMessage()) |
| .blame(exp); |
| throw useTempModelExc |
| ? new _TemplateModelException(e, this, desc) : new _MiscTemplateException(e, this, desc); |
| } |
| return format; |
| } |
| |
| /** |
| * Gets the {@link TemplateNumberFormat} <em>for the current locale</em>. |
| * |
| * @param formatString |
| * Not {@code null} |
| * @param cacheResult |
| * If the results should stored in the {@link Environment}-level cache. It will still try to get the |
| * result from the cache regardless of this parameter. |
| */ |
| private TemplateNumberFormat getTemplateNumberFormat(String formatString, boolean cacheResult) |
| throws TemplateValueFormatException { |
| if (cachedTemplateNumberFormats == null) { |
| if (cacheResult) { |
| cachedTemplateNumberFormats = new HashMap<>(); |
| } |
| } else { |
| TemplateNumberFormat format = cachedTemplateNumberFormats.get(formatString); |
| if (format != null) { |
| return format; |
| } |
| } |
| |
| TemplateNumberFormat format = getTemplateNumberFormatWithoutCache(formatString, getLocale()); |
| |
| if (cacheResult) { |
| cachedTemplateNumberFormats.put(formatString, format); |
| } |
| return format; |
| } |
| |
| static final String COMPUTER_FORMAT_STRING = "computer"; |
| |
| /** |
| * Returns the {@link TemplateNumberFormat} for the given parameters without using the {@link Environment}-level |
| * cache. Of course, the {@link TemplateNumberFormatFactory} involved might still uses its own cache. |
| * |
| * @param formatString |
| * Not {@code null} |
| * @param locale |
| * Not {@code null} |
| */ |
| private TemplateNumberFormat getTemplateNumberFormatWithoutCache(String formatString, Locale locale) |
| throws TemplateValueFormatException { |
| int formatStringLen = formatString.length(); |
| if (formatStringLen > 1 |
| && formatString.charAt(0) == '@' |
| && (isIcI2324OrLater() || hasCustomFormats()) |
| && Character.isLetter(formatString.charAt(1))) { |
| final String name; |
| final String params; |
| { |
| int endIdx; |
| findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) { |
| char c = formatString.charAt(endIdx); |
| if (c == ' ' || c == '_') { |
| break findParamsStart; |
| } |
| } |
| name = formatString.substring(1, endIdx); |
| params = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : ""; |
| } |
| |
| TemplateNumberFormatFactory formatFactory = getCustomNumberFormat(name); |
| if (formatFactory == null) { |
| throw new UndefinedCustomFormatException( |
| "No custom number format was defined with name " + StringUtil.jQuote(name)); |
| } |
| |
| return formatFactory.get(params, locale, this); |
| } else if (formatStringLen >= 1 && formatString.charAt(0) == 'c' |
| && (formatStringLen == 1 || formatString.equals(COMPUTER_FORMAT_STRING))) { |
| return getCTemplateNumberFormatWithPre2331IcIBug(); |
| } else { |
| return JavaTemplateNumberFormatFactory.INSTANCE.get(formatString, locale, this); |
| } |
| } |
| |
| /** |
| * Returns the {@link NumberFormat} used for the {@code c} built-in, except, if |
| * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is less than 2.3.31, |
| * this will wrongly give the format that the {@code c} built-in used before Incompatible Improvements 2.3.21. |
| * See more at {@link Configuration#Configuration(Version)}. |
| * |
| * @deprecated Use {@link #getCTemplateNumberFormat()} instead. This method can't return the format used when |
| * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is 2.3.32, |
| * or greater, and instead it will fall back to return the format that was used for 2.3.31. Also, as its described |
| * earlier, this method was inconsistent with {@code ?c} between Incompatible Improvements 2.3.21 and 2.3.30, while |
| * {@link #getCTemplateNumberFormat()} behaves as {@code ?c} for all Incompatible Improvements value. |
| */ |
| @Deprecated |
| public NumberFormat getCNumberFormat() { |
| if (cNumberFormat == null) { |
| CFormat cFormat = getCFormat(); |
| if (cFormat == LegacyCFormat.INSTANCE && configuration.getIncompatibleImprovements().intValue() < _VersionInts.V_2_3_31) { |
| // Emulate old bug |
| cNumberFormat = ((LegacyCFormat) cFormat).getLegacyNumberFormat(_VersionInts.V_2_3_20); |
| } else { |
| cNumberFormat = cFormat.getLegacyNumberFormat(this); |
| } |
| } |
| return cNumberFormat; |
| } |
| |
| /** |
| * Returns the {@link TemplateNumberFormat} that {@code ?c}/{@code ?cn} uses. |
| * Calling this method for many times is fine, as it internally caches the result object. |
| * Remember that {@link TemplateNumberFormat}-s aren't thread-safe objects, so the resulting object should only |
| * be used in the same thread where this {@link Environment} runs. |
| * |
| * @since 2.3.32 |
| */ |
| public TemplateNumberFormat getCTemplateNumberFormat() { |
| if (cTemplateNumberFormat == null) { |
| cTemplateNumberFormat = getCFormat().getTemplateNumberFormat(this); |
| } |
| return cTemplateNumberFormat; |
| } |
| |
| /** |
| * Like {@link #getCTemplateNumberFormat()}, but emulates the same bug as |
| * {@link #getCNumberFormat()} if a legacy default {@link CFormat} is used. |
| */ |
| private TemplateNumberFormat getCTemplateNumberFormatWithPre2331IcIBug() { |
| if (cTemplateNumberFormatWithPre2331IcIBug == null) { |
| CFormat cFormat = getCFormat(); |
| if (cFormat == LegacyCFormat.INSTANCE && configuration.getIncompatibleImprovements().intValue() < _VersionInts.V_2_3_31) { |
| // Emulate old bug |
| cTemplateNumberFormatWithPre2331IcIBug = ((LegacyCFormat) cFormat).getTemplateNumberFormat(_VersionInts.V_2_3_20); |
| } else { |
| cTemplateNumberFormatWithPre2331IcIBug = cFormat.getTemplateNumberFormat(this); |
| } |
| } |
| return cTemplateNumberFormatWithPre2331IcIBug; |
| } |
| |
| @Override |
| public void setCFormat(CFormat cFormat) { |
| CFormat prevCFormat = getCFormat(); |
| super.setCFormat(cFormat); |
| if (prevCFormat != cFormat) { |
| cTemplateNumberFormat = null; |
| cTemplateNumberFormatWithPre2331IcIBug = null; |
| cNumberFormat = null; |
| if (cachedTemplateNumberFormats != null) { |
| cachedTemplateNumberFormats.remove(C_FORMAT_STRING); |
| cachedTemplateNumberFormats.remove(COMPUTER_FORMAT_STRING); |
| } |
| clearCachedTrueAndFalseString(); |
| } |
| } |
| |
| @Override |
| public void setTimeFormat(String timeFormat) { |
| String prevTimeFormat = getTimeFormat(); |
| super.setTimeFormat(timeFormat); |
| if (!timeFormat.equals(prevTimeFormat)) { |
| if (cachedTempDateFormatArray != null) { |
| for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) { |
| cachedTempDateFormatArray[i + TemplateDateModel.TIME] = null; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void setDateFormat(String dateFormat) { |
| String prevDateFormat = getDateFormat(); |
| super.setDateFormat(dateFormat); |
| if (!dateFormat.equals(prevDateFormat)) { |
| if (cachedTempDateFormatArray != null) { |
| for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) { |
| cachedTempDateFormatArray[i + TemplateDateModel.DATE] = null; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void setDateTimeFormat(String dateTimeFormat) { |
| String prevDateTimeFormat = getDateTimeFormat(); |
| super.setDateTimeFormat(dateTimeFormat); |
| if (!dateTimeFormat.equals(prevDateTimeFormat)) { |
| if (cachedTempDateFormatArray != null) { |
| for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) { |
| cachedTempDateFormatArray[i + TemplateDateModel.DATETIME] = null; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void setBooleanFormat(String booleanFormat) { |
| super.setBooleanFormat(booleanFormat); |
| clearCachedTrueAndFalseString(); |
| } |
| |
| String formatBoolean(boolean value, boolean fallbackToTrueFalse) throws TemplateException { |
| if (value) { |
| String s = getTrueStringValue(); |
| if (s == null) { |
| if (fallbackToTrueFalse) { |
| return MiscUtil.C_TRUE; |
| } else { |
| throw new _MiscTemplateException(getNullBooleanFormatErrorDescription()); |
| } |
| } else { |
| return s; |
| } |
| } else { |
| String s = getFalseStringValue(); |
| if (s == null) { |
| if (fallbackToTrueFalse) { |
| return MiscUtil.C_FALSE; |
| } else { |
| throw new _MiscTemplateException(getNullBooleanFormatErrorDescription()); |
| } |
| } else { |
| return s; |
| } |
| } |
| } |
| |
| private _ErrorDescriptionBuilder getNullBooleanFormatErrorDescription() { |
| return new _ErrorDescriptionBuilder( |
| "Can't convert boolean to string automatically, because the \"", BOOLEAN_FORMAT_KEY ,"\" setting was ", |
| new _DelayedJQuote(getBooleanFormat()), |
| (getBooleanFormat().equals(BOOLEAN_FORMAT_LEGACY_DEFAULT) |
| ? ", which is the legacy deprecated default, and we treat it as if no format was set. " |
| + "This is the default configuration; you should provide the format explicitly for each " |
| + "place where you print a boolean." |
| : ".") |
| ).tips( |
| "Write something like myBool?string('yes', 'no') to specify boolean formatting in place.", |
| new Object[]{ |
| "If you want \"true\"/\"false\" result as you are generating computer-language output " |
| + "(not for direct human consumption), then use \"?c\", like ${myBool?c}. (If you " |
| + "always generate computer-language output, then it's might be reasonable to set " |
| + "the \"", BOOLEAN_FORMAT_KEY, "\" setting to \"c\" instead.)", |
| }, |
| new Object[] { |
| "If you need the same two values on most places, the programmers can set the \"", |
| BOOLEAN_FORMAT_KEY ,"\" setting to something like \"yes,no\". However, then it will be easy to " |
| + "unwillingly format booleans like that." |
| } |
| ); |
| } |
| |
| /** |
| * Returns the string to which {@code true} is converted to for human audience, or {@code null} if automatic |
| * coercion to string is not allowed. |
| * |
| * <p>This value is deduced from the {@code "boolean_format"} setting. |
| * Confusingly, for backward compatibility (at least until 2.4) that defaults to {@code "true,false"}, yet this |
| * defaults to {@code null}. That's so because {@code "true,false"} is treated exceptionally, as that default is a |
| * historical mistake in FreeMarker, since it targets computer language output, not human writing. Thus it's |
| * ignored, and instead we admit that we don't know how to show boolean values. |
| */ |
| String getTrueStringValue() { |
| if (trueAndFalseStringsCachedForParent == getParent()) { |
| return cachedTrueString; |
| } |
| cacheTrueAndFalseStrings(); |
| return cachedTrueString; |
| } |
| |
| /** |
| * Same as {@link #getTrueStringValue()} but with {@code false}. |
| */ |
| String getFalseStringValue() { |
| if (trueAndFalseStringsCachedForParent == getParent()) { |
| return cachedFalseString; |
| } |
| cacheTrueAndFalseStrings(); |
| return cachedFalseString; |
| } |
| |
| private void clearCachedTrueAndFalseString() { |
| trueAndFalseStringsCachedForParent = null; |
| cachedTrueString = null; |
| cachedFalseString = null; |
| } |
| |
| private void cacheTrueAndFalseStrings() { |
| String[] parsedBooleanFormat = parseBooleanFormat(getBooleanFormat()); |
| if (parsedBooleanFormat != null) { |
| if (parsedBooleanFormat.length == 0) { |
| CFormat cFormat = getCFormat(); |
| cachedTrueString = cFormat.getTrueString(); |
| cachedFalseString = cFormat.getFalseString(); |
| } else { |
| cachedTrueString = parsedBooleanFormat[0]; |
| cachedFalseString = parsedBooleanFormat[1]; |
| } |
| } else { |
| // This happens for BOOLEAN_FORMAT_LEGACY_DEFAULT deliberately. That's the default for BC, but it's not a |
| // good default for human audience formatting, so we pretend that it wasn't set. |
| cachedTrueString = null; |
| cachedFalseString = null; |
| } |
| trueAndFalseStringsCachedForParent = getParent(); |
| } |
| |
| public Configuration getConfiguration() { |
| return configuration; |
| } |
| |
| TemplateModel getLastReturnValue() { |
| return lastReturnValue; |
| } |
| |
| void setLastReturnValue(TemplateModel lastReturnValue) { |
| this.lastReturnValue = lastReturnValue; |
| } |
| |
| void clearLastReturnValue() { |
| this.lastReturnValue = null; |
| } |
| |
| /** |
| * @param tdmSourceExpr |
| * The blamed expression if an error occurs; only used for error messages. |
| */ |
| String formatDateToPlainText(TemplateDateModel tdm, Expression tdmSourceExpr, |
| boolean useTempModelExc) throws TemplateException { |
| TemplateDateFormat format = getTemplateDateFormat(tdm, tdmSourceExpr, useTempModelExc); |
| |
| try { |
| return EvalUtil.assertFormatResultNotNull(format.formatToPlainText(tdm)); |
| } catch (TemplateValueFormatException e) { |
| throw _MessageUtil.newCantFormatDateException(format, tdmSourceExpr, e, useTempModelExc); |
| } |
| } |
| |
| /** |
| * @param blamedDateSourceExp |
| * The blamed expression if an error occurs; only used for error messages. |
| * @param blamedFormatterExp |
| * The blamed expression if an error occurs; only used for error messages. |
| */ |
| String formatDateToPlainText(TemplateDateModel tdm, String formatString, |
| Expression blamedDateSourceExp, Expression blamedFormatterExp, |
| boolean useTempModelExc) throws TemplateException { |
| Date date = EvalUtil.modelToDate(tdm, blamedDateSourceExp); |
| |
| TemplateDateFormat format = getTemplateDateFormat( |
| formatString, tdm.getDateType(), date.getClass(), |
| blamedDateSourceExp, blamedFormatterExp, |
| useTempModelExc); |
| |
| try { |
| return EvalUtil.assertFormatResultNotNull(format.formatToPlainText(tdm)); |
| } catch (TemplateValueFormatException e) { |
| throw _MessageUtil.newCantFormatDateException(format, blamedDateSourceExp, e, useTempModelExc); |
| } |
| } |
| |
| /** |
| * Gets a {@link TemplateDateFormat} using the date/time/datetime format settings and the current locale and time |
| * zone. (The current locale is the locale returned by {@link #getLocale()}. The current time zone is |
| * {@link #getTimeZone()} or {@link #getSQLDateAndTimeTimeZone()}). |
| * |
| * @param dateType |
| * The FTL date type; see the similar parameter of |
| * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} |
| * @param dateClass |
| * The exact {@link Date} class, like {@link java.sql.Date} or {@link java.sql.Time}; this can influences |
| * time zone selection. See also: {@link #setSQLDateAndTimeTimeZone(TimeZone)} |
| */ |
| public TemplateDateFormat getTemplateDateFormat(int dateType, Class<? extends Date> dateClass) |
| throws TemplateValueFormatException { |
| boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); |
| return getTemplateDateFormat(dateType, shouldUseSQLDTTimeZone(isSQLDateOrTime), isSQLDateOrTime); |
| } |
| |
| /** |
| * Gets a {@link TemplateDateFormat} for the specified format string and the current locale and time zone. (The |
| * current locale is the locale returned by {@link #getLocale()}. The current time zone is {@link #getTimeZone()} or |
| * {@link #getSQLDateAndTimeTimeZone()}). |
| * |
| * <p> |
| * Note on performance: The result will be cached in the {@link Environment} instance. However, at least in 2.3.24 |
| * the cached entries that depend on the current locale or the current time zone or the current date/time/datetime |
| * format of the {@link Environment} will be lost when those settings are changed. |
| * |
| * @param formatString |
| * Like {@code "iso m"} or {@code "dd.MM.yyyy HH:mm"} or {@code "@somethingCustom"} or |
| * {@code "@somethingCustom params"} |
| * |
| * @since 2.3.24 |
| */ |
| public TemplateDateFormat getTemplateDateFormat( |
| String formatString, int dateType, Class<? extends Date> dateClass) |
| throws TemplateValueFormatException { |
| boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); |
| return getTemplateDateFormat( |
| formatString, dateType, |
| shouldUseSQLDTTimeZone(isSQLDateOrTime), isSQLDateOrTime, true); |
| } |
| |
| /** |
| * Like {@link #getTemplateDateFormat(String, int, Class)}, but allows you to use a different locale than the |
| * current one. If you want to use the current locale, use {@link #getTemplateDateFormat(String, int, Class)} |
| * instead. |
| * |
| * <p> |
| * Performance notes regarding the locale and time zone parameters of |
| * {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} apply. |
| * |
| * @param locale |
| * Can't be {@code null}; See the similar parameter of |
| * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} |
| * |
| * @see #getTemplateDateFormat(String, int, Class) |
| * |
| * @since 2.4 |
| */ |
| public TemplateDateFormat getTemplateDateFormat( |
| String formatString, |
| int dateType, Class<? extends Date> dateClass, |
| Locale locale) |
| throws TemplateValueFormatException { |
| boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); |
| boolean useSQLDTTZ = shouldUseSQLDTTimeZone(isSQLDateOrTime); |
| return getTemplateDateFormat( |
| formatString, |
| dateType, locale, useSQLDTTZ ? getSQLDateAndTimeTimeZone() : getTimeZone(), isSQLDateOrTime); |
| } |
| |
| /** |
| * Like {@link #getTemplateDateFormat(String, int, Class)}, but allows you to use a different locale and time zone |
| * than the current one. If you want to use the current locale and time zone, use |
| * {@link #getTemplateDateFormat(String, int, Class)} instead. |
| * |
| * <p> |
| * Performance notes regarding the locale and time zone parameters of |
| * {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} apply. |
| * |
| * @param timeZone |
| * The {@link TimeZone} used if {@code dateClass} is not an SQL date-only or time-only type. Can't be |
| * {@code null}. |
| * @param sqlDateAndTimeTimeZone |
| * The {@link TimeZone} used if {@code dateClass} is an SQL date-only or time-only type. Can't be |
| * {@code null}. |
| * |
| * @see #getTemplateDateFormat(String, int, Class) |
| * |
| * @since 2.4 |
| */ |
| public TemplateDateFormat getTemplateDateFormat( |
| String formatString, |
| int dateType, Class<? extends Date> dateClass, |
| Locale locale, TimeZone timeZone, TimeZone sqlDateAndTimeTimeZone) |
| throws TemplateValueFormatException { |
| boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); |
| boolean useSQLDTTZ = shouldUseSQLDTTimeZone(isSQLDateOrTime); |
| return getTemplateDateFormat( |
| formatString, |
| dateType, locale, useSQLDTTZ ? sqlDateAndTimeTimeZone : timeZone, isSQLDateOrTime); |
| } |
| |
| /** |
| * Gets a {@link TemplateDateFormat} for the specified parameters. This is mostly meant to be used by |
| * {@link TemplateDateFormatFactory} implementations to delegate to a format based on a specific format string. It |
| * works well for that, as its parameters are the same low level values as the parameters of |
| * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}. For other tasks |
| * consider the other overloads of this method. |
| * |
| * <p> |
| * Note on performance (which was true at least for 2.3.24): Unless the locale happens to be equal to the current |
| * locale and the time zone with one of the current time zones ({@link #getTimeZone()} or |
| * {@link #getSQLDateAndTimeTimeZone()}), the {@link Environment}-level format cache can't be used, so the format |
| * string has to be parsed and the matching factory has to be get an invoked, which is much more expensive than |
| * getting the format from the cache. Thus the returned format should be stored by the caller for later reuse (but |
| * only within the current thread and in relation to the current {@link Environment}), if it will be needed |
| * frequently. |
| * |
| * @param formatString |
| * Like {@code "iso m"} or {@code "dd.MM.yyyy HH:mm"} or {@code "@somethingCustom"} or |
| * {@code "@somethingCustom params"} |
| * @param dateType |
| * The FTL date type; see the similar parameter of |
| * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} |
| * @param timeZone |
| * Not {@code null}; See the similar parameter of |
| * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} |
| * @param locale |
| * Not {@code null}; See the similar parameter of |
| * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} |
| * @param zonelessInput |
| * See the similar parameter of |
| * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} |
| * |
| * @since 2.3.24 |
| */ |
| public TemplateDateFormat getTemplateDateFormat( |
| String formatString, |
| int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput) |
| throws TemplateValueFormatException { |
| Locale currentLocale = getLocale(); |
| if (locale.equals(currentLocale)) { |
| int equalCurrentTZ; |
| TimeZone currentTimeZone = getTimeZone(); |
| if (timeZone.equals(currentTimeZone)) { |
| equalCurrentTZ = 1; |
| } else { |
| TimeZone currentSQLDTTimeZone = getSQLDateAndTimeTimeZone(); |
| if (timeZone.equals(currentSQLDTTimeZone)) { |
| equalCurrentTZ = 2; |
| } else { |
| equalCurrentTZ = 0; |
| } |
| } |
| if (equalCurrentTZ != 0) { |
| return getTemplateDateFormat(formatString, dateType, equalCurrentTZ == 2, zonelessInput, true); |
| } |
| // Falls through |
| } |
| return getTemplateDateFormatWithoutCache(formatString, dateType, locale, timeZone, zonelessInput); |
| } |
| |
| TemplateDateFormat getTemplateDateFormat(TemplateDateModel tdm, Expression tdmSourceExpr, boolean useTempModelExc) |
| throws TemplateModelException, TemplateException { |
| Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr); |
| |
| TemplateDateFormat format = getTemplateDateFormat( |
| tdm.getDateType(), date.getClass(), tdmSourceExpr, |
| useTempModelExc); |
| return format; |
| } |
| |
| /** |
| * Same as {@link #getTemplateDateFormat(int, Class)}, but translates the exceptions to {@link TemplateException}-s. |
| */ |
| TemplateDateFormat getTemplateDateFormat( |
| int dateType, Class<? extends Date> dateClass, Expression blamedDateSourceExp, boolean useTempModelExc) |
| throws TemplateException { |
| try { |
| return getTemplateDateFormat(dateType, dateClass); |
| } catch (UnknownDateTypeFormattingUnsupportedException e) { |
| throw _MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e); |
| } catch (TemplateValueFormatException e) { |
| String settingName; |
| String settingValue; |
| switch (dateType) { |
| case TemplateDateModel.TIME: |
| settingName = Configurable.TIME_FORMAT_KEY; |
| settingValue = getTimeFormat(); |
| break; |
| case TemplateDateModel.DATE: |
| settingName = Configurable.DATE_FORMAT_KEY; |
| settingValue = getDateFormat(); |
| break; |
| case TemplateDateModel.DATETIME: |
| settingName = Configurable.DATETIME_FORMAT_KEY; |
| settingValue = getDateTimeFormat(); |
| break; |
| default: |
| settingName = "???"; |
| settingValue = "???"; |
| } |
| |
| _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( |
| "The value of the \"", settingName, |
| "\" FreeMarker configuration setting is a malformed date/time/datetime format string: ", |
| new _DelayedJQuote(settingValue), ". Reason given: ", |
| e.getMessage()); |
| throw useTempModelExc ? new _TemplateModelException(e, desc) : new _MiscTemplateException(e, desc); |
| } |
| } |
| |
| /** |
| * Same as {@link #getTemplateDateFormat(String, int, Class)}, but translates the exceptions to |
| * {@link TemplateException}-s. |
| */ |
| TemplateDateFormat getTemplateDateFormat( |
| String formatString, int dateType, Class<? extends Date> dateClass, |
| Expression blamedDateSourceExp, Expression blamedFormatterExp, |
| boolean useTempModelExc) |
| throws TemplateException { |
| try { |
| return getTemplateDateFormat(formatString, dateType, dateClass); |
| } catch (UnknownDateTypeFormattingUnsupportedException e) { |
| throw _MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e); |
| } catch (TemplateValueFormatException e) { |
| _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( |
| "Can't create date/time/datetime format based on format string ", |
| new _DelayedJQuote(formatString), ". Reason given: ", |
| e.getMessage()) |
| .blame(blamedFormatterExp); |
| throw useTempModelExc ? new _TemplateModelException(e, desc) : new _MiscTemplateException(e, desc); |
| } |
| } |
| |
| /** |
| * Used to get the {@link TemplateDateFormat} according the date/time/datetime format settings, for the current |
| * locale and time zone. See {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} for the meaning |
| * of some of the parameters. |
| */ |
| private TemplateDateFormat getTemplateDateFormat(int dateType, boolean useSQLDTTZ, boolean zonelessInput) |
| throws TemplateValueFormatException { |
| if (dateType == TemplateDateModel.UNKNOWN) { |
| throw new UnknownDateTypeFormattingUnsupportedException(); |
| } |
| int cacheIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTZ); |
| TemplateDateFormat[] cachedTemplateDateFormats = this.cachedTempDateFormatArray; |
| if (cachedTemplateDateFormats == null) { |
| cachedTemplateDateFormats = new TemplateDateFormat[CACHED_TDFS_LENGTH]; |
| this.cachedTempDateFormatArray = cachedTemplateDateFormats; |
| } |
| TemplateDateFormat format = cachedTemplateDateFormats[cacheIdx]; |
| if (format == null) { |
| final String formatString; |
| switch (dateType) { |
| case TemplateDateModel.TIME: |
| formatString = getTimeFormat(); |
| break; |
| case TemplateDateModel.DATE: |
| formatString = getDateFormat(); |
| break; |
| case TemplateDateModel.DATETIME: |
| formatString = getDateTimeFormat(); |
| break; |
| default: |
| throw new IllegalArgumentException("Invalid date type enum: " + Integer.valueOf(dateType)); |
| } |
| |
| format = getTemplateDateFormat(formatString, dateType, useSQLDTTZ, zonelessInput, false); |
| |
| cachedTemplateDateFormats[cacheIdx] = format; |
| } |
| return format; |
| } |
| |
| /** |
| * Used to get the {@link TemplateDateFormat} for the specified parameters, using the {@link Environment}-level |
| * cache. As the {@link Environment}-level cache currently only stores formats for the current locale and time zone, |
| * there's no parameter to specify those. |
| * |
| * @param cacheResult |
| * If the results should stored in the {@link Environment}-level cache. It will still try to get the |
| * result from the cache regardless of this parameter. |
| */ |
| private TemplateDateFormat getTemplateDateFormat( |
| String formatString, int dateType, boolean useSQLDTTimeZone, boolean zonelessInput, |
| boolean cacheResult) |
| throws TemplateValueFormatException { |
| HashMap<String, TemplateDateFormat> cachedFormatsByFormatString; |
| readFromCache: do { |
| HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray = this.cachedTempDateFormatsByFmtStrArray; |
| if (cachedTempDateFormatsByFmtStrArray == null) { |
| if (cacheResult) { |
| cachedTempDateFormatsByFmtStrArray = new HashMap[CACHED_TDFS_LENGTH]; |
| this.cachedTempDateFormatsByFmtStrArray = cachedTempDateFormatsByFmtStrArray; |
| } else { |
| cachedFormatsByFormatString = null; |
| break readFromCache; |
| } |
| } |
| |
| TemplateDateFormat format; |
| { |
| int cacheArrIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTimeZone); |
| cachedFormatsByFormatString = cachedTempDateFormatsByFmtStrArray[cacheArrIdx]; |
| if (cachedFormatsByFormatString == null) { |
| if (cacheResult) { |
| cachedFormatsByFormatString = new HashMap<>(4); |
| cachedTempDateFormatsByFmtStrArray[cacheArrIdx] = cachedFormatsByFormatString; |
| format = null; |
| } else { |
| break readFromCache; |
| } |
| } else { |
| format = cachedFormatsByFormatString.get(formatString); |
| } |
| } |
| |
| if (format != null) { |
| return format; |
| } |
| // Cache miss; falls through |
| } while (false); |
| |
| TemplateDateFormat format = getTemplateDateFormatWithoutCache( |
| formatString, |
| dateType, getLocale(), useSQLDTTimeZone ? getSQLDateAndTimeTimeZone() : getTimeZone(), |
| zonelessInput); |
| if (cacheResult) { |
| // We know here that cachedFormatsByFormatString != null |
| cachedFormatsByFormatString.put(formatString, format); |
| } |
| return format; |
| } |
| |
| /** |
| * Returns the {@link TemplateDateFormat} for the given parameters without using the {@link Environment}-level |
| * cache. Of course, the {@link TemplateDateFormatFactory} involved might still uses its own cache, which can be |
| * global (class-loader-level) or {@link Environment}-level. |
| * |
| * @param formatString |
| * See the similar parameter of {@link TemplateDateFormatFactory#get} |
| * @param dateType |
| * See the similar parameter of {@link TemplateDateFormatFactory#get} |
| * @param zonelessInput |
| * See the similar parameter of {@link TemplateDateFormatFactory#get} |
| */ |
| private TemplateDateFormat getTemplateDateFormatWithoutCache( |
| String formatString, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput) |
| throws TemplateValueFormatException { |
| final int formatStringLen = formatString.length(); |
| final String formatParams; |
| |
| TemplateDateFormatFactory formatFactory; |
| char firstChar = formatStringLen != 0 ? formatString.charAt(0) : 0; |
| |
| // As of Java 8, 'x' and 'i' (lower case) are illegal date format letters, so this is backward-compatible. |
| if (firstChar == 'x' |
| && formatStringLen > 1 |
| && formatString.charAt(1) == 's') { |
| formatFactory = XSTemplateDateFormatFactory.INSTANCE; |
| formatParams = formatString; // for speed, we don't remove the prefix |
| } else if (firstChar == 'i' |
| && formatStringLen > 2 |
| && formatString.charAt(1) == 's' |
| && formatString.charAt(2) == 'o') { |
| formatFactory = ISOTemplateDateFormatFactory.INSTANCE; |
| formatParams = formatString; // for speed, we don't remove the prefix |
| } else if (firstChar == '@' |
| && formatStringLen > 1 |
| && (isIcI2324OrLater() || hasCustomFormats()) |
| && Character.isLetter(formatString.charAt(1))) { |
| final String name; |
| { |
| int endIdx; |
| findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) { |
| char c = formatString.charAt(endIdx); |
| if (c == ' ' || c == '_') { |
| break findParamsStart; |
| } |
| } |
| name = formatString.substring(1, endIdx); |
| formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : ""; |
| } |
| |
| formatFactory = getCustomDateFormat(name); |
| if (formatFactory == null) { |
| throw new UndefinedCustomFormatException( |
| "No custom date format was defined with name " + StringUtil.jQuote(name)); |
| } |
| } else { |
| formatParams = formatString; |
| formatFactory = JavaTemplateDateFormatFactory.INSTANCE; |
| } |
| |
| return formatFactory.get(formatParams, dateType, locale, timeZone, |
| zonelessInput, this); |
| } |
| |
| boolean shouldUseSQLDTTZ(Class dateClass) { |
| // Attention! If you update this method, update all overloads of it! |
| return dateClass != Date.class // This pre-condition is only for speed |
| && !isSQLDateAndTimeTimeZoneSameAsNormal() |
| && isSQLDateOrTimeClass(dateClass); |
| } |
| |
| private boolean shouldUseSQLDTTimeZone(boolean sqlDateOrTime) { |
| // Attention! If you update this method, update all overloads of it! |
| return sqlDateOrTime && !isSQLDateAndTimeTimeZoneSameAsNormal(); |
| } |
| |
| /** |
| * Tells if the given class is or is subclass of {@link java.sql.Date} or {@link java.sql.Time}. |
| */ |
| private static boolean isSQLDateOrTimeClass(Class dateClass) { |
| // We do shortcuts for the most common cases. |
| return dateClass != java.util.Date.class |
| && (dateClass == java.sql.Date.class || dateClass == Time.class |
| || (dateClass != Timestamp.class |
| && (java.sql.Date.class.isAssignableFrom(dateClass) |
| || Time.class.isAssignableFrom(dateClass)))); |
| } |
| |
| private int getTemplateDateFormatCacheArrayIndex(int dateType, boolean zonelessInput, boolean sqlDTTZ) { |
| return dateType |
| + (zonelessInput ? CACHED_TDFS_ZONELESS_INPUT_OFFS : 0) |
| + (sqlDTTZ ? CACHED_TDFS_SQL_D_T_TZ_OFFS : 0); |
| } |
| |
| /** |
| * Returns the {@link DateToISO8601CalendarFactory} used by the the "iso_" built-ins. Be careful when using this; it |
| * should only by used with |
| * {@link DateUtil#dateToISO8601String(Date, boolean, boolean, boolean, int, TimeZone, DateToISO8601CalendarFactory)} |
| * and {@link DateUtil#dateToXSString(Date, boolean, boolean, boolean, int, TimeZone, DateToISO8601CalendarFactory)} |
| * . |
| */ |
| DateToISO8601CalendarFactory getISOBuiltInCalendarFactory() { |
| if (isoBuiltInCalendarFactory == null) { |
| isoBuiltInCalendarFactory = new DateUtil.TrivialDateToISO8601CalendarFactory(); |
| } |
| return isoBuiltInCalendarFactory; |
| } |
| |
| TemplateTransformModel getTransform(Expression exp) throws TemplateException { |
| TemplateTransformModel ttm = null; |
| TemplateModel tm = exp.eval(this); |
| if (tm instanceof TemplateTransformModel) { |
| ttm = (TemplateTransformModel) tm; |
| } else if (exp instanceof Identifier) { |
| tm = configuration.getSharedVariable(exp.toString()); |
| if (tm instanceof TemplateTransformModel) { |
| ttm = (TemplateTransformModel) tm; |
| } |
| } |
| return ttm; |
| } |
| |
| /** |
| * Returns the loop or macro local variable corresponding to this variable name. |
| * Returns {@code null} if no such variable exists with the given name, or the variable was set to |
| * {@code null}. Doesn't read namespace or global variables. |
| */ |
| public TemplateModel getLocalVariable(String name) throws TemplateModelException { |
| TemplateModel val = getNullableLocalVariable(name); |
| return val != TemplateNullModel.INSTANCE ? val : null; |
| } |
| |
| /** |
| * Similar to {@link #getLocalVariable(String)}, but might return {@link TemplateNullModel}. Only used internally, |
| * as {@link TemplateNullModel} is internal. |
| * |
| * @since 2.3.29 |
| */ |
| private final TemplateModel getNullableLocalVariable(String name) throws TemplateModelException { |
| if (localContextStack != null) { |
| for (int i = localContextStack.size() - 1; i >= 0; i--) { |
| LocalContext lc = localContextStack.get(i); |
| TemplateModel tm = lc.getLocalVariable(name); |
| if (tm != null) { |
| return tm; |
| } |
| } |
| } |
| return currentMacroContext == null ? null : currentMacroContext.getLocalVariable(name); |
| } |
| |
| /** |
| * Returns the variable that is visible in this context, or {@code null} if the variable is not found. This is the |
| * correspondent to an FTL top-level variable reading expression. That is, it tries to find the the variable in this |
| * order: |
| * <ol> |
| * <li>An loop variable (if we're in a loop or user defined directive body) such as foo_has_next |
| * <li>A local variable (if we're in a macro) |
| * <li>A variable defined in the current namespace (say, via <#assign ...>) |
| * <li>A variable defined globally (say, via <#global ....>) |
| * <li>Variable in the data model: |
| * <ol> |
| * <li>A variable in the root hash that was exposed to this rendering environment in the Template.process(...) call |
| * <li>A shared variable set in the configuration via a call to Configuration.setSharedVariable(...) |
| * </ol> |
| * </li> |
| * </ol> |
| */ |
| public TemplateModel getVariable(String name) throws TemplateModelException { |
| TemplateModel result = getNullableLocalVariable(name); |
| if (result != null) { |
| return result != TemplateNullModel.INSTANCE ? result : null; |
| } |
| |
| result = currentNamespace.get(name); |
| if (result != null) { |
| return result; |
| |
| } |
| return getGlobalVariable(name); |
| } |
| |
| /** |
| * Returns the globally visible variable of the given name, or {@code null}. This corresponds to FTL |
| * <code>.globals.<i>name</i></code>. This will first look at variables that were assigned globally via: |
| * {@code <#global ...>} and then at the data model exposed to the template, and then at the |
| * {@linkplain Configuration#setSharedVariables(Map)} shared variables} in the {@link Configuration}. |
| */ |
| public TemplateModel getGlobalVariable(String name) throws TemplateModelException { |
| TemplateModel result = globalNamespace.get(name); |
| if (result != null) { |
| return result; |
| } |
| |
| return getDataModelOrSharedVariable(name); |
| } |
| |
| /** |
| * Returns the variable from the data-model, or if it's not there, then from the |
| * {@linkplain Configuration#setSharedVariables(Map)} shared variables} |
| * |
| * @since 2.3.30 |
| */ |
| public TemplateModel getDataModelOrSharedVariable(String name) throws TemplateModelException { |
| TemplateModel dataModelVal = rootDataModel.get(name); |
| if (dataModelVal != null) { |
| return dataModelVal; |
| } |
| |
| return configuration.getSharedVariable(name); |
| } |
| |
| /** |
| * Sets a variable in the global namespace, like {@code <#global name=value>}. |
| * This can be considered a convenient shorthand for {@code getGlobalNamespace().put(name, model)}. |
| * |
| * <p>Note that this is not an exact pair of {@link #getGlobalVariable(String)}, as that falls back to higher scopes |
| * if the variable is not in the global namespace. |
| * |
| * @param name |
| * The name of the variable. |
| * @param value |
| * The new value of the variable. {@code null} in effect removes the local variable (reading it will fall |
| * back to higher scope). |
| */ |
| public void setGlobalVariable(String name, TemplateModel value) { |
| globalNamespace.put(name, value); |
| } |
| |
| /** |
| * Sets a variable in the current namespace, like {@code <#assign name=value>}. |
| * This can be considered a convenient shorthand for: {@code getCurrentNamespace().put(name, model)}. |
| * |
| * @param name |
| * The name of the variable. |
| * @param value |
| * The new value of the variable. {@code null} in effect removes the local variable (reading it will fall |
| * back to higher scope). |
| */ |
| public void setVariable(String name, TemplateModel value) { |
| currentNamespace.put(name, value); |
| } |
| |
| /** |
| * Sets a local variable that's on the top-level inside a macro or function invocation, like |
| * {@code <#local name=value>}. |
| * Note that just like {@code <#local name=value>}, this will not set loop variables; it will totally ignore |
| * them, and might sets a local variable that a loop variable currently "shadows". As such, it's not exactly the |
| * pair of {@link #getLocalVariable(String)}, which also reads loop variables. |
| * |
| * @param name |
| * The name of the variable. |
| * @param value |
| * The new value of the variable. {@code null} in effect removes the local variable (reading it will fall |
| * back to higher scope). |
| * @throws IllegalStateException |
| * if the environment is not executing a macro body. |
| */ |
| public void setLocalVariable(String name, TemplateModel value) { |
| if (currentMacroContext == null) { |
| throw new IllegalStateException("Not executing macro body"); |
| } |
| currentMacroContext.setLocalVar(name, value); |
| } |
| |
| /** |
| * Returns a set of variable names that are known at the time of call. This includes names of all shared variables |
| * in the {@link Configuration}, names of all global variables that were assigned during the template processing, |
| * names of all variables in the current name-space, names of all local variables and loop variables. If the passed |
| * root data model implements the {@link TemplateHashModelEx} interface, then all names it retrieves through a call |
| * to {@link TemplateHashModelEx#keys()} method are returned as well. The method returns a new Set object on each |
| * call that is completely disconnected from the Environment. That is, modifying the set will have no effect on the |
| * Environment object. |
| */ |
| public Set getKnownVariableNames() throws TemplateModelException { |
| // shared vars. |
| Set set = configuration.getSharedVariableNames(); |
| |
| // root hash |
| if (rootDataModel instanceof TemplateHashModelEx) { |
| TemplateModelIterator rootNames = ((TemplateHashModelEx) rootDataModel).keys().iterator(); |
| while (rootNames.hasNext()) { |
| set.add(((TemplateScalarModel) rootNames.next()).getAsString()); |
| } |
| } |
| |
| // globals |
| for (TemplateModelIterator tmi = globalNamespace.keys().iterator(); tmi.hasNext();) { |
| set.add(((TemplateScalarModel) tmi.next()).getAsString()); |
| } |
| |
| // current name-space |
| for (TemplateModelIterator tmi = currentNamespace.keys().iterator(); tmi.hasNext();) { |
| set.add(((TemplateScalarModel) tmi.next()).getAsString()); |
| } |
| |
| // locals and loop vars |
| if (currentMacroContext != null) { |
| set.addAll(currentMacroContext.getLocalVariableNames()); |
| } |
| if (localContextStack != null) { |
| for (int i = localContextStack.size() - 1; i >= 0; i--) { |
| LocalContext lc = localContextStack.get(i); |
| set.addAll(lc.getLocalVariableNames()); |
| } |
| } |
| return set; |
| } |
| |
| /** |
| * Prints the current FTL stack trace. Useful for debugging. {@link TemplateException}s incorporate this information |
| * in their stack traces. |
| */ |
| public void outputInstructionStack(PrintWriter pw) { |
| outputInstructionStack(getInstructionStackSnapshot(), false, pw); |
| pw.flush(); |
| } |
| |
| private static final int TERSE_MODE_INSTRUCTION_STACK_TRACE_LIMIT = 10; |
| |
| /** |
| * Prints an FTL stack trace based on a stack trace snapshot. |
| * |
| * @param w |
| * If it's a {@link PrintWriter}, {@link PrintWriter#println()} will be used for line-breaks. |
| * @see #getInstructionStackSnapshot() |
| * @since 2.3.21 |
| */ |
| static void outputInstructionStack( |
| TemplateElement[] instructionStackSnapshot, boolean terseMode, Writer w) { |
| final PrintWriter pw = (PrintWriter) (w instanceof PrintWriter ? w : null); |
| try { |
| if (instructionStackSnapshot != null) { |
| final int totalFrames = instructionStackSnapshot.length; |
| int framesToPrint = terseMode |
| ? (totalFrames <= TERSE_MODE_INSTRUCTION_STACK_TRACE_LIMIT |
| ? totalFrames |
| : TERSE_MODE_INSTRUCTION_STACK_TRACE_LIMIT - 1) |
| : totalFrames; |
| boolean hideNestringRelatedFrames = terseMode && framesToPrint < totalFrames; |
| int nestingRelatedFramesHidden = 0; |
| int trailingFramesHidden = 0; |
| int framesPrinted = 0; |
| for (int frameIdx = 0; frameIdx < totalFrames; frameIdx++) { |
| TemplateElement stackEl = instructionStackSnapshot[frameIdx]; |
| final boolean nestingRelatedElement = (frameIdx > 0 && stackEl instanceof BodyInstruction) |
| || (frameIdx > 1 && instructionStackSnapshot[frameIdx - 1] instanceof BodyInstruction); |
| if (framesPrinted < framesToPrint) { |
| if (!nestingRelatedElement || !hideNestringRelatedFrames) { |
| w.write(frameIdx == 0 |
| ? "\t- Failed at: " |
| : (nestingRelatedElement |
| ? "\t~ Reached through: " |
| : "\t- Reached through: ")); |
| w.write(instructionStackItemToString(stackEl)); |
| if (pw != null) pw.println(); |
| else |
| w.write('\n'); |
| framesPrinted++; |
| } else { |
| nestingRelatedFramesHidden++; |
| } |
| } else { |
| trailingFramesHidden++; |
| } |
| } |
| |
| boolean hadClosingNotes = false; |
| if (trailingFramesHidden > 0) { |
| w.write("\t... (Had "); |
| w.write(String.valueOf(trailingFramesHidden + nestingRelatedFramesHidden)); |
| w.write(" more, hidden for tersenes)"); |
| hadClosingNotes = true; |
| } |
| if (nestingRelatedFramesHidden > 0) { |
| if (hadClosingNotes) { |
| w.write(' '); |
| } else { |
| w.write('\t'); |
| } |
| w.write("(Hidden " + nestingRelatedFramesHidden + " \"~\" lines for terseness)"); |
| if (pw != null) pw.println(); |
| else |
| w.write('\n'); |
| hadClosingNotes = true; |
| } |
| if (hadClosingNotes) { |
| if (pw != null) pw.println(); |
| else |
| w.write('\n'); |
| } |
| } else { |
| w.write("(The stack was empty)"); |
| if (pw != null) pw.println(); |
| else |
| w.write('\n'); |
| } |
| } catch (IOException e) { |
| LOG.error("Failed to print FTL stack trace", e); |
| } |
| } |
| |
| /** |
| * Returns the snapshot of what would be printed as FTL stack trace. |
| * |
| * @since 2.3.20 |
| */ |
| TemplateElement[] getInstructionStackSnapshot() { |
| int requiredLength = 0; |
| int ln = instructionStackSize; |
| |
| for (int i = 0; i < ln; i++) { |
| TemplateElement stackEl = instructionStack[i]; |
| if (i == ln - 1 || 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 = instructionStack[i]; |
| if (i == ln - 1 || stackEl.isShownInStackTrace()) { |
| result[dstIdx--] = stackEl; |
| } |
| } |
| |
| return result; |
| } |
| |
| static String instructionStackItemToString(TemplateElement stackEl) { |
| StringBuilder sb = new StringBuilder(); |
| appendInstructionStackItem(stackEl, sb); |
| return sb.toString(); |
| } |
| |
| static void appendInstructionStackItem(TemplateElement stackEl, StringBuilder sb) { |
| sb.append(_MessageUtil.shorten(stackEl.getDescription(), 40)); |
| |
| sb.append(" ["); |
| Macro enclosingMacro = getEnclosingMacro(stackEl); |
| if (enclosingMacro != null) { |
| sb.append(_MessageUtil.formatLocationForEvaluationError( |
| enclosingMacro, stackEl.beginLine, stackEl.beginColumn)); |
| } else { |
| sb.append(_MessageUtil.formatLocationForEvaluationError( |
| stackEl.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.getParentElement(); |
| } |
| return null; |
| } |
| |
| private void pushLocalContext(LocalContext localContext) { |
| if (localContextStack == null) { |
| localContextStack = new LocalContextStack(); |
| } |
| localContextStack.push(localContext); |
| } |
| |
| LocalContextStack 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 loadedLibs.get(name); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the main namespace. This corresponds to the FTL {@code .main} hash. |
| */ |
| public Namespace getMainNamespace() { |
| return mainNamespace; |
| } |
| |
| /** |
| * Returns the current namespace. This corresponds to the FTL {@code .namespace} hash. Initially, the current name |
| * space is the main namespace, but when inside an {@code #import}-ed template, it will change to the namespace of |
| * that import. Note that {@code #include} doesn't affect the namespace, so if you are in an {@code #import}-ed |
| * template and then from there do an {@code #include}, the current namespace will remain the namespace of the |
| * {@code #import}. |
| */ |
| public Namespace getCurrentNamespace() { |
| return currentNamespace; |
| } |
| |
| /** |
| * Returns the name-space that contains the globally visible non-data-model variables (usually created with |
| * {@code <#global ...>}). |
| */ |
| public Namespace getGlobalNamespace() { |
| return globalNamespace; |
| } |
| |
| /** |
| * Returns a view of the data-model (also known as the template context in some other template engines) |
| * that falls back to {@linkplain Configuration#setSharedVariable(String, TemplateModel) shared variables}. |
| */ |
| public TemplateHashModel getDataModel() { |
| return rootDataModel instanceof TemplateHashModelEx |
| ? new TemplateHashModelEx() { |
| @Override |
| public boolean isEmpty() throws TemplateModelException { |
| return false; |
| } |
| |
| @Override |
| public TemplateModel get(String key) throws TemplateModelException { |
| return getDataModelOrSharedVariable(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 |
| @Override |
| public TemplateCollectionModel values() throws TemplateModelException { |
| return ((TemplateHashModelEx) rootDataModel).values(); |
| } |
| |
| @Override |
| public TemplateCollectionModel keys() throws TemplateModelException { |
| return ((TemplateHashModelEx) rootDataModel).keys(); |
| } |
| |
| @Override |
| public int size() throws TemplateModelException { |
| return ((TemplateHashModelEx) rootDataModel).size(); |
| } |
| } |
| : new TemplateHashModel() { |
| @Override |
| public boolean isEmpty() { |
| return false; |
| } |
| |
| @Override |
| public TemplateModel get(String key) throws TemplateModelException { |
| TemplateModel value = rootDataModel.get(key); |
| return value != null ? value : configuration.getSharedVariable(key); |
| } |
| }; |
| } |
| |
| /** |
| * 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() { |
| |
| @Override |
| public boolean isEmpty() { |
| return false; |
| } |
| |
| @Override |
| public TemplateModel get(String key) throws TemplateModelException { |
| TemplateModel result = globalNamespace.get(key); |
| if (result == null) { |
| result = rootDataModel.get(key); |
| } |
| if (result == null) { |
| result = configuration.getSharedVariable(key); |
| } |
| return result; |
| } |
| }; |
| } |
| |
| /** |
| * Sets the {@link TemplateProcessingTracer} to use for this {@link Environment}; |
| * can be {@code null} to not have one. The default is also {@code null}. |
| * |
| * @since 2.3.33 |
| */ |
| public void setTemplateProcessingTracer(TemplateProcessingTracer templateProcessingTracer) { |
| this.templateProcessingTracer = templateProcessingTracer; |
| } |
| |
| /** |
| * Getter pair of {@link #setTemplateProcessingTracer(TemplateProcessingTracer)}. Can be {@code null}. |
| * |
| * @since 2.3.33 |
| */ |
| public TemplateProcessingTracer getTemplateProcessingTracer() { |
| return templateProcessingTracer; |
| } |
| |
| private void pushElement(TemplateElement element) { |
| final int newSize = ++instructionStackSize; |
| TemplateElement[] instructionStack = this.instructionStack; |
| if (newSize > instructionStack.length) { |
| final TemplateElement[] newInstructionStack = new TemplateElement[newSize * 2]; |
| for (int i = 0; i < instructionStack.length; i++) { |
| newInstructionStack[i] = instructionStack[i]; |
| } |
| instructionStack = newInstructionStack; |
| this.instructionStack = instructionStack; |
| } |
| instructionStack[newSize - 1] = element; |
| if (templateProcessingTracer != null) { |
| templateProcessingTracer.enterElement(this, element); |
| } |
| } |
| |
| private void popElement() { |
| if (templateProcessingTracer != null) { |
| TemplateElement element = instructionStack[instructionStackSize - 1]; |
| templateProcessingTracer.exitElement(this); |
| } |
| instructionStackSize--; |
| } |
| |
| void replaceElementStackTop(TemplateElement instr) { |
| instructionStack[instructionStackSize - 1] = instr; |
| } |
| |
| public TemplateNodeModel getCurrentVisitorNode() { |
| return currentVisitorNode; |
| } |
| |
| /** |
| * sets TemplateNodeModel as the current visitor node. {@code .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; |
| int size = nodeNamespaces.size(); |
| for (i = startIndex; i < 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 template root relative. |
| * |
| * <p> |
| * It's the same as <code>include(getTemplateForInclusion(name, encoding, parse))</code>. But, you may want to |
| * separately call these two methods, so you can determine the source of exceptions more precisely, and thus achieve |
| * more intelligent error handling. |
| * |
| * @see #getTemplateForInclusion(String name, String encoding, boolean parse) |
| * @see #include(Template includedTemplate) |
| */ |
| public void include(String name, String encoding, boolean parse) |
| throws IOException, TemplateException { |
| include(getTemplateForInclusion(name, encoding, parse)); |
| } |
| |
| /** |
| * Same as {@link #getTemplateForInclusion(String, String, boolean, boolean)} with {@code false} |
| * {@code ignoreMissing} argument. |
| */ |
| public Template getTemplateForInclusion(String name, String encoding, boolean parse) |
| throws IOException { |
| return getTemplateForInclusion(name, encoding, parse, false); |
| } |
| |
| /** |
| * Gets a template for inclusion; used for implementing {@link #include(Template includedTemplate)}. The advantage |
| * over simply using <code>config.getTemplate(...)</code> is that it chooses the default encoding exactly as the |
| * <code>include</code> directive does, although that encoding selection mechanism is a historical baggage and |
| * considered to be harmful. |
| * |
| * @param name |
| * the name of the template, relatively to the template root directory (not the to the directory of the |
| * currently executing template file). (Note that you can use |
| * {@link freemarker.cache.TemplateCache#getFullTemplatePath} to convert paths to template root relative |
| * paths.) For more details see the identical parameter of |
| * {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)} |
| * |
| * @param encoding |
| * the charset of the obtained template. If {@code null}, the encoding of the top template that is |
| * currently being processed in this {@link Environment} is used, which can lead to odd situations, so |
| * using {@code null} is not recommended. In most applications, the value of |
| * {@link Configuration#getEncoding(Locale)} (or {@link Configuration#getDefaultEncoding()}) should be |
| * used here. |
| * |
| * @param parseAsFTL |
| * See identical parameter of {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)} |
| * |
| * @param ignoreMissing |
| * See identical parameter of {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)} |
| * |
| * @return Same as {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)} |
| * @throws IOException |
| * Same as exceptions thrown by |
| * {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)} |
| * |
| * @since 2.3.21 |
| */ |
| public Template getTemplateForInclusion(String name, String encoding, boolean parseAsFTL, boolean ignoreMissing) |
| throws IOException { |
| return configuration.getTemplate( |
| name, getLocale(), getIncludedTemplateCustomLookupCondition(), |
| encoding != null ? encoding : getIncludedTemplateEncoding(), |
| parseAsFTL, |
| ignoreMissing); |
| } |
| |
| private Object getIncludedTemplateCustomLookupCondition() { |
| return getTemplate().getCustomLookupCondition(); |
| } |
| |
| private String getIncludedTemplateEncoding() { |
| String encoding; |
| // This branch shouldn't exist, as it doesn't make much sense to inherit encoding. But we have to keep BC. |
| encoding = getTemplate().getEncoding(); |
| if (encoding == null) { |
| encoding = configuration.getEncoding(this.getLocale()); |
| } |
| return encoding; |
| } |
| |
| /** |
| * Processes a Template in the context of this <code>Environment</code>, including its output in the |
| * <code>Environment</code>'s Writer. |
| * |
| * @param includedTemplate |
| * the template to process. Note that it does <em>not</em> need to be a template returned by |
| * {@link #getTemplateForInclusion(String name, String encoding, boolean parse)}. |
| */ |
| public void include(Template includedTemplate) |
| throws TemplateException, IOException { |
| final Template prevTemplate; |
| final boolean parentReplacementOn = isBeforeIcI2322(); |
| prevTemplate = getTemplate(); |
| if (parentReplacementOn) { |
| setParent(includedTemplate); |
| } else { |
| legacyParent = includedTemplate; |
| } |
| |
| importMacros(includedTemplate); |
| try { |
| visit(includedTemplate.getRootTreeNode()); |
| } finally { |
| if (parentReplacementOn) { |
| setParent(prevTemplate); |
| } else { |
| legacyParent = prevTemplate; |
| } |
| } |
| } |
| |
| /** |
| * Emulates <code>import</code> directive, except that <code>templateName</code> must be template root relative. |
| * |
| * <p> |
| * It's the same as <code>importLib(getTemplateForImporting(templateName), 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. |
| * |
| * <p> |
| * If it will be a lazy or an eager import is decided by the value of {@link #getLazyImports()}. You |
| * can also directly control that aspect by using {@link #importLib(String, String, boolean)} instead. |
| * |
| * @return Not {@code null}. This is possibly a lazily self-initializing namespace, which means that it will only |
| * try to get and process the imported template when you access its content. |
| * |
| * @see #getTemplateForImporting(String templateName) |
| * @see #importLib(Template includedTemplate, String namespaceVarName) |
| * @see #importLib(String, String, boolean) |
| */ |
| public Namespace importLib(String templateName, String targetNsVarName) |
| throws IOException, TemplateException { |
| return importLib(templateName, targetNsVarName, getLazyImports()); |
| } |
| |
| /** |
| * Does what the <code>#import</code> directive does, but with an already loaded template. |
| * |
| * @param loadedTemplate |
| * The template to import. Note that it does <em>not</em> need to be a template returned by |
| * {@link #getTemplateForImporting(String name)}. Not {@code null}. |
| * @param targetNsVarName |
| * The name of the FTL variable that will store the namespace. If {@code null}, the namespace |
| * won't be stored in a variable (but it's still returned). |
| * |
| * @return The namespace of the imported template, already initialized. |
| * |
| * @see #getTemplateForImporting(String name) |
| * @see #importLib(Template includedTemplate, String namespaceVarName) |
| */ |
| public Namespace importLib(Template loadedTemplate, String targetNsVarName) |
| throws IOException, TemplateException { |
| return importLib(null, loadedTemplate, targetNsVarName); |
| } |
| |
| /** |
| * Like {@link #importLib(String, String)}, but you can specify if you want a |
| * {@linkplain #setLazyImports(boolean) lazy import} or not. |
| * |
| * @return Not {@code null}. This is possibly a lazily self-initializing namespace, which mean that it will only try |
| * to get and process the imported template when you access its content. |
| * |
| * @since 2.3.25 |
| */ |
| public Namespace importLib(String templateName, String targetNsVarName, boolean lazy) |
| throws IOException, TemplateException { |
| return lazy |
| ? importLib(templateName, null, targetNsVarName) |
| : importLib(null, getTemplateForImporting(templateName), targetNsVarName); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * @param templateName |
| * Ignored if {@code loadedTemaplate} is set (so we do eager import), otherwise it can't be {@code null}. |
| * Assumed to be template root directory relative (not relative to the current template). |
| * @param loadedTemplate |
| * {@code null} exactly if we want a lazy import |
| */ |
| private Namespace importLib( |
| String templateName, final Template loadedTemplate, final String targetNsVarName) |
| throws IOException, TemplateException { |
| final boolean lazyImport; |
| if (loadedTemplate != null) { |
| lazyImport = false; |
| // As we have an already normalized name, we use it. 2.3.x note: We should use the template.sourceName as |
| // namespace key, but historically we use the looked up name (template.name); check what lazy import does if |
| // that will be fixed, as that can't do the template lookup, yet the keys must be the same. |
| templateName = loadedTemplate.getName(); |
| } else { |
| lazyImport = true; |
| // We can't cause a template lookup here (see TemplateLookupStrategy), as that can be expensive. We exploit |
| // that (at least in 2.3.x) the name used for eager import namespace key isn't the template.sourceName, but |
| // the looked up name (template.name), which we can get quickly: |
| TemplateNameFormat tnf = getConfiguration().getTemplateNameFormat(); |
| templateName = _CacheAPI.normalizeRootBasedName(tnf, templateName); |
| } |
| |
| if (loadedLibs == null) { |
| loadedLibs = new HashMap(); |
| } |
| final Namespace existingNamespace = loadedLibs.get(templateName); |
| if (existingNamespace != null) { |
| if (targetNsVarName != null) { |
| setVariable(targetNsVarName, existingNamespace); |
| if (isIcI2324OrLater() && currentNamespace == mainNamespace) { |
| globalNamespace.put(targetNsVarName, existingNamespace); |
| } |
| } |
| if (!lazyImport && existingNamespace instanceof LazilyInitializedNamespace) { |
| ((LazilyInitializedNamespace) existingNamespace).ensureInitializedTME(); |
| } |
| } else { |
| final Namespace newNamespace |
| = lazyImport ? new LazilyInitializedNamespace(templateName) : new Namespace(loadedTemplate); |
| loadedLibs.put(templateName, newNamespace); |
| |
| if (targetNsVarName != null) { |
| setVariable(targetNsVarName, newNamespace); |
| if (currentNamespace == mainNamespace) { |
| globalNamespace.put(targetNsVarName, newNamespace); |
| } |
| } |
| |
| if (!lazyImport) { |
| initializeImportLibNamespace(newNamespace, loadedTemplate); |
| } |
| } |
| return loadedLibs.get(templateName); |
| } |
| |
| private void initializeImportLibNamespace(final Namespace newNamespace, Template loadedTemplate) |
| throws TemplateException, IOException { |
| Namespace prevNamespace = this.currentNamespace; |
| this.currentNamespace = newNamespace; |
| Writer prevOut = out; |
| this.out = NullWriter.INSTANCE; |
| try { |
| include(loadedTemplate); |
| } finally { |
| this.out = prevOut; |
| this.currentNamespace = prevNamespace; |
| } |
| } |
| |
| /** |
| * Resolves a reference to a template (like the one used in {@code #include} or {@code #import}), assuming a base |
| * name. This gives a root based, even if non-normalized and possibly non-absolute (but then relative to the root) |
| * template name, that could be used for {@link Configuration#getTemplate(String)}. This is mostly used when a |
| * template refers to another template. |
| * <p> |
| * If you need to guarantee that the result is also an absolute path, then apply |
| * {@link #rootBasedToAbsoluteTemplateName(String)} on it. |
| * |
| * @param baseName |
| * The name to which relative {@code targetName}-s are relative to. Maybe {@code null} (happens when |
| * resolving names in nameless templates), which means that the base is the root "directory", and so the |
| * {@code targetName} is returned without change. Assuming {@link TemplateNameFormat#DEFAULT_2_3_0} or |
| * {@link TemplateNameFormat#DEFAULT_2_4_0}, the rules are as follows. If you want to specify a base |
| * directory here, it must end with {@code "/"}. If it doesn't end with {@code "/"}, it's parent |
| * directory will be used as the base path. Might starts with a scheme part (like {@code "foo://"}, or |
| * with {@link TemplateNameFormat#DEFAULT_2_4_0} even just with {@code "foo:"}). |
| * @param targetName |
| * The name of the template, which is either a relative or absolute name. Assuming |
| * {@link TemplateNameFormat#DEFAULT_2_3_0} or {@link TemplateNameFormat#DEFAULT_2_4_0}, the rules are as |
| * follows. If it starts with {@code "/"} or contains a scheme part separator ({@code "://"}, also, with |
| * {@link TemplateNameFormat#DEFAULT_2_4_0} a {@code ":"} with no {@code "/"} anywhere before it) then |
| * it's an absolute name, otherwise it's a relative path. Relative paths are interpreted relatively to |
| * the {@code baseName}. Absolute names are simply returned as is, ignoring the {@code baseName}, except, |
| * when the {@code baseName} has scheme part while the {@code targetName} doesn't have, then the schema |
| * of the {@code baseName} is prepended to the {@code targetName}. |
| * |
| * @since 2.3.22 |
| */ |
| public String toFullTemplateName(String baseName, String targetName) |
| throws MalformedTemplateNameException { |
| if (isClassicCompatible() /* FM1 only had absolute names */ || baseName == null) { |
| return targetName; |
| } |
| |
| return _CacheAPI.toRootBasedName(configuration.getTemplateNameFormat(), baseName, targetName); |
| } |
| |
| /** |
| * Converts a root based name (a name that's either relative to the root, or is absolute), which are typically used |
| * by the API (such as for {@link Configuration#getTemplate(String)}), to an absolute name, which can be safely |
| * passed to {@code <#include path>} and such, as it won't be misinterpreted to be relative to the directory of the |
| * template. For example, {@code "foo/bar.ftl"} is converted to {@code "/foo/bar.ftl"}, while {@code "/foo/bar"} or |
| * {@code "foo://bar/baz"} remains as is, as they are already absolute names (see {@link TemplateNameFormat} for |
| * more about the format of names). |
| * |
| * <p> |
| * You only need this if the template name will be passed to {@code <#include name>}, {@code <#import name>}, |
| * {@code .get_optional_template(name)} or a similar construct in a template, otherwise using non-absolute root |
| * based names is fine. |
| * |
| * @since 2.3.28 |
| */ |
| public String rootBasedToAbsoluteTemplateName(String rootBasedName) throws MalformedTemplateNameException { |
| return _CacheAPI.rootBasedNameToAbsoluteName(configuration.getTemplateNameFormat(), rootBasedName); |
| } |
| |
| 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 IdentityHashMap<Object, Object> customStateVariables; |
| |
| /** |
| * Returns the value of a custom state variable, or {@code null} if it's missing; see |
| * {@link #setCustomState(Object, Object)} for more. |
| * |
| * @since 2.3.24 |
| */ |
| public Object getCustomState(Object identityKey) { |
| if (customStateVariables == null) { |
| return null; |
| } |
| return customStateVariables.get(identityKey); |
| } |
| |
| /** |
| * Sets the value of a custom state variable. Custom state variables meant to be used by |
| * {@link TemplateNumberFormatFactory}-es, {@link TemplateDateFormatFactory}-es, and similar user-implementable, |
| * pluggable objects, which want to maintain an {@link Environment}-scoped state (such as a cache). |
| * |
| * @param identityKey |
| * The key that identifies the variable, by its object identity (not by {@link Object#equals(Object)}). |
| * This should be something like a {@code private static final Object CUSTOM_STATE_KEY = new Object();} |
| * in the class that needs this state variable. |
| * @param value |
| * The value of the variable. Can be anything, even {@code null}. |
| * |
| * @return The previous value of the variable, or {@code null} if the variable didn't exist. |
| * |
| * @since 2.3.24 |
| */ |
| public Object setCustomState(Object identityKey, Object value) { |
| IdentityHashMap<Object, Object> customStateVariables = this.customStateVariables; |
| if (customStateVariables == null) { |
| customStateVariables = new IdentityHashMap<>(); |
| this.customStateVariables = customStateVariables; |
| } |
| return customStateVariables.put(identityKey, value); |
| } |
| |
| final class NestedElementTemplateDirectiveBody implements TemplateDirectiveBody { |
| |
| private final TemplateElement[] childBuffer; |
| |
| private NestedElementTemplateDirectiveBody(TemplateElement[] childBuffer) { |
| this.childBuffer = childBuffer; |
| } |
| |
| @Override |
| public void render(Writer newOut) throws TemplateException, IOException { |
| Writer prevOut = out; |
| out = newOut; |
| try { |
| visit(childBuffer); |
| } finally { |
| out = prevOut; |
| } |
| } |
| |
| TemplateElement[] getChildrenBuffer() { |
| return childBuffer; |
| } |
| |
| } |
| |
| public class Namespace extends SimpleHash { |
| |
| private Template template; |
| |
| Namespace() { |
| super(_ObjectWrappers.SAFE_OBJECT_WRAPPER); |
| this.template = Environment.this.getTemplate(); |
| } |
| |
| Namespace(Template template) { |
| super(_ObjectWrappers.SAFE_OBJECT_WRAPPER); |
| this.template = template; |
| } |
| |
| /** |
| * @return the Template object with which this Namespace is associated. |
| */ |
| public Template getTemplate() { |
| return template == null ? Environment.this.getTemplate() : template; |
| } |
| |
| void setTemplate(Template template) { |
| this.template = template; |
| } |
| |
| } |
| |
| private enum InitializationStatus { |
| UNINITIALIZED, INITIALIZING, INITIALIZED, FAILED |
| } |
| |
| class LazilyInitializedNamespace extends Namespace { |
| |
| private final String templateName; |
| private final Locale locale; |
| private final String encoding; |
| private final Object customLookupCondition; |
| |
| private InitializationStatus status = InitializationStatus.UNINITIALIZED; |
| |
| /** |
| * @param templateName |
| * Must be root relative |
| */ |
| private LazilyInitializedNamespace(String templateName) { |
| super(null); |
| |
| this.templateName = templateName; |
| // Make snapshot of all settings that influence template resolution: |
| this.locale = getLocale(); |
| this.encoding = getIncludedTemplateEncoding(); |
| this.customLookupCondition = getIncludedTemplateCustomLookupCondition(); |
| } |
| |
| private void ensureInitializedTME() throws TemplateModelException { |
| if (status != InitializationStatus.INITIALIZED && status != InitializationStatus.INITIALIZING) { |
| if (status == InitializationStatus.FAILED) { |
| throw new TemplateModelException( |
| "Lazy initialization of the imported namespace for " |
| + StringUtil.jQuote(templateName) |
| + " has already failed earlier; won't retry it."); |
| } |
| try { |
| status = InitializationStatus.INITIALIZING; |
| initialize(); |
| status = InitializationStatus.INITIALIZED; |
| } catch (Exception e) { |
| // [FM3] Rethrow TemplateException-s as is |
| throw new TemplateModelException( |
| "Lazy initialization of the imported namespace for " |
| + StringUtil.jQuote(templateName) |
| + " has failed; see cause exception", e); |
| } finally { |
| if (status != InitializationStatus.INITIALIZED) { |
| status = InitializationStatus.FAILED; |
| } |
| } |
| } |
| } |
| |
| private void ensureInitializedRTE() { |
| try { |
| ensureInitializedTME(); |
| } catch (TemplateModelException e) { |
| throw new RuntimeException(e.getMessage(), e.getCause()); |
| } |
| } |
| |
| private void initialize() throws IOException, TemplateException { |
| setTemplate(configuration.getTemplate( |
| templateName, locale, customLookupCondition, encoding, |
| true, false)); |
| Locale lastLocale = getLocale(); |
| try { |
| setLocale(locale); |
| initializeImportLibNamespace(this, getTemplate()); |
| } finally { |
| setLocale(lastLocale); |
| } |
| } |
| |
| @Override |
| protected Map copyMap(Map map) { |
| ensureInitializedRTE(); |
| return super.copyMap(map); |
| } |
| |
| @Override |
| public Template getTemplate() { |
| ensureInitializedRTE(); |
| return super.getTemplate(); |
| } |
| |
| @Override |
| public void put(String key, Object value) { |
| ensureInitializedRTE(); |
| super.put(key, value); |
| } |
| |
| @Override |
| public void put(String key, boolean b) { |
| ensureInitializedRTE(); |
| super.put(key, b); |
| } |
| |
| @Override |
| public TemplateModel get(String key) throws TemplateModelException { |
| ensureInitializedTME(); |
| return super.get(key); |
| } |
| |
| @Override |
| public boolean containsKey(String key) { |
| ensureInitializedRTE(); |
| return super.containsKey(key); |
| } |
| |
| @Override |
| public void remove(String key) { |
| ensureInitializedRTE(); |
| super.remove(key); |
| } |
| |
| @Override |
| public void putAll(Map m) { |
| ensureInitializedRTE(); |
| super.putAll(m); |
| } |
| |
| @Override |
| public Map toMap() throws TemplateModelException { |
| ensureInitializedTME(); |
| return super.toMap(); |
| } |
| |
| @Override |
| public String toString() { |
| ensureInitializedRTE(); |
| return super.toString(); |
| } |
| |
| @Override |
| public int size() { |
| ensureInitializedRTE(); |
| return super.size(); |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| ensureInitializedRTE(); |
| return super.isEmpty(); |
| } |
| |
| @Override |
| public TemplateCollectionModel keys() { |
| ensureInitializedRTE(); |
| return super.keys(); |
| } |
| |
| @Override |
| public TemplateCollectionModel values() { |
| ensureInitializedRTE(); |
| return super.values(); |
| } |
| |
| @Override |
| public KeyValuePairIterator keyValuePairIterator() { |
| ensureInitializedRTE(); |
| return super.keyValuePairIterator(); |
| } |
| |
| |
| } |
| |
| private static final Writer EMPTY_BODY_WRITER = new Writer() { |
| |
| @Override |
| public void write(char[] cbuf, int off, int len) throws IOException { |
| if (len > 0) { |
| throw new IOException( |
| "This transform does not allow nested content."); |
| } |
| } |
| |
| @Override |
| public void flush() { |
| } |
| |
| @Override |
| public void close() { |
| } |
| }; |
| |
| private boolean isBeforeIcI2322() { |
| return configuration.getIncompatibleImprovements().intValue() < _VersionInts.V_2_3_22; |
| } |
| |
| boolean isIcI2324OrLater() { |
| return configuration.getIncompatibleImprovements().intValue() >= _VersionInts.V_2_3_24; |
| } |
| |
| /** |
| * See {@link #setFastInvalidReferenceExceptions(boolean)}. |
| */ |
| boolean getFastInvalidReferenceExceptions() { |
| return fastInvalidReferenceExceptions; |
| } |
| |
| /** |
| * Sets if for invalid references {@link InvalidReferenceException#FAST_INSTANCE} should be thrown, or a new |
| * {@link InvalidReferenceException}. The "fast" instance is used if we know that the error will be handled so that |
| * its message will not be logged or shown anywhere. |
| */ |
| boolean setFastInvalidReferenceExceptions(boolean b) { |
| boolean res = fastInvalidReferenceExceptions; |
| fastInvalidReferenceExceptions = b; |
| return res; |
| } |
| |
| } |