http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java new file mode 100644 index 0000000..6713200 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java @@ -0,0 +1,3213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.sql.Time; +import java.sql.Timestamp; +import java.text.Collator; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateDirectiveBody; +import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateHashModelEx; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateModelIterator; +import org.apache.freemarker.core.model.TemplateNodeModel; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.TemplateTransformModel; +import org.apache.freemarker.core.model.TransformControl; +import org.apache.freemarker.core.model.impl.SimpleHash; +import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; +import org.apache.freemarker.core.templateresolver.TemplateNameFormat; +import org.apache.freemarker.core.templateresolver.TemplateResolver; +import org.apache.freemarker.core.templateresolver._CacheAPI; +import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat; +import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2; +import org.apache.freemarker.core.util.UndeclaredThrowableException; +import org.apache.freemarker.core.util._DateUtil; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; +import org.apache.freemarker.core.util._NullWriter; +import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateNumberFormat; +import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateValueFormatException; +import org.apache.freemarker.core.valueformat.UndefinedCustomFormatException; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; +import org.apache.freemarker.core.valueformat.impl.ISOTemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.impl.JavaTemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.impl.JavaTemplateNumberFormatFactory; +import org.apache.freemarker.core.valueformat.impl.XSTemplateDateFormatFactory; +import org.slf4j.Logger; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Object that represents the runtime environment during template processing. For every invocation of a + * <tt>Template.process()</tt> method, a new instance of this object is created, and then discarded when + * <tt>process()</tt> returns. This object stores the set of temporary variables created by the template, the value of + * settings set by the template, the reference to the data model root, etc. Everything that is needed to fulfill the + * template processing job. + * + * <p> + * Data models that need to access the <tt>Environment</tt> object that represents the template processing on the + * current thread can use the {@link #getCurrentEnvironment()} method. + * + * <p> + * If you need to modify or read this object before or after the <tt>process</tt> call, use + * {@link Template#createProcessingEnvironment(Object rootMap, Writer out, ObjectWrapper wrapper)} + */ +public final class Environment extends MutableProcessingConfiguration<Environment> implements CustomStateScope { + + private static final ThreadLocal<Environment> TLS_ENVIRONMENT = new ThreadLocal(); + + private static final Logger LOG = _CoreLogs.RUNTIME; + private static final Logger LOG_ATTEMPT = _CoreLogs.ATTEMPT; + + // Do not use this object directly; deepClone it first! DecimalFormat isn't + // thread-safe. + private static final DecimalFormat C_NUMBER_FORMAT = new DecimalFormat( + "0.################", + new DecimalFormatSymbols(Locale.US)); + + static { + C_NUMBER_FORMAT.setGroupingUsed(false); + C_NUMBER_FORMAT.setDecimalSeparatorAlwaysShown(false); + } + + private final Configuration configuration; + private final TemplateHashModel rootDataModel; + private ASTElement[] instructionStack = new ASTElement[16]; + private int instructionStackSize = 0; + private final ArrayList recoveredErrorStack = new ArrayList(); + + private TemplateNumberFormat cachedTemplateNumberFormat; + private Map<String, TemplateNumberFormat> cachedTemplateNumberFormats; + private Map<CustomStateKey, Object> customStateMap; + + private TemplateBooleanFormat cachedTemplateBooleanFormat; + + /** + * Stores the date/time/date-time formatters that are used when no format is explicitly given at the place of + * formatting. That is, in situations like ${lastModified} or even ${lastModified?date}, but not in situations like + * ${lastModified?string.iso}. + * + * <p> + * The index of the array is calculated from what kind of formatter we want (see + * {@link #getTemplateDateFormatCacheArrayIndex(int, boolean, boolean)}):<br> + * Zoned input: 0: U, 1: T, 2: D, 3: DT<br> + * Zoneless input: 4: U, 5: T, 6: D, 7: DT<br> + * SQL D T TZ + Zoned input: 8: U, 9: T, 10: D, 11: DT<br> + * SQL D T TZ + Zoneless input: 12: U, 13: T, 14: D, 15: DT + * + * <p> + * This is a lazily filled cache. It starts out as {@code null}, then when first needed the array will be created. + * The array elements also start out as {@code null}-s, and they are filled as the particular kind of formatter is + * first needed. + */ + private TemplateDateFormat[] cachedTempDateFormatArray; + /** Similar to {@link #cachedTempDateFormatArray}, but used when a formatting string was specified. */ + private HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray; + private static final int CACHED_TDFS_ZONELESS_INPUT_OFFS = 4; + private static final int CACHED_TDFS_SQL_D_T_TZ_OFFS = CACHED_TDFS_ZONELESS_INPUT_OFFS * 2; + private static final int CACHED_TDFS_LENGTH = CACHED_TDFS_SQL_D_T_TZ_OFFS * 2; + + /** Caches the result of {@link #isSQLDateAndTimeTimeZoneSameAsNormal()}. */ + private Boolean cachedSQLDateAndTimeTimeZoneSameAsNormal; + + private NumberFormat cNumberFormat; + + /** + * Used by the "iso_" built-ins to accelerate formatting. + * + * @see #getISOBuiltInCalendarFactory() + */ + private DateToISO8601CalendarFactory isoBuiltInCalendarFactory; + + private Collator cachedCollator; + + private Writer out; + private ASTDirMacro.Context currentMacroContext; + private LocalContextStack localContextStack; + private final Template mainTemplate; + private final Namespace mainNamespace; + private Namespace currentNamespace, globalNamespace; + private HashMap<String, Namespace> loadedLibs; + + private boolean inAttemptBlock; + private Throwable lastThrowable; + + private TemplateModel lastReturnValue; + private HashMap macroToNamespaceLookup = new HashMap(); + + private TemplateNodeModel currentVisitorNode; + private TemplateSequenceModel nodeNamespaces; + // Things we keep track of for the fallback mechanism. + private int nodeNamespaceIndex; + private String currentNodeName, currentNodeNS; + + private Charset cachedURLEscapingCharset; + private boolean cachedURLEscapingCharsetSet; + + private boolean fastInvalidReferenceExceptions; + + /** + * Retrieves the environment object associated with the current thread, or {@code null} if there's no template + * processing going on in this thread. Data model implementations that need access to the environment can call this + * method to obtain the environment object that represents the template processing that is currently running on the + * current thread. + */ + public static Environment getCurrentEnvironment() { + return TLS_ENVIRONMENT.get(); + } + + public static Environment getCurrentEnvironmentNotNull() { + Environment currentEnvironment = getCurrentEnvironment(); + if (currentEnvironment == null) { + throw new IllegalStateException("There's no FreeMarker Environemnt in this this thread."); + } + return currentEnvironment; + } + + static void setCurrentEnvironment(Environment env) { + TLS_ENVIRONMENT.set(env); + } + + public Environment(Template template, final TemplateHashModel rootDataModel, Writer out) { + mainTemplate = template; + configuration = template.getConfiguration(); + globalNamespace = new Namespace(null); + currentNamespace = mainNamespace = new Namespace(mainTemplate); + this.out = out; + this.rootDataModel = rootDataModel; + importMacros(template); + } + + /** + * Returns the topmost {@link Template}, with other words, the one for which this {@link Environment} was created. + * That template will never change, like {@code #include} or macro calls don't change it. + * + * @see #getCurrentNamespace() + * + * @since 2.3.22 + */ + public Template getMainTemplate() { + return mainTemplate; + } + + /** + * Returns the {@link Template} that we are "lexically" inside at the moment. This template will change when + * entering an {@code #include} or calling a macro or function in another template, or returning to yet another + * template with {@code #nested}. As such, it's useful in {@link TemplateDirectiveModel} to find out if from where + * the directive was called from. + * + * @see #getMainTemplate() + * @see #getCurrentNamespace() + * + * @since 2.3.23 + */ + @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm") + public Template getCurrentTemplate() { + int ln = instructionStackSize; + return ln == 0 ? getMainTemplate() : instructionStack[ln - 1].getTemplate(); + } + + public Template getCurrentTemplateNotNull() { + Template currentTemplate = getCurrentTemplate(); + if (currentTemplate == null) { + throw new IllegalStateException("There's no current template at the moment."); + } + return currentTemplate; + } + + /** + * Gets the currently executing <em>custom</em> directive's call place information, or {@code null} if there's no + * executing custom directive. This currently only works for calls made from templates with the {@code <@...>} + * syntax. This should only be called from the {@link TemplateDirectiveModel} that was invoked with {@code <@...>}, + * otherwise its return value is not defined by this API (it's usually {@code null}). + * + * @since 2.3.22 + */ + @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "False alarm") + public DirectiveCallPlace getCurrentDirectiveCallPlace() { + int ln = instructionStackSize; + if (ln == 0) return null; + ASTElement te = instructionStack[ln - 1]; + if (te instanceof ASTDirUserDefined) return (ASTDirUserDefined) te; + if (te instanceof ASTDirMacro && ln > 1 && instructionStack[ln - 2] instanceof ASTDirUserDefined) { + return (ASTDirUserDefined) instructionStack[ln - 2]; + } + return null; + } + + /** + * Deletes cached values that meant to be valid only during a single template execution. + */ + private void clearCachedValues() { + cachedTemplateNumberFormats = null; + cachedTemplateNumberFormat = null; + + cachedTempDateFormatArray = null; + cachedTempDateFormatsByFmtStrArray = null; + + cachedCollator = null; + cachedURLEscapingCharset = null; + cachedURLEscapingCharsetSet = false; + } + + /** + * Processes the template to which this environment belongs to. + */ + public void process() throws TemplateException, IOException { + Environment savedEnv = TLS_ENVIRONMENT.get(); + TLS_ENVIRONMENT.set(this); + try { + // Cached values from a previous execution are possibly outdated. + clearCachedValues(); + try { + doAutoImportsAndIncludes(this); + visit(getMainTemplate().getRootASTNode()); + // 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 { + TLS_ENVIRONMENT.set(savedEnv); + } + } + + /** + * Executes the auto-imports and auto-includes for the main template of this environment. + * This is not meant to be called or overridden by code outside of FreeMarker. + */ + private void doAutoImportsAndIncludes(Environment env) throws TemplateException, IOException { + Template t = getMainTemplate(); + doAutoImports(t); + doAutoIncludes(t); + } + + private void doAutoImports(Template t) throws IOException, TemplateException { + Map<String, String> envAutoImports = isAutoImportsSet() ? getAutoImports() : null; + Map<String, String> tAutoImports = t.isAutoImportsSet() ? t.getAutoImports() : null; + + boolean lazyAutoImports = getLazyAutoImports() != null ? getLazyAutoImports() : getLazyImports(); + + for (Map.Entry<String, String> autoImport : configuration.getAutoImports().entrySet()) { + String nsVarName = autoImport.getKey(); + if ((tAutoImports == null || !tAutoImports.containsKey(nsVarName)) + && (envAutoImports == null || !envAutoImports.containsKey(nsVarName))) { + importLib(autoImport.getValue(), nsVarName, lazyAutoImports); + } + } + if (tAutoImports != null) { + for (Map.Entry<String, String> autoImport : tAutoImports.entrySet()) { + String nsVarName = autoImport.getKey(); + if (envAutoImports == null || !envAutoImports.containsKey(nsVarName)) { + importLib(autoImport.getValue(), nsVarName, lazyAutoImports); + } + } + } + if (envAutoImports != null) { + for (Map.Entry<String, String> autoImport : envAutoImports.entrySet()) { + String nsVarName = autoImport.getKey(); + importLib(autoImport.getValue(), nsVarName, lazyAutoImports); + } + } + } + + private void doAutoIncludes(Template t) throws TemplateException, IOException { + // We can't store autoIncludes in LinkedHashSet-s because setAutoIncludes(List) allows duplicates, + // unfortunately. Yet we have to prevent duplicates among Configuration levels, with the lowest levels having + // priority. So we build some Set-s to do that, but we avoid the most common cases where they aren't needed. + + List<String> tAutoIncludes = t.isAutoIncludesSet() ? t.getAutoIncludes() : null; + List<String> envAutoIncludes = isAutoIncludesSet() ? getAutoIncludes() : null; + + for (String templateName : configuration.getAutoIncludes()) { + if ((tAutoIncludes == null || !tAutoIncludes.contains(templateName)) + && (envAutoIncludes == null || !envAutoIncludes.contains(templateName))) { + include(configuration.getTemplate(templateName, getLocale())); + } + } + + if (tAutoIncludes != null) { + for (String templateName : tAutoIncludes) { + if (envAutoIncludes == null || !envAutoIncludes.contains(templateName)) { + include(configuration.getTemplate(templateName, getLocale())); + } + } + } + + if (envAutoIncludes != null) { + for (String templateName : envAutoIncludes) { + include(configuration.getTemplate(templateName, getLocale())); + } + } + } + + /** + * "Visit" the template element. + */ + void visit(ASTElement element) throws IOException, TemplateException { + // ATTENTION: This method body is manually "inlined" into visit(ASTElement[]); keep them in sync! + pushElement(element); + try { + ASTElement[] templateElementsToVisit = element.accept(this); + if (templateElementsToVisit != null) { + for (ASTElement el : templateElementsToVisit) { + if (el == null) { + break; // Skip unused trailing buffer capacity + } + visit(el); + } + } + } catch (TemplateException te) { + handleTemplateException(te); + } finally { + popElement(); + } + // ATTENTION: This method body above is manually "inlined" into visit(ASTElement[]); keep them in sync! + } + + /** + * @param elementBuffer + * The elements to visit; might contains trailing {@code null}-s. Can be {@code null}. + * + * @since 2.3.24 + */ + final void visit(ASTElement[] elementBuffer) throws IOException, TemplateException { + if (elementBuffer == null) { + return; + } + for (ASTElement element : elementBuffer) { + if (element == null) { + break; // Skip unused trailing buffer capacity + } + + // ATTENTION: This part is the manually "inlining" of visit(ASTElement[]); keep them in sync! + // We don't just let Hotspot to do it, as we want a hard guarantee regarding maximum stack usage. + pushElement(element); + try { + ASTElement[] templateElementsToVisit = element.accept(this); + if (templateElementsToVisit != null) { + for (ASTElement el : templateElementsToVisit) { + if (el == null) { + break; // Skip unused trailing buffer capacity + } + visit(el); + } + } + } catch (TemplateException te) { + handleTemplateException(te); + } finally { + popElement(); + } + // ATTENTION: This part above is the manually "inlining" of visit(ASTElement[]); keep them in sync! + } + } + + @SuppressFBWarnings(value = "RANGE_ARRAY_INDEX", justification = "Not called when stack is empty") + private ASTElement replaceTopElement(ASTElement element) { + return instructionStack[instructionStackSize - 1] = element; + } + + private static final TemplateModel[] NO_OUT_ARGS = new TemplateModel[0]; + + void visit(final ASTElement element, + TemplateDirectiveModel directiveModel, Map args, + final List bodyParameterNames) throws TemplateException, IOException { + visit(new ASTElement[] { element }, directiveModel, args, bodyParameterNames); + } + + void visit(final ASTElement[] childBuffer, + TemplateDirectiveModel directiveModel, Map args, + final List bodyParameterNames) throws TemplateException, IOException { + TemplateDirectiveBody nested; + if (childBuffer == null) { + nested = null; + } else { + nested = new NestedElementTemplateDirectiveBody(childBuffer); + } + final TemplateModel[] outArgs; + if (bodyParameterNames == null || bodyParameterNames.isEmpty()) { + outArgs = NO_OUT_ARGS; + } else { + outArgs = new TemplateModel[bodyParameterNames.size()]; + } + if (outArgs.length > 0) { + pushLocalContext(new LocalContext() { + + @Override + public TemplateModel getLocalVariable(String name) { + int index = bodyParameterNames.indexOf(name); + return index != -1 ? outArgs[index] : null; + } + + @Override + public Collection getLocalVariableNames() { + return bodyParameterNames; + } + }); + } + try { + directiveModel.execute(this, args, outArgs, nested); + } finally { + if (outArgs.length > 0) { + localContextStack.pop(); + } + } + } + + /** + * "Visit" the template element, passing the output through a TemplateTransformModel + * + * @param elementBuffer + * the element to visit through a transform; might contains trailing {@code null}-s + * @param transform + * the transform to pass the element output through + * @param args + * optional arguments fed to the transform + */ + void visitAndTransform(ASTElement[] elementBuffer, + TemplateTransformModel transform, + Map args) + throws TemplateException, IOException { + try { + Writer tw = transform.getWriter(out, args); + if (tw == null) tw = EMPTY_BODY_WRITER; + TransformControl tc = tw instanceof TransformControl + ? (TransformControl) tw + : null; + + Writer prevOut = out; + out = tw; + try { + if (tc == null || tc.onStart() != TransformControl.SKIP_BODY) { + do { + visit(elementBuffer); + } while (tc != null && tc.afterBody() == TransformControl.REPEAT_EVALUATION); + } + } catch (Throwable t) { + try { + if (tc != null) { + tc.onError(t); + } else { + throw t; + } + } catch (TemplateException e) { + throw e; + } catch (IOException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (Error e) { + throw e; + } catch (Throwable e) { + throw new UndeclaredThrowableException(e); + } + } finally { + out = prevOut; + tw.close(); + } + } catch (TemplateException te) { + handleTemplateException(te); + } + } + + /** + * Visit a block using buffering/recovery + */ + void visitAttemptRecover( + ASTDirAttemptRecoverContainer attemptBlock, ASTElement attemptedSection, ASTDirRecover recoverySection) + throws TemplateException, IOException { + Writer prevOut = out; + StringWriter sw = new StringWriter(); + out = sw; + TemplateException thrownException = null; + boolean lastFIRE = setFastInvalidReferenceExceptions(false); + boolean lastInAttemptBlock = inAttemptBlock; + try { + inAttemptBlock = true; + visit(attemptedSection); + } catch (TemplateException te) { + thrownException = te; + } finally { + inAttemptBlock = lastInAttemptBlock; + setFastInvalidReferenceExceptions(lastFIRE); + out = prevOut; + } + if (thrownException != null) { + if (LOG_ATTEMPT.isDebugEnabled()) { + LOG_ATTEMPT.debug("Error in attempt block " + + attemptBlock.getStartLocationQuoted(), thrownException); + } + try { + recoveredErrorStack.add(thrownException); + visit(recoverySection); + } finally { + recoveredErrorStack.remove(recoveredErrorStack.size() - 1); + } + } else { + out.write(sw.toString()); + } + } + + String getCurrentRecoveredErrorMessage() throws TemplateException { + if (recoveredErrorStack.isEmpty()) { + throw new _MiscTemplateException(this, ".error is not available outside of a #recover block"); + } + return ((Throwable) recoveredErrorStack.get(recoveredErrorStack.size() - 1)).getMessage(); + } + + /** + * Tells if we are inside an <tt>#attempt</tt> block (but before <tt>#recover</tt>). This can be useful for + * {@link TemplateExceptionHandler}-s, as then they may don't want to print the error to the output, as + * <tt>#attempt</tt> will roll it back anyway. + * + * @since 2.3.20 + */ + public boolean isInAttemptBlock() { + return inAttemptBlock; + } + + /** + * Used for {@code #nested}. + */ + void invokeNestedContent(ASTDirNested.Context bodyCtx) throws TemplateException, IOException { + ASTDirMacro.Context invokingMacroContext = getCurrentMacroContext(); + LocalContextStack prevLocalContextStack = localContextStack; + ASTElement[] nestedContentBuffer = invokingMacroContext.nestedContentBuffer; + if (nestedContentBuffer != null) { + currentMacroContext = invokingMacroContext.prevMacroContext; + currentNamespace = invokingMacroContext.nestedContentNamespace; + + localContextStack = invokingMacroContext.prevLocalContextStack; + if (invokingMacroContext.nestedContentParameterNames != null) { + pushLocalContext(bodyCtx); + } + try { + visit(nestedContentBuffer); + } finally { + if (invokingMacroContext.nestedContentParameterNames != null) { + localContextStack.pop(); + } + currentMacroContext = invokingMacroContext; + currentNamespace = getMacroNamespace(invokingMacroContext.getMacro()); + localContextStack = prevLocalContextStack; + } + } + } + + /** + * "visit" an ASTDirList + */ + boolean visitIteratorBlock(ASTDirList.IterationContext ictxt) + throws TemplateException, IOException { + pushLocalContext(ictxt); + try { + return ictxt.accept(this); + } catch (TemplateException te) { + handleTemplateException(te); + return true; + } finally { + localContextStack.pop(); + } + } + + /** + * Used for {@code #visit} and {@code #recurse}. + */ + void invokeNodeHandlerFor(TemplateNodeModel node, TemplateSequenceModel namespaces) + throws TemplateException, IOException { + if (nodeNamespaces == null) { + NativeSequence seq = new NativeSequence(1); + seq.add(currentNamespace); + nodeNamespaces = seq; + } + int prevNodeNamespaceIndex = nodeNamespaceIndex; + String prevNodeName = currentNodeName; + String prevNodeNS = currentNodeNS; + TemplateSequenceModel prevNodeNamespaces = nodeNamespaces; + TemplateNodeModel prevVisitorNode = currentVisitorNode; + currentVisitorNode = node; + if (namespaces != null) { + nodeNamespaces = namespaces; + } + try { + TemplateModel macroOrTransform = getNodeProcessor(node); + if (macroOrTransform instanceof ASTDirMacro) { + invoke((ASTDirMacro) macroOrTransform, null, null, null, null); + } else if (macroOrTransform instanceof TemplateTransformModel) { + visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null); + } else { + String nodeType = node.getNodeType(); + if (nodeType != null) { + // If the node's type is 'text', we just output it. + if ((nodeType.equals("text") && node instanceof TemplateScalarModel)) { + out.write(((TemplateScalarModel) node).getAsString()); + } else if (nodeType.equals("document")) { + recurse(node, namespaces); + } + // We complain here, unless the node's type is 'pi', or "comment" or "document_type", in which case + // we just ignore it. + else if (!nodeType.equals("pi") + && !nodeType.equals("comment") + && !nodeType.equals("document_type")) { + throw new _MiscTemplateException( + this, noNodeHandlerDefinedDescription(node, node.getNodeNamespace(), nodeType)); + } + } else { + throw new _MiscTemplateException( + this, noNodeHandlerDefinedDescription(node, node.getNodeNamespace(), "default")); + } + } + } finally { + currentVisitorNode = prevVisitorNode; + nodeNamespaceIndex = prevNodeNamespaceIndex; + currentNodeName = prevNodeName; + currentNodeNS = prevNodeNS; + nodeNamespaces = prevNodeNamespaces; + } + } + + private Object[] noNodeHandlerDefinedDescription( + TemplateNodeModel node, String ns, String nodeType) + throws TemplateModelException { + String nsPrefix; + if (ns != null) { + if (ns.length() > 0) { + nsPrefix = " and namespace "; + } else { + nsPrefix = " and no namespace"; + } + } else { + nsPrefix = ""; + ns = ""; + } + return new Object[] { "No macro or directive is defined for node named ", + new _DelayedJQuote(node.getNodeName()), nsPrefix, ns, + ", and there is no fallback handler called @", nodeType, " either." }; + } + + void fallback() throws TemplateException, IOException { + TemplateModel macroOrTransform = getNodeProcessor(currentNodeName, currentNodeNS, nodeNamespaceIndex); + if (macroOrTransform instanceof ASTDirMacro) { + invoke((ASTDirMacro) macroOrTransform, null, null, null, null); + } else if (macroOrTransform instanceof TemplateTransformModel) { + visitAndTransform(null, (TemplateTransformModel) macroOrTransform, null); + } + } + + /** + * Calls the macro or function with the given arguments and nested block. + */ + void invoke(ASTDirMacro macro, + Map namedArgs, List positionalArgs, + List bodyParameterNames, ASTElement[] childBuffer) throws TemplateException, IOException { + if (macro == ASTDirMacro.DO_NOTHING_MACRO) { + return; + } + + pushElement(macro); + try { + final ASTDirMacro.Context macroCtx = macro.new Context(this, childBuffer, bodyParameterNames); + setMacroContextLocalsFromArguments(macroCtx, macro, namedArgs, positionalArgs); + + final ASTDirMacro.Context prevMacroCtx = currentMacroContext; + currentMacroContext = macroCtx; + + final LocalContextStack prevLocalContextStack = localContextStack; + localContextStack = null; + + final Namespace prevNamespace = currentNamespace; + currentNamespace = (Namespace) macroToNamespaceLookup.get(macro); + + try { + macroCtx.sanityCheck(this); + visit(macro.getChildBuffer()); + } catch (ASTDirReturn.Return re) { + // Not an error, just a <#return> + } catch (TemplateException te) { + handleTemplateException(te); + } finally { + currentMacroContext = prevMacroCtx; + localContextStack = prevLocalContextStack; + currentNamespace = prevNamespace; + } + } finally { + popElement(); + } + } + + /** + * Sets the local variables corresponding to the macro call arguments in the macro context. + */ + private void setMacroContextLocalsFromArguments( + final ASTDirMacro.Context macroCtx, + final ASTDirMacro macro, + final Map namedArgs, final List positionalArgs) throws TemplateException { + String catchAllParamName = macro.getCatchAll(); + if (namedArgs != null) { + final NativeHashEx2 catchAllParamValue; + if (catchAllParamName != null) { + catchAllParamValue = new NativeHashEx2(); + macroCtx.setLocalVar(catchAllParamName, catchAllParamValue); + } else { + catchAllParamValue = null; + } + + for (Map.Entry argNameAndValExp : (Set<Map.Entry>) namedArgs.entrySet()) { + final String argName = (String) argNameAndValExp.getKey(); + final boolean isArgNameDeclared = macro.hasArgNamed(argName); + if (isArgNameDeclared || catchAllParamName != null) { + ASTExpression argValueExp = (ASTExpression) argNameAndValExp.getValue(); + TemplateModel argValue = argValueExp.eval(this); + if (isArgNameDeclared) { + macroCtx.setLocalVar(argName, argValue); + } else { + catchAllParamValue.put(argName, argValue); + } + } else { + throw new _MiscTemplateException(this, + (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()), + " has no parameter with name ", new _DelayedJQuote(argName), "."); + } + } + } else if (positionalArgs != null) { + final NativeSequence catchAllParamValue; + if (catchAllParamName != null) { + catchAllParamValue = new NativeSequence(8); + macroCtx.setLocalVar(catchAllParamName, catchAllParamValue); + } else { + catchAllParamValue = null; + } + + String[] argNames = macro.getArgumentNamesInternal(); + final int argsCnt = positionalArgs.size(); + if (argNames.length < argsCnt && catchAllParamName == null) { + throw new _MiscTemplateException(this, + (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()), + " only accepts ", new _DelayedToString(argNames.length), " parameters, but got ", + new _DelayedToString(argsCnt), "."); + } + for (int i = 0; i < argsCnt; i++) { + ASTExpression argValueExp = (ASTExpression) positionalArgs.get(i); + TemplateModel argValue = argValueExp.eval(this); + try { + if (i < argNames.length) { + String argName = argNames[i]; + macroCtx.setLocalVar(argName, argValue); + } else { + catchAllParamValue.add(argValue); + } + } catch (RuntimeException re) { + throw new _MiscTemplateException(re, this); + } + } + } + } + + /** + * Defines the given macro in the current namespace (doesn't call it). + */ + void visitMacroDef(ASTDirMacro macro) { + macroToNamespaceLookup.put(macro, currentNamespace); + currentNamespace.put(macro.getName(), macro); + } + + Namespace getMacroNamespace(ASTDirMacro macro) { + return (Namespace) macroToNamespaceLookup.get(macro); + } + + void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces) + throws TemplateException, IOException { + if (node == null) { + node = getCurrentVisitorNode(); + if (node == null) { + throw new _TemplateModelException( + "The target node of recursion is missing or null."); + } + } + TemplateSequenceModel children = node.getChildNodes(); + if (children == null) return; + for (int i = 0; i < children.size(); i++) { + TemplateNodeModel child = (TemplateNodeModel) children.get(i); + if (child != null) { + invokeNodeHandlerFor(child, namespaces); + } + } + } + + ASTDirMacro.Context getCurrentMacroContext() { + return currentMacroContext; + } + + private void handleTemplateException(TemplateException templateException) + throws TemplateException { + // Logic to prevent double-handling of the exception in + // nested visit() calls. + if (lastThrowable == templateException) { + throw templateException; + } + lastThrowable = templateException; + + // Log the exception, if logTemplateExceptions isn't false. However, even if it's false, if we are inside + // an #attempt block, it has to be logged, as it certainly won't bubble up to the caller of FreeMarker. + if (LOG.isErrorEnabled() && (isInAttemptBlock() || getLogTemplateExceptions())) { + LOG.error("Error executing FreeMarker template", templateException); + } + + // Stop exception is not passed to the handler, but + // explicitly rethrown. + if (templateException instanceof StopException) { + throw templateException; + } + + // Finally, pass the exception to the handler + getTemplateExceptionHandler().handleTemplateException(templateException, this, out); + } + + @Override + public void setTemplateExceptionHandler(TemplateExceptionHandler templateExceptionHandler) { + super.setTemplateExceptionHandler(templateExceptionHandler); + lastThrowable = null; + } + + @Override + protected TemplateExceptionHandler getDefaultTemplateExceptionHandler() { + return getMainTemplate().getTemplateExceptionHandler(); + } + + @Override + protected ArithmeticEngine getDefaultArithmeticEngine() { + return getMainTemplate().getArithmeticEngine(); + } + + @Override + protected ObjectWrapper getDefaultObjectWrapper() { + return getMainTemplate().getObjectWrapper(); + } + + @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 + protected Locale getDefaultLocale() { + return getMainTemplate().getLocale(); + } + + @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 + protected TimeZone getDefaultTimeZone() { + return getMainTemplate().getTimeZone(); + } + + @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; + } + } + + @Override + protected TimeZone getDefaultSQLDateAndTimeTimeZone() { + return getMainTemplate().getSQLDateAndTimeTimeZone(); + } + + // 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(Charset urlEscapingCharset) { + cachedURLEscapingCharsetSet = false; + super.setURLEscapingCharset(urlEscapingCharset); + } + + @Override + protected Charset getDefaultURLEscapingCharset() { + return getMainTemplate().getURLEscapingCharset(); + } + + @Override + protected TemplateClassResolver getDefaultNewBuiltinClassResolver() { + return getMainTemplate().getNewBuiltinClassResolver(); + } + + @Override + protected boolean getDefaultAutoFlush() { + return getMainTemplate().getAutoFlush(); + } + + @Override + protected boolean getDefaultShowErrorTips() { + return getMainTemplate().getShowErrorTips(); + } + + @Override + protected boolean getDefaultAPIBuiltinEnabled() { + return getMainTemplate().getAPIBuiltinEnabled(); + } + + @Override + protected boolean getDefaultLogTemplateExceptions() { + return getMainTemplate().getLogTemplateExceptions(); + } + + @Override + protected boolean getDefaultLazyImports() { + return getMainTemplate().getLazyImports(); + } + + @Override + protected Boolean getDefaultLazyAutoImports() { + return getMainTemplate().getLazyAutoImports(); + } + + @Override + protected Map<String, String> getDefaultAutoImports() { + return getMainTemplate().getAutoImports(); + } + + @Override + protected List<String> getDefaultAutoIncludes() { + return getMainTemplate().getAutoIncludes(); + } + + @Override + protected Object getDefaultCustomAttribute(Object name) { + return getMainTemplate().getCustomAttribute(name); + } + + @Override + protected Map<Object, Object> getDefaultCustomAttributes() { + return getMainTemplate().getCustomAttributes(); + } + + /* + * Note that altough it's not allowed to set this setting with the <tt>setting</tt> directive, it still must be + * allowed to set it from Java code while the template executes, since some frameworks allow templates to actually + * change the output encoding on-the-fly. + */ + @Override + public void setOutputEncoding(Charset outputEncoding) { + cachedURLEscapingCharsetSet = false; + super.setOutputEncoding(outputEncoding); + } + + @Override + protected Charset getDefaultOutputEncoding() { + return getMainTemplate().getOutputEncoding(); + } + + /** + * 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. + */ + Charset 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; + } + + @Override + protected String getDefaultNumberFormat() { + return getMainTemplate().getNumberFormat(); + } + + @Override + protected Map<String, TemplateNumberFormatFactory> getDefaultCustomNumberFormats() { + return getMainTemplate().getCustomNumberFormats(); + } + + @Override + protected TemplateNumberFormatFactory getDefaultCustomNumberFormat(String name) { + return getMainTemplate().getCustomNumberFormat(name); + } + + @Override + protected String getDefaultBooleanFormat() { + return getMainTemplate().getBooleanFormat(); + } + + String formatBoolean(boolean value, boolean fallbackToTrueFalse) throws TemplateException { + TemplateBooleanFormat templateBooleanFormat = getTemplateBooleanFormat(); + if (value) { + String s = templateBooleanFormat.getTrueStringValue(); + if (s == null) { + if (fallbackToTrueFalse) { + return MiscUtil.C_TRUE; + } else { + throw new _MiscTemplateException(getNullBooleanFormatErrorDescription()); + } + } else { + return s; + } + } else { + String s = templateBooleanFormat.getFalseStringValue(); + if (s == null) { + if (fallbackToTrueFalse) { + return MiscUtil.C_FALSE; + } else { + throw new _MiscTemplateException(getNullBooleanFormatErrorDescription()); + } + } else { + return s; + } + } + } + + TemplateBooleanFormat getTemplateBooleanFormat() { + TemplateBooleanFormat format = cachedTemplateBooleanFormat; + if (format == null) { + format = TemplateBooleanFormat.getInstance(getBooleanFormat()); + cachedTemplateBooleanFormat = format; + } + return format; + } + + @Override + public void setBooleanFormat(String booleanFormat) { + String previousFormat = getBooleanFormat(); + super.setBooleanFormat(booleanFormat); + if (!booleanFormat.equals(previousFormat)) { + cachedTemplateBooleanFormat = null; + } + } + + 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(TemplateBooleanFormat.C_TRUE_FALSE) + ? ", which is the legacy default computer-language format, and hence isn't accepted." + : ".") + ).tips( + "If you just want \"true\"/\"false\" result as you are generting computer-language output, " + + "use \"?c\", like ${myBool?c}.", + "You can write myBool?string('yes', 'no') and like to specify boolean formatting in place.", + new Object[] { + "If you need the same two values on most places, the programmers should set the \"", + BOOLEAN_FORMAT_KEY ,"\" setting to something like \"yes,no\"." } + ); + } + + /** + * Format number with the default number format. + * + * @param exp + * The blamed expression if an error occurs; it's only needed for better error messages + */ + String formatNumberToPlainText(TemplateNumberModel number, ASTExpression exp, boolean useTempModelExc) + throws TemplateException { + return formatNumberToPlainText(number, getTemplateNumberFormat(exp, useTempModelExc), exp, useTempModelExc); + } + + /** + * Format number with the number format specified as the parameter, with the current locale. + * + * @param exp + * The blamed expression if an error occurs; it's only needed for better error messages + */ + String formatNumberToPlainText( + TemplateNumberModel number, TemplateNumberFormat format, ASTExpression exp, + boolean useTempModelExc) + throws TemplateException { + try { + return _EvalUtil.assertFormatResultNotNull(format.formatToPlainText(number)); + } catch (TemplateValueFormatException e) { + throw MessageUtil.newCantFormatNumberException(format, exp, e, useTempModelExc); + } + } + + /** + * Returns the current number format ({@link #getNumberFormat()}) as {@link TemplateNumberFormat}. + * + * <p> + * Performance notes: The result is stored for reuse, so calling this method frequently is usually not a problem. + * However, at least as of this writing (2.3.24), changing the current locale {@link #setLocale(Locale)} or changing + * the current number format ({@link #setNumberFormat(String)}) will drop the stored value, so it will have to be + * recalculated. + * + * @since 2.3.24 + */ + public TemplateNumberFormat getTemplateNumberFormat() throws TemplateValueFormatException { + TemplateNumberFormat format = cachedTemplateNumberFormat; + if (format == null) { + format = getTemplateNumberFormat(getNumberFormat(), false); + cachedTemplateNumberFormat = format; + } + return format; + } + + /** + * Returns the number format as {@link TemplateNumberFormat} for the given format string and the current locale. + * (The current locale is the locale returned by {@link #getLocale()}.) Note that the result will be cached in the + * {@link Environment} instance (though at least in 2.3.24 the cache will be flushed if the current locale of the + * {@link Environment} is changed). + * + * @param formatString + * A string that you could also use as the value of the {@code numberFormat} configuration setting. Can't + * be {@code null}. + * + * @since 2.3.24 + */ + public TemplateNumberFormat getTemplateNumberFormat(String formatString) throws TemplateValueFormatException { + return getTemplateNumberFormat(formatString, true); + } + + /** + * Returns the number format as {@link TemplateNumberFormat}, for the given format string and locale. To get a + * number format for the current locale, use {@link #getTemplateNumberFormat(String)} instead. + * + * <p> + * Note on performance (which was true at least for 2.3.24): Unless the locale happens to be equal to the current + * locale, the {@link Environment}-level format cache can't be used, so the format string has to be parsed and the + * matching factory has to be get an invoked, which is much more expensive than getting the format from the cache. + * Thus the returned format should be stored by the caller for later reuse (but only within the current thread and + * in relation to the current {@link Environment}), if it will be needed frequently. + * + * @param formatString + * A string that you could also use as the value of the {@code numberFormat} configuration setting. + * @param locale + * The locale of the number format; not {@code null}. + * + * @since 2.3.24 + */ + public TemplateNumberFormat getTemplateNumberFormat(String formatString, Locale locale) + throws TemplateValueFormatException { + if (locale.equals(getLocale())) { + getTemplateNumberFormat(formatString); + } + + return getTemplateNumberFormatWithoutCache(formatString, locale); + } + + /** + * Convenience wrapper around {@link #getTemplateNumberFormat()} to be called during expression evaluation. + */ + TemplateNumberFormat getTemplateNumberFormat(ASTExpression exp, boolean useTempModelExc) throws TemplateException { + TemplateNumberFormat format; + try { + format = getTemplateNumberFormat(); + } catch (TemplateValueFormatException e) { + _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( + "Failed to get number format object for the current number format string, ", + new _DelayedJQuote(getNumberFormat()), ": ", e.getMessage()) + .blame(exp); + throw useTempModelExc + ? new _TemplateModelException(e, this, desc) : new _MiscTemplateException(e, this, desc); + } + return format; + } + + /** + * Convenience wrapper around {@link #getTemplateNumberFormat(String)} to be called during expression evaluation. + * + * @param exp + * The blamed expression if an error occurs; it's only needed for better error messages + */ + TemplateNumberFormat getTemplateNumberFormat(String formatString, ASTExpression exp, boolean useTempModelExc) + throws TemplateException { + TemplateNumberFormat format; + try { + format = getTemplateNumberFormat(formatString); + } catch (TemplateValueFormatException e) { + _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( + "Failed to get number format object for the ", new _DelayedJQuote(formatString), + " number format string: ", e.getMessage()) + .blame(exp); + throw useTempModelExc + ? new _TemplateModelException(e, this, desc) : new _MiscTemplateException(e, this, desc); + } + return format; + } + + /** + * Gets the {@link TemplateNumberFormat} <em>for the current locale</em>. + * + * @param formatString + * Not {@code null} + * @param cacheResult + * If the results should stored in the {@link Environment}-level cache. It will still try to get the + * result from the cache regardless of this parameter. + */ + private TemplateNumberFormat getTemplateNumberFormat(String formatString, boolean cacheResult) + throws TemplateValueFormatException { + if (cachedTemplateNumberFormats == null) { + if (cacheResult) { + cachedTemplateNumberFormats = new HashMap<>(); + } + } else { + TemplateNumberFormat format = cachedTemplateNumberFormats.get(formatString); + if (format != null) { + return format; + } + } + + TemplateNumberFormat format = getTemplateNumberFormatWithoutCache(formatString, getLocale()); + + if (cacheResult) { + cachedTemplateNumberFormats.put(formatString, format); + } + return format; + } + + /** + * Returns the {@link TemplateNumberFormat} for the given parameters without using the {@link Environment}-level + * cache. Of course, the {@link TemplateNumberFormatFactory} involved might still uses its own cache. + * + * @param formatString + * Not {@code null} + * @param locale + * Not {@code null} + */ + private TemplateNumberFormat getTemplateNumberFormatWithoutCache(String formatString, Locale locale) + throws TemplateValueFormatException { + int formatStringLen = formatString.length(); + if (formatStringLen > 1 + && formatString.charAt(0) == '@' + && Character.isLetter(formatString.charAt(1))) { + final String name; + final String params; + { + int endIdx; + findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) { + char c = formatString.charAt(endIdx); + if (c == ' ' || c == '_') { + break findParamsStart; + } + } + name = formatString.substring(1, endIdx); + params = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : ""; + } + + TemplateNumberFormatFactory formatFactory = getCustomNumberFormat(name); + if (formatFactory == null) { + throw new UndefinedCustomFormatException( + "No custom number format was defined with name " + _StringUtil.jQuote(name)); + } + + return formatFactory.get(params, locale, this); + } else { + return JavaTemplateNumberFormatFactory.INSTANCE.get(formatString, locale, this); + } + } + + /** + * Returns the {@link NumberFormat} used for the <tt>c</tt> built-in. This is always US English + * <code>"0.################"</code>, without grouping and without superfluous decimal separator. + */ + public NumberFormat getCNumberFormat() { + // It can't be cached in a static field, because DecimalFormat-s aren't + // thread-safe. + if (cNumberFormat == null) { + cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT.clone(); + } + return cNumberFormat; + } + + @Override + public void setTimeFormat(String timeFormat) { + String prevTimeFormat = getTimeFormat(); + super.setTimeFormat(timeFormat); + if (!timeFormat.equals(prevTimeFormat)) { + if (cachedTempDateFormatArray != null) { + for (int i = 0; i < CACHED_TDFS_LENGTH; i += CACHED_TDFS_ZONELESS_INPUT_OFFS) { + cachedTempDateFormatArray[i + TemplateDateModel.TIME] = null; + } + } + } + } + + @Override + protected String getDefaultTimeFormat() { + return getMainTemplate().getTimeFormat(); + } + + @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 + protected String getDefaultDateFormat() { + return getMainTemplate().getDateFormat(); + } + + @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 + protected String getDefaultDateTimeFormat() { + return getMainTemplate().getDateTimeFormat(); + } + + @Override + protected Map<String, TemplateDateFormatFactory> getDefaultCustomDateFormats() { + return getMainTemplate().getCustomDateFormats(); + } + + @Override + protected TemplateDateFormatFactory getDefaultCustomDateFormat(String name) { + return getMainTemplate().getCustomDateFormat(name); + } + + public Configuration getConfiguration() { + return configuration; + } + + TemplateModel getLastReturnValue() { + return lastReturnValue; + } + + void setLastReturnValue(TemplateModel lastReturnValue) { + this.lastReturnValue = lastReturnValue; + } + + void clearLastReturnValue() { + lastReturnValue = null; + } + + /** + * @param tdmSourceExpr + * The blamed expression if an error occurs; only used for error messages. + */ + String formatDateToPlainText(TemplateDateModel tdm, ASTExpression tdmSourceExpr, + boolean useTempModelExc) throws TemplateException { + TemplateDateFormat format = getTemplateDateFormat(tdm, tdmSourceExpr, useTempModelExc); + + try { + return _EvalUtil.assertFormatResultNotNull(format.formatToPlainText(tdm)); + } catch (TemplateValueFormatException e) { + throw MessageUtil.newCantFormatDateException(format, tdmSourceExpr, e, useTempModelExc); + } + } + + /** + * @param blamedDateSourceExp + * The blamed expression if an error occurs; only used for error messages. + * @param blamedFormatterExp + * The blamed expression if an error occurs; only used for error messages. + */ + String formatDateToPlainText(TemplateDateModel tdm, String formatString, + ASTExpression blamedDateSourceExp, ASTExpression blamedFormatterExp, + boolean useTempModelExc) throws TemplateException { + Date date = _EvalUtil.modelToDate(tdm, blamedDateSourceExp); + + TemplateDateFormat format = getTemplateDateFormat( + formatString, tdm.getDateType(), date.getClass(), + blamedDateSourceExp, blamedFormatterExp, + useTempModelExc); + + try { + return _EvalUtil.assertFormatResultNotNull(format.formatToPlainText(tdm)); + } catch (TemplateValueFormatException e) { + throw MessageUtil.newCantFormatDateException(format, blamedDateSourceExp, e, useTempModelExc); + } + } + + /** + * Gets a {@link TemplateDateFormat} using the date/time/datetime format settings and the current locale and time + * zone. (The current locale is the locale returned by {@link #getLocale()}. The current time zone is + * {@link #getTimeZone()} or {@link #getSQLDateAndTimeTimeZone()}). + * + * @param dateType + * The FTL date type; see the similar parameter of + * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} + * @param dateClass + * The exact {@link Date} class, like {@link java.sql.Date} or {@link java.sql.Time}; this can influences + * time zone selection. See also: {@link #setSQLDateAndTimeTimeZone(TimeZone)} + */ + public TemplateDateFormat getTemplateDateFormat(int dateType, Class<? extends Date> dateClass) + throws TemplateValueFormatException { + boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); + return getTemplateDateFormat(dateType, shouldUseSQLDTTimeZone(isSQLDateOrTime), isSQLDateOrTime); + } + + /** + * Gets a {@link TemplateDateFormat} for the specified format string and the current locale and time zone. (The + * current locale is the locale returned by {@link #getLocale()}. The current time zone is {@link #getTimeZone()} or + * {@link #getSQLDateAndTimeTimeZone()}). + * + * <p> + * Note on performance: The result will be cached in the {@link Environment} instance. However, at least in 2.3.24 + * the cached entries that depend on the current locale or the current time zone or the current date/time/datetime + * format of the {@link Environment} will be lost when those settings are changed. + * + * @param formatString + * Like {@code "iso m"} or {@code "dd.MM.yyyy HH:mm"} or {@code "@somethingCustom"} or + * {@code "@somethingCustom params"} + * + * @since 2.3.24 + */ + public TemplateDateFormat getTemplateDateFormat( + String formatString, int dateType, Class<? extends Date> dateClass) + throws TemplateValueFormatException { + boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); + return getTemplateDateFormat( + formatString, dateType, + shouldUseSQLDTTimeZone(isSQLDateOrTime), isSQLDateOrTime, true); + } + + /** + * Like {@link #getTemplateDateFormat(String, int, Class)}, but allows you to use a different locale than the + * current one. If you want to use the current locale, use {@link #getTemplateDateFormat(String, int, Class)} + * instead. + * + * <p> + * Performance notes regarding the locale and time zone parameters of + * {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} apply. + * + * @param locale + * Can't be {@code null}; See the similar parameter of + * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} + * + * @see #getTemplateDateFormat(String, int, Class) + * + * @since 2.4 + */ + public TemplateDateFormat getTemplateDateFormat( + String formatString, + int dateType, Class<? extends Date> dateClass, + Locale locale) + throws TemplateValueFormatException { + boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); + boolean useSQLDTTZ = shouldUseSQLDTTimeZone(isSQLDateOrTime); + return getTemplateDateFormat( + formatString, + dateType, locale, useSQLDTTZ ? getSQLDateAndTimeTimeZone() : getTimeZone(), isSQLDateOrTime); + } + + /** + * Like {@link #getTemplateDateFormat(String, int, Class)}, but allows you to use a different locale and time zone + * than the current one. If you want to use the current locale and time zone, use + * {@link #getTemplateDateFormat(String, int, Class)} instead. + * + * <p> + * Performance notes regarding the locale and time zone parameters of + * {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} apply. + * + * @param timeZone + * The {@link TimeZone} used if {@code dateClass} is not an SQL date-only or time-only type. Can't be + * {@code null}. + * @param sqlDateAndTimeTimeZone + * The {@link TimeZone} used if {@code dateClass} is an SQL date-only or time-only type. Can't be + * {@code null}. + * + * @see #getTemplateDateFormat(String, int, Class) + * + * @since 2.4 + */ + public TemplateDateFormat getTemplateDateFormat( + String formatString, + int dateType, Class<? extends Date> dateClass, + Locale locale, TimeZone timeZone, TimeZone sqlDateAndTimeTimeZone) + throws TemplateValueFormatException { + boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); + boolean useSQLDTTZ = shouldUseSQLDTTimeZone(isSQLDateOrTime); + return getTemplateDateFormat( + formatString, + dateType, locale, useSQLDTTZ ? sqlDateAndTimeTimeZone : timeZone, isSQLDateOrTime); + } + + /** + * Gets a {@link TemplateDateFormat} for the specified parameters. This is mostly meant to be used by + * {@link TemplateDateFormatFactory} implementations to delegate to a format based on a specific format string. It + * works well for that, as its parameters are the same low level values as the parameters of + * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)}. For other tasks + * consider the other overloads of this method. + * + * <p> + * Note on performance (which was true at least for 2.3.24): Unless the locale happens to be equal to the current + * locale and the time zone with one of the current time zones ({@link #getTimeZone()} or + * {@link #getSQLDateAndTimeTimeZone()}), the {@link Environment}-level format cache can't be used, so the format + * string has to be parsed and the matching factory has to be get an invoked, which is much more expensive than + * getting the format from the cache. Thus the returned format should be stored by the caller for later reuse (but + * only within the current thread and in relation to the current {@link Environment}), if it will be needed + * frequently. + * + * @param formatString + * Like {@code "iso m"} or {@code "dd.MM.yyyy HH:mm"} or {@code "@somethingCustom"} or + * {@code "@somethingCustom params"} + * @param dateType + * The FTL date type; see the similar parameter of + * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} + * @param timeZone + * Not {@code null}; See the similar parameter of + * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} + * @param locale + * Not {@code null}; See the similar parameter of + * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} + * @param zonelessInput + * See the similar parameter of + * {@link TemplateDateFormatFactory#get(String, int, Locale, TimeZone, boolean, Environment)} + * + * @since 2.3.24 + */ + public TemplateDateFormat getTemplateDateFormat( + String formatString, + int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput) + throws TemplateValueFormatException { + Locale currentLocale = getLocale(); + if (locale.equals(currentLocale)) { + int equalCurrentTZ; + TimeZone currentTimeZone = getTimeZone(); + if (timeZone.equals(currentTimeZone)) { + equalCurrentTZ = 1; + } else { + TimeZone currentSQLDTTimeZone = getSQLDateAndTimeTimeZone(); + if (timeZone.equals(currentSQLDTTimeZone)) { + equalCurrentTZ = 2; + } else { + equalCurrentTZ = 0; + } + } + if (equalCurrentTZ != 0) { + return getTemplateDateFormat(formatString, dateType, equalCurrentTZ == 2, zonelessInput, true); + } + // Falls through + } + return getTemplateDateFormatWithoutCache(formatString, dateType, locale, timeZone, zonelessInput); + } + + TemplateDateFormat getTemplateDateFormat(TemplateDateModel tdm, ASTExpression tdmSourceExpr, boolean useTempModelExc) + throws TemplateException { + Date date = _EvalUtil.modelToDate(tdm, tdmSourceExpr); + + return getTemplateDateFormat( + tdm.getDateType(), date.getClass(), tdmSourceExpr, + useTempModelExc); + } + + /** + * Same as {@link #getTemplateDateFormat(int, Class)}, but translates the exceptions to {@link TemplateException}-s. + */ + TemplateDateFormat getTemplateDateFormat( + int dateType, Class<? extends Date> dateClass, ASTExpression blamedDateSourceExp, boolean useTempModelExc) + throws TemplateException { + try { + return getTemplateDateFormat(dateType, dateClass); + } catch (UnknownDateTypeFormattingUnsupportedException e) { + throw MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e); + } catch (TemplateValueFormatException e) { + String settingName; + String settingValue; + switch (dateType) { + case TemplateDateModel.TIME: + settingName = MutableProcessingConfiguration.TIME_FORMAT_KEY; + settingValue = getTimeFormat(); + break; + case TemplateDateModel.DATE: + settingName = MutableProcessingConfiguration.DATE_FORMAT_KEY; + settingValue = getDateFormat(); + break; + case TemplateDateModel.DATETIME: + settingName = MutableProcessingConfiguration.DATETIME_FORMAT_KEY; + settingValue = getDateTimeFormat(); + break; + default: + settingName = "???"; + settingValue = "???"; + } + + _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( + "The value of the \"", settingName, + "\" FreeMarker configuration setting is a malformed date/time/datetime format string: ", + new _DelayedJQuote(settingValue), ". Reason given: ", + e.getMessage()); + throw useTempModelExc ? new _TemplateModelException(e, desc) : new _MiscTemplateException(e, desc); + } + } + + /** + * Same as {@link #getTemplateDateFormat(String, int, Class)}, but translates the exceptions to + * {@link TemplateException}-s. + */ + TemplateDateFormat getTemplateDateFormat( + String formatString, int dateType, Class<? extends Date> dateClass, + ASTExpression blamedDateSourceExp, ASTExpression blamedFormatterExp, + boolean useTempModelExc) + throws TemplateException { + try { + return getTemplateDateFormat(formatString, dateType, dateClass); + } catch (UnknownDateTypeFormattingUnsupportedException e) { + throw MessageUtil.newCantFormatUnknownTypeDateException(blamedDateSourceExp, e); + } catch (TemplateValueFormatException e) { + _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( + "Can't invoke date/time/datetime format based on format string ", + new _DelayedJQuote(formatString), ". Reason given: ", + e.getMessage()) + .blame(blamedFormatterExp); + throw useTempModelExc ? new _TemplateModelException(e, desc) : new _MiscTemplateException(e, desc); + } + } + + /** + * Used to get the {@link TemplateDateFormat} according the date/time/datetime format settings, for the current + * locale and time zone. See {@link #getTemplateDateFormat(String, int, Locale, TimeZone, boolean)} for the meaning + * of some of the parameters. + */ + private TemplateDateFormat getTemplateDateFormat(int dateType, boolean useSQLDTTZ, boolean zonelessInput) + throws TemplateValueFormatException { + if (dateType == TemplateDateModel.UNKNOWN) { + throw new UnknownDateTypeFormattingUnsupportedException(); + } + int cacheIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTZ); + TemplateDateFormat[] cachedTempDateFormatArray = this.cachedTempDateFormatArray; + if (cachedTempDateFormatArray == null) { + cachedTempDateFormatArray = new TemplateDateFormat[CACHED_TDFS_LENGTH]; + this.cachedTempDateFormatArray = cachedTempDateFormatArray; + } + TemplateDateFormat format = cachedTempDateFormatArray[cacheIdx]; + if (format == null) { + final String formatString; + switch (dateType) { + case TemplateDateModel.TIME: + formatString = getTimeFormat(); + break; + case TemplateDateModel.DATE: + formatString = getDateFormat(); + break; + case TemplateDateModel.DATETIME: + formatString = getDateTimeFormat(); + break; + default: + throw new IllegalArgumentException("Invalid date type enum: " + Integer.valueOf(dateType)); + } + + format = getTemplateDateFormat(formatString, dateType, useSQLDTTZ, zonelessInput, false); + + cachedTempDateFormatArray[cacheIdx] = format; + } + return format; + } + + /** + * Used to get the {@link TemplateDateFormat} for the specified parameters, using the {@link Environment}-level + * cache. As the {@link Environment}-level cache currently only stores formats for the current locale and time zone, + * there's no parameter to specify those. + * + * @param cacheResult + * If the results should stored in the {@link Environment}-level cache. It will still try to get the + * result from the cache regardless of this parameter. + */ + private TemplateDateFormat getTemplateDateFormat( + String formatString, int dateType, boolean useSQLDTTimeZone, boolean zonelessInput, + boolean cacheResult) + throws TemplateValueFormatException { + HashMap<String, TemplateDateFormat> cachedFormatsByFormatString; + readFromCache: do { + HashMap<String, TemplateDateFormat>[] cachedTempDateFormatsByFmtStrArray = this.cachedTempDateFormatsByFmtStrArray; + if (cachedTempDateFormatsByFmtStrArray == null) { + if (cacheResult) { + cachedTempDateFormatsByFmtStrArray = new HashMap[CACHED_TDFS_LENGTH]; + this.cachedTempDateFormatsByFmtStrArray = cachedTempDateFormatsByFmtStrArray; + } else { + cachedFormatsByFormatString = null; + break readFromCache; + } + } + + TemplateDateFormat format; + { + int cacheArrIdx = getTemplateDateFormatCacheArrayIndex(dateType, zonelessInput, useSQLDTTimeZone); + cachedFormatsByFormatString = cachedTempDateFormatsByFmtStrArray[cacheArrIdx]; + if (cachedFormatsByFormatString == null) { + if (cacheResult) { + cachedFormatsByFormatString = new HashMap<>(4); + cachedTempDateFormatsByFmtStrArray[cacheArrIdx] = cachedFormatsByFormatString; + format = null; + } else { + break readFromCache; + } + } else { + format = cachedFormatsByFormatString.get(formatString); + } + } + + if (format != null) { + return format; + } + // Cache miss; falls through + } while (false); + + TemplateDateFormat format = getTemplateDateFormatWithoutCache( + formatString, + dateType, getLocale(), useSQLDTTimeZone ? getSQLDateAndTimeTimeZone() : getTimeZone(), + zonelessInput); + if (cacheResult) { + // We know here that cachedFormatsByFormatString != null + cachedFormatsByFormatString.put(formatString, format); + } + return format; + } + + /** + * Returns the {@link TemplateDateFormat} for the given parameters without using the {@link Environment}-level + * cache. Of course, the {@link TemplateDateFormatFactory} involved might still uses its own cache, which can be + * global (class-loader-level) or {@link Environment}-level. + * + * @param formatString + * See the similar parameter of {@link TemplateDateFormatFactory#get} + * @param dateType + * See the similar parameter of {@link TemplateDateFormatFactory#get} + * @param zonelessInput + * See the similar parameter of {@link TemplateDateFormatFactory#get} + */ + private TemplateDateFormat getTemplateDateFormatWithoutCache( + String formatString, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput) + throws TemplateValueFormatException { + final int formatStringLen = formatString.length(); + final String formatParams; + + TemplateDateFormatFactory formatFactory; + char firstChar = formatStringLen != 0 ? formatString.charAt(0) : 0; + + // As of Java 8, 'x' and 'i' (lower case) are illegal date format letters, so this is backward-compatible. + if (firstChar == 'x' + && formatStringLen > 1 + && formatString.charAt(1) == 's') { + formatFactory = XSTemplateDateFormatFactory.INSTANCE; + formatParams = formatString; // for speed, we don't remove the prefix + } else if (firstChar == 'i' + && formatStringLen > 2 + && formatString.charAt(1) == 's' + && formatString.charAt(2) == 'o') { + formatFactory = ISOTemplateDateFormatFactory.INSTANCE; + formatParams = formatString; // for speed, we don't remove the prefix + } else if (firstChar == '@' + && formatStringLen > 1 + && Character.isLetter(formatString.charAt(1))) { + final String name; + { + int endIdx; + findParamsStart: for (endIdx = 1; endIdx < formatStringLen; endIdx++) { + char c = formatString.charAt(endIdx); + if (c == ' ' || c == '_') { + break findParamsStart; + } + } + name = formatString.substring(1, endIdx); + formatParams = endIdx < formatStringLen ? formatString.substring(endIdx + 1) : ""; + } + + formatFactory = getCustomDateFormat(name); + if (formatFactory == null) { + throw new UndefinedCustomFormatException( + "No custom date format was defined with name " + _StringUtil.jQuote(name)); + } + } else { + formatParams = formatString; + formatFactory = JavaTemplateDateFormatFactory.INSTANCE; + } + + return formatFactory.get(formatParams, dateType, locale, timeZone, + zonelessInput, this); + } + + boolean shouldUseSQLDTTZ(Class dateClass) { + // Attention! If you update this method, update all overloads of it! + return dateClass != Date.class // This pre-condition is only for speed + && !isSQLDateAndTimeTimeZoneSameAsNormal() + && isSQLDateOrTimeClass(dateClass); + } + + private boolean shouldUseSQLDTTimeZone(boolean sqlDateOrTime) { + // Attention! If you update this method, update all overloads of it! + return sqlDateOrTime && !isSQLDateAndTimeTimeZoneSameAsNormal(); + } + + /** + * Tells if the given class is or is subclass of {@link java.sql.Date} or {@link java.sql.Time}. + */ + private static boolean isSQLDateOrTimeClass(Class dateClass) { +
<TRUNCATED>
