You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/14 10:53:14 UTC

[31/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th

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;
+    }
+}