http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java new file mode 100644 index 0000000..a8fc5ae --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java @@ -0,0 +1,991 @@ +/* + * 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.Reader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.outputformat.OutputFormat; +import org.apache.freemarker.core.util.CommonBuilder; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; + +/** + * A partial set of configuration settings used for customizing the {@link Configuration}-level settings for individual + * {@link Template}-s (or rather, for a group of templates). That it's partial means that you should call the + * corresponding {@code isXxxSet()} before getting a settings, or else you may cause + * {@link SettingValueNotSetException}. (The fallback to the {@link Configuration} setting isn't automatic to keep + * the dependency graph of configuration related beans non-cyclic. As user code seldom reads settings from here anyway, + * this compromise was chosen.) + * <p> + * Note on the {@code locale} setting: When used with the standard template loading/caching mechanism ({@link + * Configuration#getTemplate(String)} and its overloads), localized lookup happens before the {@code locale} specified + * here could have effect. The {@code locale} will be only set in the template that the localized lookup has already + * found. + * <p> + * This class is immutable. Use {@link TemplateConfiguration.Builder} to create a new instance. + * + * @see Template#Template(String, String, Reader, Configuration, TemplateConfiguration, Charset) + */ +public final class TemplateConfiguration implements ParsingAndProcessingConfiguration { + + private final Locale locale; + private final String numberFormat; + private final String timeFormat; + private final String dateFormat; + private final String dateTimeFormat; + private final TimeZone timeZone; + private final TimeZone sqlDateAndTimeTimeZone; + private final boolean sqlDateAndTimeTimeZoneSet; + private final String booleanFormat; + private final TemplateExceptionHandler templateExceptionHandler; + private final ArithmeticEngine arithmeticEngine; + private final ObjectWrapper objectWrapper; + private final Charset outputEncoding; + private final boolean outputEncodingSet; + private final Charset urlEscapingCharset; + private final boolean urlEscapingCharsetSet; + private final Boolean autoFlush; + private final TemplateClassResolver newBuiltinClassResolver; + private final Boolean showErrorTips; + private final Boolean apiBuiltinEnabled; + private final Boolean logTemplateExceptions; + private final Map<String, TemplateDateFormatFactory> customDateFormats; + private final Map<String, TemplateNumberFormatFactory> customNumberFormats; + private final Map<String, String> autoImports; + private final List<String> autoIncludes; + private final Boolean lazyImports; + private final Boolean lazyAutoImports; + private final boolean lazyAutoImportsSet; + private final Map<Object, Object> customAttributes; + + private final TemplateLanguage templateLanguage; + private final Integer tagSyntax; + private final Integer namingConvention; + private final Boolean whitespaceStripping; + private final Integer autoEscapingPolicy; + private final Boolean recognizeStandardFileExtensions; + private final OutputFormat outputFormat; + private final Charset sourceEncoding; + private final Integer tabSize; + + private TemplateConfiguration(Builder builder) { + locale = builder.isLocaleSet() ? builder.getLocale() : null; + numberFormat = builder.isNumberFormatSet() ? builder.getNumberFormat() : null; + timeFormat = builder.isTimeFormatSet() ? builder.getTimeFormat() : null; + dateFormat = builder.isDateFormatSet() ? builder.getDateFormat() : null; + dateTimeFormat = builder.isDateTimeFormatSet() ? builder.getDateTimeFormat() : null; + timeZone = builder.isTimeZoneSet() ? builder.getTimeZone() : null; + sqlDateAndTimeTimeZoneSet = builder.isSQLDateAndTimeTimeZoneSet(); + sqlDateAndTimeTimeZone = sqlDateAndTimeTimeZoneSet ? builder.getSQLDateAndTimeTimeZone() : null; + booleanFormat = builder.isBooleanFormatSet() ? builder.getBooleanFormat() : null; + templateExceptionHandler = builder.isTemplateExceptionHandlerSet() ? builder.getTemplateExceptionHandler() : null; + arithmeticEngine = builder.isArithmeticEngineSet() ? builder.getArithmeticEngine() : null; + objectWrapper = builder.isObjectWrapperSet() ? builder.getObjectWrapper() : null; + outputEncodingSet = builder.isOutputEncodingSet(); + outputEncoding = outputEncodingSet ? builder.getOutputEncoding() : null; + urlEscapingCharsetSet = builder.isURLEscapingCharsetSet(); + urlEscapingCharset = urlEscapingCharsetSet ? builder.getURLEscapingCharset() : null; + autoFlush = builder.isAutoFlushSet() ? builder.getAutoFlush() : null; + newBuiltinClassResolver = builder.isNewBuiltinClassResolverSet() ? builder.getNewBuiltinClassResolver() : null; + showErrorTips = builder.isShowErrorTipsSet() ? builder.getShowErrorTips() : null; + apiBuiltinEnabled = builder.isAPIBuiltinEnabledSet() ? builder.getAPIBuiltinEnabled() : null; + logTemplateExceptions = builder.isLogTemplateExceptionsSet() ? builder.getLogTemplateExceptions() : null; + customDateFormats = builder.isCustomDateFormatsSet() ? builder.getCustomDateFormats() : null; + customNumberFormats = builder.isCustomNumberFormatsSet() ? builder.getCustomNumberFormats() : null; + autoImports = builder.isAutoImportsSet() ? builder.getAutoImports() : null; + autoIncludes = builder.isAutoIncludesSet() ? builder.getAutoIncludes() : null; + lazyImports = builder.isLazyImportsSet() ? builder.getLazyImports() : null; + lazyAutoImportsSet = builder.isLazyAutoImportsSet(); + lazyAutoImports = lazyAutoImportsSet ? builder.getLazyAutoImports() : null; + customAttributes = builder.isCustomAttributesSet() ? builder.getCustomAttributes() : null; + + templateLanguage = builder.isTemplateLanguageSet() ? builder.getTemplateLanguage() : null; + tagSyntax = builder.isTagSyntaxSet() ? builder.getTagSyntax() : null; + namingConvention = builder.isNamingConventionSet() ? builder.getNamingConvention() : null; + whitespaceStripping = builder.isWhitespaceStrippingSet() ? builder.getWhitespaceStripping() : null; + autoEscapingPolicy = builder.isAutoEscapingPolicySet() ? builder.getAutoEscapingPolicy() : null; + recognizeStandardFileExtensions = builder.isRecognizeStandardFileExtensionsSet() ? builder.getRecognizeStandardFileExtensions() : null; + outputFormat = builder.isOutputFormatSet() ? builder.getOutputFormat() : null; + sourceEncoding = builder.isSourceEncodingSet() ? builder.getSourceEncoding() : null; + tabSize = builder.isTabSizeSet() ? builder.getTabSize() : null; + } + + private static <K,V> Map<K,V> mergeMaps(Map<K,V> m1, Map<K,V> m2, boolean overwriteUpdatesOrder) { + if (m1 == null) return m2; + if (m2 == null) return m1; + if (m1.isEmpty()) return m2; + if (m2.isEmpty()) return m1; + + LinkedHashMap<K, V> mergedM = new LinkedHashMap<>((m1.size() + m2.size()) * 4 / 3 + 1, 0.75f); + mergedM.putAll(m1); + if (overwriteUpdatesOrder) { + for (K m2Key : m2.keySet()) { + mergedM.remove(m2Key); // So that duplicate keys are moved after m1 keys + } + } + mergedM.putAll(m2); + return mergedM; + } + + private static List<String> mergeLists(List<String> list1, List<String> list2) { + if (list1 == null) return list2; + if (list2 == null) return list1; + if (list1.isEmpty()) return list2; + if (list2.isEmpty()) return list1; + + ArrayList<String> mergedList = new ArrayList<>(list1.size() + list2.size()); + mergedList.addAll(list1); + mergedList.addAll(list2); + return mergedList; + } + + /** + * For internal usage only, copies the custom attributes set directly on this objects into another + * {@link MutableProcessingConfiguration}. The target {@link MutableProcessingConfiguration} is assumed to be not seen be other thread than the current + * one yet. (That is, the operation is not synchronized on the target {@link MutableProcessingConfiguration}, only on the source + * {@link MutableProcessingConfiguration}) + * + * @since 2.3.24 + */ + private void copyDirectCustomAttributes(MutableProcessingConfiguration<?> target, boolean overwriteExisting) { + if (customAttributes == null) { + return; + } + for (Map.Entry<?, ?> custAttrEnt : customAttributes.entrySet()) { + Object custAttrKey = custAttrEnt.getKey(); + if (overwriteExisting || !target.isCustomAttributeSet(custAttrKey)) { + target.setCustomAttribute(custAttrKey, custAttrEnt.getValue()); + } + } + } + + @Override + public int getTagSyntax() { + if (!isTagSyntaxSet()) { + throw new SettingValueNotSetException("tagSyntax"); + } + return tagSyntax; + } + + @Override + public boolean isTagSyntaxSet() { + return tagSyntax != null; + } + + @Override + public TemplateLanguage getTemplateLanguage() { + if (!isTemplateLanguageSet()) { + throw new SettingValueNotSetException("templateLanguage"); + } + return templateLanguage; + } + + @Override + public boolean isTemplateLanguageSet() { + return templateLanguage != null; + } + + @Override + public int getNamingConvention() { + if (!isNamingConventionSet()) { + throw new SettingValueNotSetException("namingConvention"); + } + return namingConvention; + } + + @Override + public boolean isNamingConventionSet() { + return namingConvention != null; + } + + @Override + public boolean getWhitespaceStripping() { + if (!isWhitespaceStrippingSet()) { + throw new SettingValueNotSetException("whitespaceStripping"); + } + return whitespaceStripping; + } + + @Override + public boolean isWhitespaceStrippingSet() { + return whitespaceStripping != null; + } + + @Override + public int getAutoEscapingPolicy() { + if (!isAutoEscapingPolicySet()) { + throw new SettingValueNotSetException("autoEscapingPolicy"); + } + return autoEscapingPolicy; + } + + @Override + public boolean isAutoEscapingPolicySet() { + return autoEscapingPolicy != null; + } + + @Override + public OutputFormat getOutputFormat() { + if (!isOutputFormatSet()) { + throw new SettingValueNotSetException("outputFormat"); + } + return outputFormat; + } + + @Override + public ArithmeticEngine getArithmeticEngine() { + if (!isArithmeticEngineSet()) { + throw new SettingValueNotSetException("arithmeticEngine"); + } + return arithmeticEngine; + } + + @Override + public boolean isArithmeticEngineSet() { + return arithmeticEngine != null; + } + + @Override + public boolean isOutputFormatSet() { + return outputFormat != null; + } + + @Override + public boolean getRecognizeStandardFileExtensions() { + if (!isRecognizeStandardFileExtensionsSet()) { + throw new SettingValueNotSetException("recognizeStandardFileExtensions"); + } + return recognizeStandardFileExtensions; + } + + @Override + public boolean isRecognizeStandardFileExtensionsSet() { + return recognizeStandardFileExtensions != null; + } + + @Override + public Charset getSourceEncoding() { + if (!isSourceEncodingSet()) { + throw new SettingValueNotSetException("sourceEncoding"); + } + return sourceEncoding; + } + + @Override + public boolean isSourceEncodingSet() { + return sourceEncoding != null; + } + + @Override + public int getTabSize() { + if (!isTabSizeSet()) { + throw new SettingValueNotSetException("tabSize"); + } + return tabSize; + } + + @Override + public boolean isTabSizeSet() { + return tabSize != null; + } + + /** + * Always throws {@link SettingValueNotSetException}, as this can't be set on the {@link TemplateConfiguration} + * level. + */ + @Override + public Version getIncompatibleImprovements() { + throw new SettingValueNotSetException("incompatibleImprovements"); + } + + @Override + public Locale getLocale() { + if (!isLocaleSet()) { + throw new SettingValueNotSetException("locale"); + } + return locale; + } + + @Override + public boolean isLocaleSet() { + return locale != null; + } + + @Override + public TimeZone getTimeZone() { + if (!isTimeZoneSet()) { + throw new SettingValueNotSetException("timeZone"); + } + return timeZone; + } + + @Override + public boolean isTimeZoneSet() { + return timeZone != null; + } + + @Override + public TimeZone getSQLDateAndTimeTimeZone() { + if (!isSQLDateAndTimeTimeZoneSet()) { + throw new SettingValueNotSetException("sqlDateAndTimeTimeZone"); + } + return sqlDateAndTimeTimeZone; + } + + @Override + public boolean isSQLDateAndTimeTimeZoneSet() { + return sqlDateAndTimeTimeZoneSet; + } + + @Override + public String getNumberFormat() { + if (!isNumberFormatSet()) { + throw new SettingValueNotSetException("numberFormat"); + } + return numberFormat; + } + + @Override + public boolean isNumberFormatSet() { + return numberFormat != null; + } + + @Override + public Map<String, TemplateNumberFormatFactory> getCustomNumberFormats() { + if (!isCustomNumberFormatsSet()) { + throw new SettingValueNotSetException("customNumberFormats"); + } + return customNumberFormats; + } + + @Override + public TemplateNumberFormatFactory getCustomNumberFormat(String name) { + return getCustomNumberFormats().get(name); + } + + @Override + public boolean isCustomNumberFormatsSet() { + return customNumberFormats != null; + } + + @Override + public String getBooleanFormat() { + if (!isBooleanFormatSet()) { + throw new SettingValueNotSetException("booleanFormat"); + } + return booleanFormat; + } + + @Override + public boolean isBooleanFormatSet() { + return booleanFormat != null; + } + + @Override + public String getTimeFormat() { + if (!isTimeFormatSet()) { + throw new SettingValueNotSetException("timeFormat"); + } + return timeFormat; + } + + @Override + public boolean isTimeFormatSet() { + return timeFormat != null; + } + + @Override + public String getDateFormat() { + if (!isDateFormatSet()) { + throw new SettingValueNotSetException("dateFormat"); + } + return dateFormat; + } + + @Override + public boolean isDateFormatSet() { + return dateFormat != null; + } + + @Override + public String getDateTimeFormat() { + if (!isDateTimeFormatSet()) { + throw new SettingValueNotSetException("dateTimeFormat"); + } + return dateTimeFormat; + } + + @Override + public boolean isDateTimeFormatSet() { + return dateTimeFormat != null; + } + + @Override + public Map<String, TemplateDateFormatFactory> getCustomDateFormats() { + if (!isCustomDateFormatsSet()) { + throw new SettingValueNotSetException("customDateFormats"); + } + return customDateFormats; + } + + @Override + public TemplateDateFormatFactory getCustomDateFormat(String name) { + if (isCustomDateFormatsSet()) { + TemplateDateFormatFactory format = customDateFormats.get(name); + if (format != null) { + return format; + } + } + return null; + } + + @Override + public boolean isCustomDateFormatsSet() { + return customDateFormats != null; + } + + @Override + public TemplateExceptionHandler getTemplateExceptionHandler() { + if (!isTemplateExceptionHandlerSet()) { + throw new SettingValueNotSetException("templateExceptionHandler"); + } + return templateExceptionHandler; + } + + @Override + public boolean isTemplateExceptionHandlerSet() { + return templateExceptionHandler != null; + } + + @Override + public ObjectWrapper getObjectWrapper() { + if (!isObjectWrapperSet()) { + throw new SettingValueNotSetException("objectWrapper"); + } + return objectWrapper; + } + + @Override + public boolean isObjectWrapperSet() { + return objectWrapper != null; + } + + @Override + public Charset getOutputEncoding() { + if (!isOutputEncodingSet()) { + throw new SettingValueNotSetException(""); + } + return outputEncoding; + } + + @Override + public boolean isOutputEncodingSet() { + return outputEncodingSet; + } + + @Override + public Charset getURLEscapingCharset() { + if (!isURLEscapingCharsetSet()) { + throw new SettingValueNotSetException("urlEscapingCharset"); + } + return urlEscapingCharset; + } + + @Override + public boolean isURLEscapingCharsetSet() { + return urlEscapingCharsetSet; + } + + @Override + public TemplateClassResolver getNewBuiltinClassResolver() { + if (!isNewBuiltinClassResolverSet()) { + throw new SettingValueNotSetException("newBuiltinClassResolver"); + } + return newBuiltinClassResolver; + } + + @Override + public boolean isNewBuiltinClassResolverSet() { + return newBuiltinClassResolver != null; + } + + @Override + public boolean getAPIBuiltinEnabled() { + if (!isAPIBuiltinEnabledSet()) { + throw new SettingValueNotSetException("apiBuiltinEnabled"); + } + return apiBuiltinEnabled; + } + + @Override + public boolean isAPIBuiltinEnabledSet() { + return apiBuiltinEnabled != null; + } + + @Override + public boolean getAutoFlush() { + if (!isAutoFlushSet()) { + throw new SettingValueNotSetException("autoFlush"); + } + return autoFlush; + } + + @Override + public boolean isAutoFlushSet() { + return autoFlush != null; + } + + @Override + public boolean getShowErrorTips() { + if (!isShowErrorTipsSet()) { + throw new SettingValueNotSetException("showErrorTips"); + } + return showErrorTips; + } + + @Override + public boolean isShowErrorTipsSet() { + return showErrorTips != null; + } + + @Override + public boolean getLogTemplateExceptions() { + if (!isLogTemplateExceptionsSet()) { + throw new SettingValueNotSetException("logTemplateExceptions"); + } + return logTemplateExceptions; + } + + @Override + public boolean isLogTemplateExceptionsSet() { + return logTemplateExceptions != null; + } + + @Override + public boolean getLazyImports() { + if (!isLazyImportsSet()) { + throw new SettingValueNotSetException("lazyImports"); + } + return lazyImports; + } + + @Override + public boolean isLazyImportsSet() { + return lazyImports != null; + } + + @Override + public Boolean getLazyAutoImports() { + if (!isLazyAutoImportsSet()) { + throw new SettingValueNotSetException("lazyAutoImports"); + } + return lazyAutoImports; + } + + @Override + public boolean isLazyAutoImportsSet() { + return lazyAutoImportsSet; + } + + @Override + public Map<String, String> getAutoImports() { + if (!isAutoImportsSet()) { + throw new SettingValueNotSetException(""); + } + return autoImports; + } + + @Override + public boolean isAutoImportsSet() { + return autoImports != null; + } + + @Override + public List<String> getAutoIncludes() { + if (!isAutoIncludesSet()) { + throw new SettingValueNotSetException("autoIncludes"); + } + return autoIncludes; + } + + @Override + public boolean isAutoIncludesSet() { + return autoIncludes != null; + } + + @Override + public Map<Object, Object> getCustomAttributes() { + if (!isCustomAttributesSet()) { + throw new SettingValueNotSetException("customAttributes"); + } + return customAttributes; + } + + @Override + public boolean isCustomAttributesSet() { + return customAttributes != null; + } + + @Override + public Object getCustomAttribute(Object name) { + Object attValue; + if (isCustomAttributesSet()) { + attValue = customAttributes.get(name); + if (attValue != null || customAttributes.containsKey(name)) { + return attValue; + } + } + return null; + } + + public static final class Builder extends MutableParsingAndProcessingConfiguration<Builder> + implements CommonBuilder<TemplateConfiguration> { + + public Builder() { + super(); + } + + @Override + public TemplateConfiguration build() { + return new TemplateConfiguration(this); + } + + @Override + protected Locale getDefaultLocale() { + throw new SettingValueNotSetException("locale"); + } + + @Override + protected TimeZone getDefaultTimeZone() { + throw new SettingValueNotSetException("timeZone"); + } + + @Override + protected TimeZone getDefaultSQLDateAndTimeTimeZone() { + throw new SettingValueNotSetException("SQLDateAndTimeTimeZone"); + } + + @Override + protected String getDefaultNumberFormat() { + throw new SettingValueNotSetException("numberFormat"); + } + + @Override + protected Map<String, TemplateNumberFormatFactory> getDefaultCustomNumberFormats() { + throw new SettingValueNotSetException("customNumberFormats"); + } + + @Override + protected TemplateNumberFormatFactory getDefaultCustomNumberFormat(String name) { + return null; + } + + @Override + protected String getDefaultBooleanFormat() { + throw new SettingValueNotSetException("booleanFormat"); + } + + @Override + protected String getDefaultTimeFormat() { + throw new SettingValueNotSetException("timeFormat"); + } + + @Override + protected String getDefaultDateFormat() { + throw new SettingValueNotSetException("dateFormat"); + } + + @Override + protected String getDefaultDateTimeFormat() { + throw new SettingValueNotSetException("dateTimeFormat"); + } + + @Override + protected Map<String, TemplateDateFormatFactory> getDefaultCustomDateFormats() { + throw new SettingValueNotSetException("customDateFormats"); + } + + @Override + protected TemplateDateFormatFactory getDefaultCustomDateFormat(String name) { + throw new SettingValueNotSetException("customDateFormat"); + } + + @Override + protected TemplateExceptionHandler getDefaultTemplateExceptionHandler() { + throw new SettingValueNotSetException("templateExceptionHandler"); + } + + @Override + protected ArithmeticEngine getDefaultArithmeticEngine() { + throw new SettingValueNotSetException("arithmeticEngine"); + } + + @Override + protected ObjectWrapper getDefaultObjectWrapper() { + throw new SettingValueNotSetException("objectWrapper"); + } + + @Override + protected Charset getDefaultOutputEncoding() { + throw new SettingValueNotSetException("outputEncoding"); + } + + @Override + protected Charset getDefaultURLEscapingCharset() { + throw new SettingValueNotSetException("URLEscapingCharset"); + } + + @Override + protected TemplateClassResolver getDefaultNewBuiltinClassResolver() { + throw new SettingValueNotSetException("newBuiltinClassResolver"); + } + + @Override + protected boolean getDefaultAutoFlush() { + throw new SettingValueNotSetException("autoFlush"); + } + + @Override + protected boolean getDefaultShowErrorTips() { + throw new SettingValueNotSetException("showErrorTips"); + } + + @Override + protected boolean getDefaultAPIBuiltinEnabled() { + throw new SettingValueNotSetException("APIBuiltinEnabled"); + } + + @Override + protected boolean getDefaultLogTemplateExceptions() { + throw new SettingValueNotSetException("logTemplateExceptions"); + } + + @Override + protected boolean getDefaultLazyImports() { + throw new SettingValueNotSetException("lazyImports"); + } + + @Override + protected Boolean getDefaultLazyAutoImports() { + throw new SettingValueNotSetException("lazyAutoImports"); + } + + @Override + protected Map<String, String> getDefaultAutoImports() { + throw new SettingValueNotSetException("autoImports"); + } + + @Override + protected List<String> getDefaultAutoIncludes() { + throw new SettingValueNotSetException("autoIncludes"); + } + + @Override + protected Object getDefaultCustomAttribute(Object name) { + return null; + } + + @Override + protected Map<Object, Object> getDefaultCustomAttributes() { + throw new SettingValueNotSetException("customAttributes"); + } + + /** + * Set all settings in this {@link Builder} that were set in the parameter + * {@link TemplateConfiguration}, possibly overwriting the earlier value in this object. (A setting is said to be + * set in a {@link TemplateConfiguration} if it was explicitly set via a setter method, as opposed to be inherited.) + */ + public void merge(ParsingAndProcessingConfiguration tc) { + if (tc.isAPIBuiltinEnabledSet()) { + setAPIBuiltinEnabled(tc.getAPIBuiltinEnabled()); + } + if (tc.isArithmeticEngineSet()) { + setArithmeticEngine(tc.getArithmeticEngine()); + } + if (tc.isAutoEscapingPolicySet()) { + setAutoEscapingPolicy(tc.getAutoEscapingPolicy()); + } + if (tc.isAutoFlushSet()) { + setAutoFlush(tc.getAutoFlush()); + } + if (tc.isBooleanFormatSet()) { + setBooleanFormat(tc.getBooleanFormat()); + } + if (tc.isCustomDateFormatsSet()) { + setCustomDateFormats(mergeMaps( + isCustomDateFormatsSet() ? getCustomDateFormats() : null, tc.getCustomDateFormats(), false)); + } + if (tc.isCustomNumberFormatsSet()) { + setCustomNumberFormats(mergeMaps( + isCustomNumberFormatsSet() ? getCustomNumberFormats() : null, tc.getCustomNumberFormats(), false)); + } + if (tc.isDateFormatSet()) { + setDateFormat(tc.getDateFormat()); + } + if (tc.isDateTimeFormatSet()) { + setDateTimeFormat(tc.getDateTimeFormat()); + } + if (tc.isSourceEncodingSet()) { + setSourceEncoding(tc.getSourceEncoding()); + } + if (tc.isLocaleSet()) { + setLocale(tc.getLocale()); + } + if (tc.isLogTemplateExceptionsSet()) { + setLogTemplateExceptions(tc.getLogTemplateExceptions()); + } + if (tc.isNamingConventionSet()) { + setNamingConvention(tc.getNamingConvention()); + } + if (tc.isNewBuiltinClassResolverSet()) { + setNewBuiltinClassResolver(tc.getNewBuiltinClassResolver()); + } + if (tc.isNumberFormatSet()) { + setNumberFormat(tc.getNumberFormat()); + } + if (tc.isObjectWrapperSet()) { + setObjectWrapper(tc.getObjectWrapper()); + } + if (tc.isOutputEncodingSet()) { + setOutputEncoding(tc.getOutputEncoding()); + } + if (tc.isOutputFormatSet()) { + setOutputFormat(tc.getOutputFormat()); + } + if (tc.isRecognizeStandardFileExtensionsSet()) { + setRecognizeStandardFileExtensions(tc.getRecognizeStandardFileExtensions()); + } + if (tc.isShowErrorTipsSet()) { + setShowErrorTips(tc.getShowErrorTips()); + } + if (tc.isSQLDateAndTimeTimeZoneSet()) { + setSQLDateAndTimeTimeZone(tc.getSQLDateAndTimeTimeZone()); + } + if (tc.isTagSyntaxSet()) { + setTagSyntax(tc.getTagSyntax()); + } + if (tc.isTemplateLanguageSet()) { + setTemplateLanguage(tc.getTemplateLanguage()); + } + if (tc.isTemplateExceptionHandlerSet()) { + setTemplateExceptionHandler(tc.getTemplateExceptionHandler()); + } + if (tc.isTimeFormatSet()) { + setTimeFormat(tc.getTimeFormat()); + } + if (tc.isTimeZoneSet()) { + setTimeZone(tc.getTimeZone()); + } + if (tc.isURLEscapingCharsetSet()) { + setURLEscapingCharset(tc.getURLEscapingCharset()); + } + if (tc.isWhitespaceStrippingSet()) { + setWhitespaceStripping(tc.getWhitespaceStripping()); + } + if (tc.isTabSizeSet()) { + setTabSize(tc.getTabSize()); + } + if (tc.isLazyImportsSet()) { + setLazyImports(tc.getLazyImports()); + } + if (tc.isLazyAutoImportsSet()) { + setLazyAutoImports(tc.getLazyAutoImports()); + } + if (tc.isAutoImportsSet()) { + setAutoImports(mergeMaps( + isAutoImportsSet() ? getAutoImports() : null, + tc.isAutoImportsSet() ? tc.getAutoImports() : null, + true)); + } + if (tc.isAutoIncludesSet()) { + setAutoIncludes(mergeLists( + isAutoIncludesSet() ? getAutoIncludes() : null, + tc.isAutoIncludesSet() ? tc.getAutoIncludes() : null)); + } + + if (tc.isCustomAttributesSet()) { + setCustomAttributesWithoutCopying(mergeMaps( + isCustomAttributesSet() ? getCustomAttributes() : null, + tc.isCustomAttributesSet() ? tc.getCustomAttributes() : null, + true)); + } + } + + @Override + public Version getIncompatibleImprovements() { + throw new SettingValueNotSetException("incompatibleImprovements"); + } + + @Override + protected int getDefaultTagSyntax() { + throw new SettingValueNotSetException("tagSyntax"); + } + + @Override + protected TemplateLanguage getDefaultTemplateLanguage() { + throw new SettingValueNotSetException("templateLanguage"); + } + + @Override + protected int getDefaultNamingConvention() { + throw new SettingValueNotSetException("namingConvention"); + } + + @Override + protected boolean getDefaultWhitespaceStripping() { + throw new SettingValueNotSetException("whitespaceStripping"); + } + + @Override + protected int getDefaultAutoEscapingPolicy() { + throw new SettingValueNotSetException("autoEscapingPolicy"); + } + + @Override + protected OutputFormat getDefaultOutputFormat() { + throw new SettingValueNotSetException("outputFormat"); + } + + @Override + protected boolean getDefaultRecognizeStandardFileExtensions() { + throw new SettingValueNotSetException("recognizeStandardFileExtensions"); + } + + @Override + protected Charset getDefaultSourceEncoding() { + throw new SettingValueNotSetException("sourceEncoding"); + } + + @Override + protected int getDefaultTabSize() { + throw new SettingValueNotSetException("tabSize"); + } + + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementArrayBuilder.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementArrayBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementArrayBuilder.java new file mode 100644 index 0000000..f8fe66b --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementArrayBuilder.java @@ -0,0 +1,102 @@ +/* + * 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 org.apache.freemarker.core.util._CollectionUtil; + +/** + * Holds an buffer (array) of {@link ASTElement}-s with the count of the utilized items in it. The un-utilized tail + * of the array must only contain {@code null}-s. + * + * @since 2.3.24 + */ +class TemplateElements { + + static final TemplateElements EMPTY = new TemplateElements(null, 0); + + private final ASTElement[] buffer; + private final int count; + + /** + * @param buffer + * The buffer; {@code null} exactly if {@code count} is 0. + * @param count + * The number of utilized buffer elements; if 0, then {@code null} must be {@code null}. + */ + TemplateElements(ASTElement[] buffer, int count) { + /* + // Assertion: + if (count == 0 && buffer != null) { + throw new IllegalArgumentException(); + } + */ + + this.buffer = buffer; + this.count = count; + } + + ASTElement[] getBuffer() { + return buffer; + } + + int getCount() { + return count; + } + + ASTElement getFirst() { + return buffer != null ? buffer[0] : null; + } + + ASTElement getLast() { + return buffer != null ? buffer[count - 1] : null; + } + + /** + * Used for some backward compatibility hacks. + */ + ASTElement asSingleElement() { + if (count == 0) { + return new ASTStaticText(_CollectionUtil.EMPTY_CHAR_ARRAY, false); + } else { + ASTElement first = buffer[0]; + if (count == 1) { + return first; + } else { + ASTImplicitParent mixedContent = new ASTImplicitParent(); + mixedContent.setChildren(this); + mixedContent.setLocation(first.getTemplate(), first, getLast()); + return mixedContent; + } + } + } + + /** + * Used for some backward compatibility hacks. + */ + ASTImplicitParent asMixedContent() { + ASTImplicitParent mixedContent = new ASTImplicitParent(); + if (count != 0) { + ASTElement first = buffer[0]; + mixedContent.setChildren(this); + mixedContent.setLocation(first.getTemplate(), first, getLast()); + } + return mixedContent; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementsToVisit.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementsToVisit.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementsToVisit.java new file mode 100644 index 0000000..9aaf0c7 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateElementsToVisit.java @@ -0,0 +1,48 @@ +/* + * 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.util.Collection; +import java.util.Collections; + +/** + * Used as the return value of {@link ASTElement#accept(Environment)} when the invoked element has nested elements + * to invoke. It would be more natural to invoke child elements before returning from + * {@link ASTElement#accept(Environment)}, however, if there's nothing to do after the child elements were invoked, + * that would mean wasting stack space. + * + * @since 2.3.24 + */ +class TemplateElementsToVisit { + + private final Collection<ASTElement> templateElements; + + TemplateElementsToVisit(Collection<ASTElement> templateElements) { + this.templateElements = null != templateElements ? templateElements : Collections.<ASTElement> emptyList(); + } + + TemplateElementsToVisit(ASTElement nestedBlock) { + this(Collections.singleton(nestedBlock)); + } + + Collection<ASTElement> getTemplateElements() { + return templateElements; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateException.java new file mode 100644 index 0000000..3ca9914 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateException.java @@ -0,0 +1,655 @@ +/* + * 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.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Method; + +import org.apache.freemarker.core.util._CollectionUtil; + +/** + * Runtime exception in a template (as opposed to a parsing-time exception: {@link ParseException}). + * It prints a special stack trace that contains the template-language stack trace along the usual Java stack trace. + */ +public class TemplateException extends Exception { + + private static final String FTL_INSTRUCTION_STACK_TRACE_TITLE + = "FTL stack trace (\"~\" means nesting-related):"; + + // Set in constructor: + private transient _ErrorDescriptionBuilder descriptionBuilder; + private final transient Environment env; + private final transient ASTExpression blamedExpression; + private transient ASTElement[] ftlInstructionStackSnapshot; + + // Calculated on demand: + private String renderedFtlInstructionStackSnapshot; // clalc. from ftlInstructionStackSnapshot + private String renderedFtlInstructionStackSnapshotTop; // clalc. from ftlInstructionStackSnapshot + private String description; // calc. from descriptionBuilder, or set by the construcor + private transient String messageWithoutStackTop; + private transient String message; + private boolean blamedExpressionStringCalculated; + private String blamedExpressionString; + private boolean positionsCalculated; + private String templateLookupName; + private String templateSourceName; + private Integer lineNumber; + private Integer columnNumber; + private Integer endLineNumber; + private Integer endColumnNumber; + + // Concurrency: + private transient Object lock = new Object(); + private transient ThreadLocal messageWasAlreadyPrintedForThisTrace; + + /** + * Constructs a TemplateException with no specified detail message + * or underlying cause. + */ + public TemplateException(Environment env) { + this(null, null, env); + } + + /** + * Constructs a TemplateException with the given detail message, + * but no underlying cause exception. + * + * @param description the description of the error that occurred + */ + public TemplateException(String description, Environment env) { + this(description, null, env); + } + + /** + * The same as {@link #TemplateException(Throwable, Environment)}; it's exists only for binary + * backward-compatibility. + */ + public TemplateException(Exception cause, Environment env) { + this(null, cause, env); + } + + /** + * Constructs a TemplateException with the given underlying Exception, + * but no detail message. + * + * @param cause the underlying {@link Exception} that caused this + * exception to be raised + * + * @since 2.3.20 + */ + public TemplateException(Throwable cause, Environment env) { + this(null, cause, env); + } + + /** + * The same as {@link #TemplateException(String, Throwable, Environment)}; it's exists only for binary + * backward-compatibility. + */ + public TemplateException(String description, Exception cause, Environment env) { + this(description, cause, env, null, null); + } + + /** + * Constructs a TemplateException with both a description of the error + * that occurred and the underlying Exception that caused this exception + * to be raised. + * + * @param description the description of the error that occurred + * @param cause the underlying {@link Exception} that caused this exception to be raised + * + * @since 2.3.20 + */ + public TemplateException(String description, Throwable cause, Environment env) { + this(description, cause, env, null, null); + } + + /** + * Don't use this; this is to be used internally by FreeMarker. No backward compatibility guarantees. + * + * @param blamedExpr Maybe {@code null}. The FTL stack in the {@link Environment} only specifies the error location + * with "template element" granularity, and this can be used to point to the expression inside the + * template element. + */ + protected TemplateException(Throwable cause, Environment env, ASTExpression blamedExpr, + _ErrorDescriptionBuilder descriptionBuilder) { + this(null, cause, env, blamedExpr, descriptionBuilder); + } + + private TemplateException( + String renderedDescription, + Throwable cause, + Environment env, ASTExpression blamedExpression, + _ErrorDescriptionBuilder descriptionBuilder) { + // Note: Keep this constructor lightweight. + + super(cause); // Message managed locally. + + if (env == null) env = Environment.getCurrentEnvironment(); + this.env = env; + + this.blamedExpression = blamedExpression; + + this.descriptionBuilder = descriptionBuilder; + description = renderedDescription; + + if (env != null) { + ftlInstructionStackSnapshot = env.getInstructionStackSnapshot(); + } + } + + private void renderMessages() { + String description = getDescription(); + + if (description != null && description.length() != 0) { + messageWithoutStackTop = description; + } else if (getCause() != null) { + messageWithoutStackTop = "No error description was specified for this error; low-level message: " + + getCause().getClass().getName() + ": " + getCause().getMessage(); + } else { + messageWithoutStackTop = "[No error description was available.]"; + } + + String stackTopFew = getFTLInstructionStackTopFew(); + if (stackTopFew != null) { + message = messageWithoutStackTop + "\n\n" + + MessageUtil.ERROR_MESSAGE_HR + "\n" + + FTL_INSTRUCTION_STACK_TRACE_TITLE + "\n" + + stackTopFew + + MessageUtil.ERROR_MESSAGE_HR; + messageWithoutStackTop = message.substring(0, messageWithoutStackTop.length()); // to reuse backing char[] + } else { + message = messageWithoutStackTop; + } + } + + private void calculatePosition() { + synchronized (lock) { + if (!positionsCalculated) { + // The expressions is the argument of the template element, so we prefer it as it's more specific. + ASTNode templateObject = blamedExpression != null + ? blamedExpression + : ( + ftlInstructionStackSnapshot != null && ftlInstructionStackSnapshot.length != 0 + ? ftlInstructionStackSnapshot[0] : null); + // Line number blow 0 means no info, negative means position in ?eval-ed value that we won't use here. + if (templateObject != null && templateObject.getBeginLine() > 0) { + final Template template = templateObject.getTemplate(); + templateLookupName = template.getLookupName(); + templateSourceName = template.getSourceName(); + lineNumber = Integer.valueOf(templateObject.getBeginLine()); + columnNumber = Integer.valueOf(templateObject.getBeginColumn()); + endLineNumber = Integer.valueOf(templateObject.getEndLine()); + endColumnNumber = Integer.valueOf(templateObject.getEndColumn()); + } + positionsCalculated = true; + deleteFTLInstructionStackSnapshotIfNotNeeded(); + } + } + } + + /** + * Returns the snapshot of the FTL stack trace at the time this exception was created. + */ + public String getFTLInstructionStack() { + synchronized (lock) { + if (ftlInstructionStackSnapshot != null || renderedFtlInstructionStackSnapshot != null) { + if (renderedFtlInstructionStackSnapshot == null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + Environment.outputInstructionStack(ftlInstructionStackSnapshot, false, pw); + pw.close(); + if (renderedFtlInstructionStackSnapshot == null) { + renderedFtlInstructionStackSnapshot = sw.toString(); + deleteFTLInstructionStackSnapshotIfNotNeeded(); + } + } + return renderedFtlInstructionStackSnapshot; + } else { + return null; + } + } + } + + private String getFTLInstructionStackTopFew() { + synchronized (lock) { + if (ftlInstructionStackSnapshot != null || renderedFtlInstructionStackSnapshotTop != null) { + if (renderedFtlInstructionStackSnapshotTop == null) { + int stackSize = ftlInstructionStackSnapshot.length; + String s; + if (stackSize == 0) { + s = ""; + } else { + StringWriter sw = new StringWriter(); + Environment.outputInstructionStack(ftlInstructionStackSnapshot, true, sw); + s = sw.toString(); + } + if (renderedFtlInstructionStackSnapshotTop == null) { + renderedFtlInstructionStackSnapshotTop = s; + deleteFTLInstructionStackSnapshotIfNotNeeded(); + } + } + return renderedFtlInstructionStackSnapshotTop.length() != 0 + ? renderedFtlInstructionStackSnapshotTop : null; + } else { + return null; + } + } + } + + private void deleteFTLInstructionStackSnapshotIfNotNeeded() { + if (renderedFtlInstructionStackSnapshot != null && renderedFtlInstructionStackSnapshotTop != null + && (positionsCalculated || blamedExpression != null)) { + ftlInstructionStackSnapshot = null; + } + + } + + private String getDescription() { + synchronized (lock) { + if (description == null && descriptionBuilder != null) { + description = descriptionBuilder.toString( + getFailingInstruction(), + env != null ? env.getShowErrorTips() : true); + descriptionBuilder = null; + } + return description; + } + } + + private ASTElement getFailingInstruction() { + if (ftlInstructionStackSnapshot != null && ftlInstructionStackSnapshot.length > 0) { + return ftlInstructionStackSnapshot[0]; + } else { + return null; + } + } + + /** + * @return the execution environment in which the exception occurred. + * {@code null} if the exception was deserialized. + */ + public Environment getEnvironment() { + return env; + } + + /** + * Overrides {@link Throwable#printStackTrace(PrintStream)} so that it will include the FTL stack trace. + */ + @Override + public void printStackTrace(PrintStream out) { + printStackTrace(out, true, true, true); + } + + /** + * Overrides {@link Throwable#printStackTrace(PrintWriter)} so that it will include the FTL stack trace. + */ + @Override + public void printStackTrace(PrintWriter out) { + printStackTrace(out, true, true, true); + } + + /** + * @param heading should the heading at the top be printed + * @param ftlStackTrace should the FTL stack trace be printed + * @param javaStackTrace should the Java stack trace be printed + * + * @since 2.3.20 + */ + public void printStackTrace(PrintWriter out, boolean heading, boolean ftlStackTrace, boolean javaStackTrace) { + synchronized (out) { + printStackTrace(new PrintWriterStackTraceWriter(out), heading, ftlStackTrace, javaStackTrace); + } + } + + /** + * @param heading should the heading at the top be printed + * @param ftlStackTrace should the FTL stack trace be printed + * @param javaStackTrace should the Java stack trace be printed + * + * @since 2.3.20 + */ + public void printStackTrace(PrintStream out, boolean heading, boolean ftlStackTrace, boolean javaStackTrace) { + synchronized (out) { + printStackTrace(new PrintStreamStackTraceWriter(out), heading, ftlStackTrace, javaStackTrace); + } + } + + private void printStackTrace(StackTraceWriter out, boolean heading, boolean ftlStackTrace, boolean javaStackTrace) { + synchronized (out) { + if (heading) { + out.println("FreeMarker template error:"); + } + + if (ftlStackTrace) { + String stackTrace = getFTLInstructionStack(); + if (stackTrace != null) { + out.println(getMessageWithoutStackTop()); // Not getMessage()! + out.println(); + out.println(MessageUtil.ERROR_MESSAGE_HR); + out.println(FTL_INSTRUCTION_STACK_TRACE_TITLE); + out.print(stackTrace); + out.println(MessageUtil.ERROR_MESSAGE_HR); + } else { + ftlStackTrace = false; + javaStackTrace = true; + } + } + + if (javaStackTrace) { + if (ftlStackTrace) { // We are after an FTL stack trace + out.println(); + out.println("Java stack trace (for programmers):"); + out.println(MessageUtil.ERROR_MESSAGE_HR); + synchronized (lock) { + if (messageWasAlreadyPrintedForThisTrace == null) { + messageWasAlreadyPrintedForThisTrace = new ThreadLocal(); + } + messageWasAlreadyPrintedForThisTrace.set(Boolean.TRUE); + } + + try { + out.printStandardStackTrace(this); + } finally { + messageWasAlreadyPrintedForThisTrace.set(Boolean.FALSE); + } + } else { // javaStackTrace only + out.printStandardStackTrace(this); + } + + if (getCause() != null) { + // Dirty hack to fight with ServletException class whose getCause() method doesn't work properly: + Throwable causeCause = getCause().getCause(); + if (causeCause == null) { + try { + // Reflection is used to prevent dependency on Servlet classes. + Method m = getCause().getClass().getMethod("getRootCause", _CollectionUtil.EMPTY_CLASS_ARRAY); + Throwable rootCause = (Throwable) m.invoke(getCause(), _CollectionUtil.EMPTY_OBJECT_ARRAY); + if (rootCause != null) { + out.println("ServletException root cause: "); + out.printStandardStackTrace(rootCause); + } + } catch (Throwable exc) { + // ignore + } + } + } + } // if (javaStackTrace) + } + } + + /** + * Prints the stack trace as if wasn't overridden by {@link TemplateException}. + * @since 2.3.20 + */ + public void printStandardStackTrace(PrintStream ps) { + super.printStackTrace(ps); + } + + /** + * Prints the stack trace as if wasn't overridden by {@link TemplateException}. + * @since 2.3.20 + */ + public void printStandardStackTrace(PrintWriter pw) { + super.printStackTrace(pw); + } + + @Override + public String getMessage() { + if (messageWasAlreadyPrintedForThisTrace != null + && messageWasAlreadyPrintedForThisTrace.get() == Boolean.TRUE) { + return "[... Exception message was already printed; see it above ...]"; + } else { + synchronized (lock) { + if (message == null) renderMessages(); + return message; + } + } + } + + /** + * Similar to {@link #getMessage()}, but it doesn't contain the position of the failing instruction at then end + * of the text. It might contains the position of the failing <em>expression</em> though as part of the expression + * quotation, as that's the part of the description. + */ + public String getMessageWithoutStackTop() { + synchronized (lock) { + if (messageWithoutStackTop == null) renderMessages(); + return messageWithoutStackTop; + } + } + + /** + * 1-based line number of the failing section, or {@code null} if the information is not available. + * + * @since 2.3.21 + */ + public Integer getLineNumber() { + synchronized (lock) { + if (!positionsCalculated) { + calculatePosition(); + } + return lineNumber; + } + } + + /** + * Returns the {@linkplain Template#getSourceName() source name} of the template where the error has occurred, or + * {@code null} if the information isn't available. This is what should be used for showing the error position. + * + * @since 2.3.22 + */ + public String getTemplateSourceName() { + synchronized (lock) { + if (!positionsCalculated) { + calculatePosition(); + } + return templateSourceName; + } + } + + /** + * Returns the {@linkplain Template#getLookupName()} () lookup name} of the template where the error has + * occurred, or {@code null} if the information isn't available. Do not use this for showing the error position; + * use {@link #getTemplateSourceName()}. + */ + public String getTemplateLookupName() { + synchronized (lock) { + if (!positionsCalculated) { + calculatePosition(); + } + return templateLookupName; + } + } + + /** + * Returns the {@linkplain #getTemplateSourceName() template source name}, or if that's {@code null} then the + * {@linkplain #getTemplateLookupName() template lookup name}. This name is primarily meant to be used in error + * messages. + */ + public String getTemplateSourceOrLookupName() { + return getTemplateSourceName() != null ? getTemplateSourceName() : getTemplateLookupName(); + } + + /** + * 1-based column number of the failing section, or {@code null} if the information is not available. + * + * @since 2.3.21 + */ + public Integer getColumnNumber() { + synchronized (lock) { + if (!positionsCalculated) { + calculatePosition(); + } + return columnNumber; + } + } + + /** + * 1-based line number of the last line that contains the failing section, or {@code null} if the information is not + * available. + * + * @since 2.3.21 + */ + public Integer getEndLineNumber() { + synchronized (lock) { + if (!positionsCalculated) { + calculatePosition(); + } + return endLineNumber; + } + } + + /** + * 1-based column number of the last character of the failing template section, or {@code null} if the information + * is not available. Note that unlike with Java string API-s, this column number is inclusive. + * + * @since 2.3.21 + */ + public Integer getEndColumnNumber() { + synchronized (lock) { + if (!positionsCalculated) { + calculatePosition(); + } + return endColumnNumber; + } + } + + /** + * If there was a blamed expression attached to this exception, it returns its canonical form, otherwise it returns + * {@code null}. This expression should always be inside the failing FTL instruction. + * + * <p>The typical application of this is getting the undefined expression from {@link InvalidReferenceException}-s. + * + * @since 2.3.21 + */ + public String getBlamedExpressionString() { + synchronized (lock) { + if (!blamedExpressionStringCalculated) { + if (blamedExpression != null) { + blamedExpressionString = blamedExpression.getCanonicalForm(); + } + blamedExpressionStringCalculated = true; + } + return blamedExpressionString; + } + } + + ASTExpression getBlamedExpression() { + return blamedExpression; + } + + private void writeObject(ObjectOutputStream out) throws IOException, ClassNotFoundException { + // These are calculated from transient fields, so this is the last chance to calculate them: + getFTLInstructionStack(); + getFTLInstructionStackTopFew(); + getDescription(); + calculatePosition(); + getBlamedExpressionString(); + + out.defaultWriteObject(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + lock = new Object(); + in.defaultReadObject(); + } + + /** Delegate to a {@link PrintWriter} or to a {@link PrintStream}. */ + private interface StackTraceWriter { + void print(Object obj); + void println(Object obj); + void println(); + void printStandardStackTrace(Throwable exception); + } + + private static class PrintStreamStackTraceWriter implements StackTraceWriter { + + private final PrintStream out; + + PrintStreamStackTraceWriter(PrintStream out) { + this.out = out; + } + + @Override + public void print(Object obj) { + out.print(obj); + } + + @Override + public void println(Object obj) { + out.println(obj); + } + + @Override + public void println() { + out.println(); + } + + @Override + public void printStandardStackTrace(Throwable exception) { + if (exception instanceof TemplateException) { + ((TemplateException) exception).printStandardStackTrace(out); + } else { + exception.printStackTrace(out); + } + } + + } + + private static class PrintWriterStackTraceWriter implements StackTraceWriter { + + private final PrintWriter out; + + PrintWriterStackTraceWriter(PrintWriter out) { + this.out = out; + } + + @Override + public void print(Object obj) { + out.print(obj); + } + + @Override + public void println(Object obj) { + out.println(obj); + } + + @Override + public void println() { + out.println(); + } + + @Override + public void printStandardStackTrace(Throwable exception) { + if (exception instanceof TemplateException) { + ((TemplateException) exception).printStandardStackTrace(out); + } else { + exception.printStackTrace(out); + } + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateExceptionHandler.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateExceptionHandler.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateExceptionHandler.java new file mode 100644 index 0000000..8270740 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateExceptionHandler.java @@ -0,0 +1,156 @@ +/* + * 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.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +import org.apache.freemarker.core.util._StringUtil; + +/** + * Used for the {@code template_exception_handler} configuration setting; + * see {@link MutableProcessingConfiguration#setTemplateExceptionHandler(TemplateExceptionHandler)} for more. + */ +public interface TemplateExceptionHandler { + + /** + * Method called after a {@link TemplateException} was raised inside a template. The exception should be re-thrown + * unless you want to suppress the exception. + * + * <p>Note that you can check with {@link Environment#isInAttemptBlock()} if you are inside a {@code #attempt} + * block, which then will handle handle this exception and roll back the output generated inside it. + * + * <p>Note that {@link StopException}-s (raised by {@code #stop}) won't be captured. + * + * <p>Note that you shouldn't log the exception in this method unless you suppress it. If there's a concern that the + * exception might won't be logged after it bubbles up from {@link Template#process(Object, Writer)}, simply + * ensure that {@link Configuration#getLogTemplateExceptions()} is {@code true}. + * + * @param te The exception that occurred; don't forget to re-throw it unless you want to suppress it + * @param env The runtime environment of the template + * @param out This is where the output of the template is written + */ + void handleTemplateException(TemplateException te, Environment env, Writer out) throws TemplateException; + + /** + * {@link TemplateExceptionHandler} that simply skips the failing instructions, letting the template continue + * executing. It does nothing to handle the event. Note that the exception is still logged, as with all + * other {@link TemplateExceptionHandler}-s. + */ + TemplateExceptionHandler IGNORE_HANDLER = new TemplateExceptionHandler() { + @Override + public void handleTemplateException(TemplateException te, Environment env, Writer out) { + // Do nothing + } + }; + + /** + * {@link TemplateExceptionHandler} that simply re-throws the exception; this should be used in most production + * systems. + */ + TemplateExceptionHandler RETHROW_HANDLER = new TemplateExceptionHandler() { + @Override + public void handleTemplateException(TemplateException te, Environment env, Writer out) + throws TemplateException { + throw te; + } + }; + + /** + * {@link TemplateExceptionHandler} useful when you developing non-HTML templates. This handler + * outputs the stack trace information to the client and then re-throws the exception. + */ + TemplateExceptionHandler DEBUG_HANDLER = new TemplateExceptionHandler() { + @Override + public void handleTemplateException(TemplateException te, Environment env, Writer out) + throws TemplateException { + if (!env.isInAttemptBlock()) { + PrintWriter pw = (out instanceof PrintWriter) ? (PrintWriter) out : new PrintWriter(out); + pw.print("FreeMarker template error (DEBUG mode; use RETHROW in production!):\n"); + te.printStackTrace(pw, false, true, true); + + pw.flush(); // To commit the HTTP response + } + throw te; + } + }; + + /** + * {@link TemplateExceptionHandler} useful when you developing HTML templates. This handler + * outputs the stack trace information to the client, formatting it so that it will be usually well readable + * in the browser, and then re-throws the exception. + */ + TemplateExceptionHandler HTML_DEBUG_HANDLER = new TemplateExceptionHandler() { + @Override + public void handleTemplateException(TemplateException te, Environment env, Writer out) + throws TemplateException { + if (!env.isInAttemptBlock()) { + boolean externalPw = out instanceof PrintWriter; + PrintWriter pw = externalPw ? (PrintWriter) out : new PrintWriter(out); + try { + pw.print("<!-- FREEMARKER ERROR MESSAGE STARTS HERE -->" + + "<!-- ]]> -->" + + "<script language=javascript>//\"></script>" + + "<script language=javascript>//'></script>" + + "<script language=javascript>//\"></script>" + + "<script language=javascript>//'></script>" + + "</title></xmp></script></noscript></style></object>" + + "</head></pre></table>" + + "</form></table></table></table></a></u></i></b>" + + "<div align='left' " + + "style='background-color:#FFFF7C; " + + "display:block; border-top:double; padding:4px; margin:0; " + + "font-family:Arial,sans-serif; "); + pw.print(FONT_RESET_CSS); + pw.print("'>" + + "<b style='font-size:12px; font-style:normal; font-weight:bold; " + + "text-decoration:none; text-transform: none;'>FreeMarker template error " + + " (HTML_DEBUG mode; use RETHROW in production!)</b>" + + "<pre style='display:block; background: none; border: 0; margin:0; padding: 0;" + + "font-family:monospace; "); + pw.print(FONT_RESET_CSS); + pw.println("; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; " + + "white-space: -o-pre-wrap; word-wrap: break-word;'>"); + + StringWriter stackTraceSW = new StringWriter(); + PrintWriter stackPW = new PrintWriter(stackTraceSW); + te.printStackTrace(stackPW, false, true, true); + stackPW.close(); + pw.println(); + pw.println(_StringUtil.XMLEncNQG(stackTraceSW.toString())); + + pw.println("</pre></div></html>"); + pw.flush(); // To commit the HTTP response + } finally { + if (!externalPw) pw.close(); + } + } // if (!env.isInAttemptBlock()) + + throw te; + } + + private static final String FONT_RESET_CSS = + "color:#A80000; font-size:12px; font-style:normal; font-variant:normal; " + + "font-weight:normal; text-decoration:none; text-transform: none"; + + }; + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateLanguage.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateLanguage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateLanguage.java new file mode 100644 index 0000000..205fa8c --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateLanguage.java @@ -0,0 +1,111 @@ +/* + * 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.InputStream; +import java.io.Reader; +import java.nio.charset.Charset; + +import org.apache.freemarker.core.util._StringUtil; + +/** + * Represents a template language. Currently this class is not mature, so it can't be implemented outside FreeMarker, + * also its methods shouldn't be called from outside FreeMarker. + */ +// [FM3] Make this mature, or hide its somehow. Actually, parse can't be hidden because custom TemplateResolver-s need +// to call it. +public abstract class TemplateLanguage { + + // FIXME [FM3] If we leave this here, FTL will be a required dependency of core (which is not nice if + // template languages will be pluggable). + public static final TemplateLanguage FTL = new TemplateLanguage("FreeMarker Template Language") { + @Override + public boolean getCanSpecifyCharsetInContent() { + return true; + } + + @Override + public Template parse(String name, String sourceName, Reader reader, Configuration cfg, + TemplateConfiguration templateConfiguration, Charset encoding, + InputStream streamToUnmarkWhenEncEstabd) throws + IOException, ParseException { + return new Template(name, sourceName, reader, cfg, templateConfiguration, + encoding, streamToUnmarkWhenEncEstabd); + } + }; + + public static final TemplateLanguage STATIC_TEXT = new TemplateLanguage("Static text") { + @Override + public boolean getCanSpecifyCharsetInContent() { + return false; + } + + @Override + public Template parse(String name, String sourceName, Reader reader, Configuration cfg, + TemplateConfiguration templateConfiguration, Charset sourceEncoding, + InputStream streamToUnmarkWhenEncEstabd) + throws IOException, ParseException { + // Read the contents into a StringWriter, then construct a single-text-block template from it. + final StringBuilder sb = new StringBuilder(); + final char[] buf = new char[4096]; + int charsRead; + while ((charsRead = reader.read(buf)) > 0) { + sb.append(buf, 0, charsRead); + } + return Template.createPlainTextTemplate(name, sourceName, sb.toString(), cfg, + sourceEncoding); + } + }; + + private final String name; + + // Package visibility to prevent user implementations until this API is mature. + TemplateLanguage(String name) { + this.name = name; + } + + /** + * Returns if the template can specify its own charset inside the template. If so, {@link #parse(String, String, + * Reader, Configuration, TemplateConfiguration, Charset, InputStream)} can throw + * {@link WrongTemplateCharsetException}, and it might gets a non-{@code null} for the {@link InputStream} + * parameter. + */ + public abstract boolean getCanSpecifyCharsetInContent(); + + /** + * See {@link Template#Template(String, String, Reader, Configuration, TemplateConfiguration, Charset, + * InputStream)}. + */ + public abstract Template parse(String name, String sourceName, Reader reader, + Configuration cfg, TemplateConfiguration templateConfiguration, + Charset encoding, InputStream streamToUnmarkWhenEncEstabd) + throws IOException, ParseException; + + public String getName() { + return name; + } + + @Override + public final String toString() { + return "TemplateLanguage(" + _StringUtil.jQuote(name) + ")"; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateNotFoundException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateNotFoundException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateNotFoundException.java new file mode 100644 index 0000000..37ba911 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateNotFoundException.java @@ -0,0 +1,64 @@ +/* + * 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.FileNotFoundException; +import java.io.Serializable; + +import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; + +/** + * Thrown when {@link Configuration#getTemplate(String)} (or similar) doesn't find a template. + * This extends {@link FileNotFoundException} for backward compatibility, but in fact has nothing to do with files, as + * FreeMarker can load templates from many other sources. + * + * @since 2.3.22 + * + * @see MalformedTemplateNameException + * @see Configuration#getTemplate(String) + */ +public final class TemplateNotFoundException extends FileNotFoundException { + + private final String templateName; + private final Object customLookupCondition; + + public TemplateNotFoundException(String templateName, Object customLookupCondition, String message) { + super(message); + this.templateName = templateName; + this.customLookupCondition = customLookupCondition; + } + + /** + * The name (path) of the template that wasn't found. + */ + public String getTemplateName() { + return templateName; + } + + /** + * The custom lookup condition with which the template was requested, or {@code null} if there's no such condition. + * See the {@code customLookupCondition} parameter of + * {@link Configuration#getTemplate(String, java.util.Locale, Serializable, boolean)}. + */ + public Object getCustomLookupCondition() { + return customLookupCondition; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateParsingConfigurationWithFallback.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateParsingConfigurationWithFallback.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateParsingConfigurationWithFallback.java new file mode 100644 index 0000000..93a5840 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateParsingConfigurationWithFallback.java @@ -0,0 +1,146 @@ +/* + * 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.nio.charset.Charset; + +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.outputformat.OutputFormat; + +/** + * Adds {@link Configuration} fallback to the {@link ParsingConfiguration} part of a {@link TemplateConfiguration}. + */ +final class TemplateParsingConfigurationWithFallback implements ParsingConfiguration { + + private final Configuration cfg; + private final TemplateConfiguration tCfg; + + TemplateParsingConfigurationWithFallback(Configuration cfg, TemplateConfiguration tCfg) { + this.cfg = cfg; + this.tCfg = tCfg; + } + + @Override + public TemplateLanguage getTemplateLanguage() { + return tCfg.isTemplateLanguageSet() ? tCfg.getTemplateLanguage() : cfg.getTemplateLanguage(); + } + + @Override + public boolean isTemplateLanguageSet() { + return true; + } + + @Override + public int getTagSyntax() { + return tCfg.isTagSyntaxSet() ? tCfg.getTagSyntax() : cfg.getTagSyntax(); + } + + @Override + public boolean isTagSyntaxSet() { + return true; + } + + @Override + public int getNamingConvention() { + return tCfg.isNamingConventionSet() ? tCfg.getNamingConvention() : cfg.getNamingConvention(); + } + + @Override + public boolean isNamingConventionSet() { + return true; + } + + @Override + public boolean getWhitespaceStripping() { + return tCfg.isWhitespaceStrippingSet() ? tCfg.getWhitespaceStripping() : cfg.getWhitespaceStripping(); + } + + @Override + public boolean isWhitespaceStrippingSet() { + return true; + } + + @Override + public ArithmeticEngine getArithmeticEngine() { + return tCfg.isArithmeticEngineSet() ? tCfg.getArithmeticEngine() : cfg.getArithmeticEngine(); + } + + @Override + public boolean isArithmeticEngineSet() { + return true; + } + + @Override + public int getAutoEscapingPolicy() { + return tCfg.isAutoEscapingPolicySet() ? tCfg.getAutoEscapingPolicy() : cfg.getAutoEscapingPolicy(); + } + + @Override + public boolean isAutoEscapingPolicySet() { + return true; + } + + @Override + public OutputFormat getOutputFormat() { + return tCfg.isOutputFormatSet() ? tCfg.getOutputFormat() : cfg.getOutputFormat(); + } + + @Override + public boolean isOutputFormatSet() { + return true; + } + + @Override + public boolean getRecognizeStandardFileExtensions() { + return tCfg.isRecognizeStandardFileExtensionsSet() ? tCfg.getRecognizeStandardFileExtensions() + : cfg.getRecognizeStandardFileExtensions(); + } + + @Override + public boolean isRecognizeStandardFileExtensionsSet() { + return true; + } + + @Override + public Version getIncompatibleImprovements() { + // This can be only set on the Configuration-level + return cfg.getIncompatibleImprovements(); + } + + @Override + public int getTabSize() { + return tCfg.isTabSizeSet() ? tCfg.getTabSize() : cfg.getTabSize(); + } + + @Override + public boolean isTabSizeSet() { + return true; + } + + @Override + public Charset getSourceEncoding() { + return tCfg.isSourceEncodingSet() ? tCfg.getSourceEncoding() : cfg.getSourceEncoding(); + } + + @Override + public boolean isSourceEncodingSet() { + return true; + } +}
