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/03/27 11:48:53 UTC

[4/6] incubator-freemarker git commit: Various refactorings of Configurable and its subclasses. This is part of the preparation for making such classes immutable, and offer builders to create them.

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/053afbf5/src/main/java/org/apache/freemarker/core/Configuration.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/Configuration.java b/src/main/java/org/apache/freemarker/core/Configuration.java
index 256b4c1..0e76d09 100644
--- a/src/main/java/org/apache/freemarker/core/Configuration.java
+++ b/src/main/java/org/apache/freemarker/core/Configuration.java
@@ -39,6 +39,7 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.freemarker.core.model.ObjectWrapper;
 import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
@@ -125,7 +126,7 @@ import org.apache.freemarker.core.util._UnmodifiableCompositeSet;
  *       useless. (For the most common cases you can use the convenience methods,
  *       {@link #setDirectoryForTemplateLoading(File)} and {@link #setClassForTemplateLoading(Class, String)} and
  *       {@link #setClassLoaderForTemplateLoading(ClassLoader, String)} too.)
- *   <li>{@link #setDefaultEncoding(String) default_encoding}: The default value is system dependent, which makes it
+ *   <li>{@link #setEncoding(String) encoding}: The default value is system dependent, which makes it
  *       fragile on servers, so it should be set explicitly, like to "UTF-8" nowadays. 
  *   <li>{@link #setTemplateExceptionHandler(TemplateExceptionHandler) template_exception_handler}: For developing
  *       HTML pages, the most convenient value is {@link TemplateExceptionHandler#HTML_DEBUG_HANDLER}. For production,
@@ -139,16 +140,17 @@ import org.apache.freemarker.core.util._UnmodifiableCompositeSet;
  * anymore, so then it's safe to make it accessible (again, via a "safe publication" technique) from multiple threads.
  * The methods that aren't for modifying settings, like {@link #getTemplate(String)}, are thread-safe.
  */
-public class Configuration extends Configurable implements Cloneable, ParserConfiguration {
+public final class Configuration extends MutableProcessingConfiguration<Configuration>
+        implements Cloneable, ParserConfiguration, ProcessingConfiguration, CustomStateScope {
     
     private static final String VERSION_PROPERTIES_PATH = "org/apache/freemarker/core/version.properties";
     
     /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
-    public static final String DEFAULT_ENCODING_KEY_SNAKE_CASE = "default_encoding"; 
+    public static final String ENCODING_KEY_SNAKE_CASE = "encoding";
     /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
-    public static final String DEFAULT_ENCODING_KEY_CAMEL_CASE = "defaultEncoding"; 
+    public static final String ENCODING_KEY_CAMEL_CASE = "encoding";
     /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
-    public static final String DEFAULT_ENCODING_KEY = DEFAULT_ENCODING_KEY_SNAKE_CASE;
+    public static final String ENCODING_KEY = ENCODING_KEY_SNAKE_CASE;
     
     /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
     public static final String LOCALIZED_LOOKUP_KEY_SNAKE_CASE = "localized_lookup";
@@ -281,7 +283,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         // Must be sorted alphabetically!
         AUTO_ESCAPING_POLICY_KEY_SNAKE_CASE,
         CACHE_STORAGE_KEY_SNAKE_CASE,
-        DEFAULT_ENCODING_KEY_SNAKE_CASE,
+            ENCODING_KEY_SNAKE_CASE,
         INCOMPATIBLE_IMPROVEMENTS_KEY_SNAKE_CASE,
         LOCALIZED_LOOKUP_KEY_SNAKE_CASE,
         NAMING_CONVENTION_KEY_SNAKE_CASE,
@@ -303,7 +305,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         // Must be sorted alphabetically!
         AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE,
         CACHE_STORAGE_KEY_CAMEL_CASE,
-        DEFAULT_ENCODING_KEY_CAMEL_CASE,
+            ENCODING_KEY_CAMEL_CASE,
         INCOMPATIBLE_IMPROVEMENTS_KEY_CAMEL_CASE,
         LOCALIZED_LOOKUP_KEY_CAMEL_CASE,
         NAMING_CONVENTION_KEY_CAMEL_CASE,
@@ -430,6 +432,9 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
 
     private HashMap/*<String, TemplateModel>*/ sharedVariables = new HashMap();
 
+    private final ConcurrentHashMap<CustomStateKey, Object> customStateMap = new ConcurrentHashMap<>(0);
+    private final Object customStateMapLock = new Object();
+
     /**
      * Needed so that it doesn't mater in what order do you call {@link #setSharedVaribles(Map)}
      * and {@link #setObjectWrapper(ObjectWrapper)}. When the user configures FreeMarker from Spring XML, he has no
@@ -437,7 +442,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      */
     private HashMap<String, Object> rewrappableSharedVariables = null;
     
-    private String defaultEncoding = getDefaultDefaultEncoding();
+    private String encoding = getDefaultEncoding();
 
     /**
      * @deprecated Use {@link #Configuration(Version)} instead. Note that the version can be still modified later with
@@ -560,7 +565,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         }
         return new DefaultSoftCacheStorage(); 
     }
-    
+
     private static class DefaultSoftCacheStorage extends SoftCacheStorage {
         // Nothing to override
     }
@@ -1236,6 +1241,11 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         return whitespaceStripping;
     }
 
+    @Override
+    public boolean isWhitespaceStrippingSet() {
+        return true;
+    }
+
     /**
      * Sets when auto-escaping should be enabled depending on the current {@linkplain OutputFormat output format};
      * default is {@link #ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY}. Note that the default output format,
@@ -1312,7 +1322,12 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     public int getAutoEscapingPolicy() {
         return autoEscapingPolicy;
     }
-    
+
+    @Override
+    public boolean isAutoEscapingPolicySet() {
+        return true;
+    }
+
     /**
      * Sets the default output format. Usually, you should leave this on its default, which is
      * {@link UndefinedOutputFormat#INSTANCE}, and then use standard file extensions like "ftlh" (for HTML) or "ftlx"
@@ -1358,7 +1373,12 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     public OutputFormat getOutputFormat() {
         return outputFormat;
     }
-    
+
+    @Override
+    public boolean isOutputFormatSet() {
+        return true;
+    }
+
     /**
      * Tells if {@link #setOutputFormat(OutputFormat)} (or equivalent) was already called on this instance.
      * 
@@ -1613,6 +1633,11 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 : recognizeStandardFileExtensions.booleanValue();
     }
 
+    @Override
+    public boolean isRecognizeStandardFileExtensionsSet() {
+        return true;
+    }
+
     /**
      * Getter pair of {@link #setTemplateLanguage(TemplateLanguage)}.
      */
@@ -1677,6 +1702,11 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         return tagSyntax;
     }
 
+    @Override
+    public boolean isTagSyntaxSet() {
+        return true;
+    }
+
     /**
      * Sets the naming convention used for the identifiers that are part of the template language. The available naming
      * conventions are legacy (directive (tag) names are all-lower-case {@code likethis}, others are snake case
@@ -1747,7 +1777,12 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     public int getNamingConvention() {
         return namingConvention;
     }
-    
+
+    @Override
+    public boolean isNamingConventionSet() {
+        return true;
+    }
+
     /**
      * Sets the assumed display width of the tab character (ASCII 9), which influences the column number shown in error
      * messages (or the column number you get through other API-s). So for example if the users edit templates in an
@@ -1780,6 +1815,31 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     public int getTabSize() {
         return tabSize;
     }
+
+    @Override
+    public boolean isTabSizeSet() {
+        return true;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T getCustomState(CustomStateKey<T> customStateKey) {
+        T customState = (T) customStateMap.get(customStateKey);
+        if (customState == null) {
+            synchronized (customStateMapLock) {
+                customState = (T) customStateMap.get(customStateKey);
+                if (customState == null) {
+                    customState = customStateKey.create();
+                    if (customState == null) {
+                        throw new IllegalStateException("CustomStateKey.create() must not return null (for key: "
+                                + customStateKey + ")");
+                    }
+                    customStateMap.put(customStateKey, customState);
+                }
+            }
+        }
+        return customState;
+    }
     
     /**
      * Retrieves the template with the given name from the template templateResolver, loading it into the templateResolver first if it's
@@ -1982,18 +2042,14 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      *
      * @param encoding The name of the charset, such as {@code "UTF-8"} or {@code "ISO-8859-1"}
      */
-    public void setDefaultEncoding(String encoding) {
-        defaultEncoding = encoding;
+    public void setEncoding(String encoding) {
+        this.encoding = encoding;
         defaultEncodingExplicitlySet = true;
     }
 
-    /**
-     * Gets the default encoding for converting bytes to characters when
-     * reading template files in a locale for which no explicit encoding
-     * was specified. Defaults to the default system encoding.
-     */
-    public String getDefaultEncoding() {
-        return defaultEncoding;
+    @Override
+    public String getEncoding() {
+        return encoding;
     }
 
     /**
@@ -2003,13 +2059,13 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      */
     public void unsetDefaultEncoding() {
         if (defaultEncodingExplicitlySet) {
-            setDefaultEncoding(getDefaultDefaultEncoding());
+            setEncoding(getDefaultEncoding());
             defaultEncodingExplicitlySet = false;
         }
     }
 
     /**
-     * Tells if {@link #setDefaultEncoding(String)} (or equivalent) was already called on this instance, or it just holds the
+     * Tells if {@link #setEncoding(String)} (or equivalent) was already called on this instance, or it just holds the
      * default value.
      *
      * @since 2.3.26
@@ -2018,7 +2074,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         return defaultEncodingExplicitlySet;
     }
 
-    static private String getDefaultDefaultEncoding() {
+    static private String getDefaultEncoding() {
         return getJVMDefaultEncoding();
     }
 
@@ -2063,7 +2119,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     }
     
     /**
-     * Adds shared variable to the configuration; It uses {@link Configurable#getObjectWrapper()} to wrap the 
+     * Adds shared variable to the configuration; It uses {@link MutableProcessingConfiguration#getObjectWrapper()} to wrap the
      * {@code value}, so it's important that the object wrapper is set before this.
      * 
      * <p>This method is <b>not</b> thread safe; use it with the same restrictions as those that modify setting values.
@@ -2245,14 +2301,14 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
             if ("TemplateUpdateInterval".equalsIgnoreCase(name)) {
                 name = TEMPLATE_UPDATE_DELAY_KEY;
             } else if ("DefaultEncoding".equalsIgnoreCase(name)) {
-                name = DEFAULT_ENCODING_KEY;
+                name = ENCODING_KEY;
             }
             
-            if (DEFAULT_ENCODING_KEY_SNAKE_CASE.equals(name) || DEFAULT_ENCODING_KEY_CAMEL_CASE.equals(name)) {
-                if (JVM_DEFAULT.equalsIgnoreCase(value)) {
-                    setDefaultEncoding(getJVMDefaultEncoding());
+            if (ENCODING_KEY_SNAKE_CASE.equals(name) || ENCODING_KEY_CAMEL_CASE.equals(name)) {
+                if (JVM_DEFAULT_VALUE.equalsIgnoreCase(value)) {
+                    setEncoding(getJVMDefaultEncoding());
                 } else {
-                    setDefaultEncoding(value);
+                    setEncoding(value);
                 }
             } else if (LOCALIZED_LOOKUP_KEY_SNAKE_CASE.equals(name) || LOCALIZED_LOOKUP_KEY_CAMEL_CASE.equals(name)) {
                 setLocalizedLookup(_StringUtil.getYesNo(value));
@@ -2270,7 +2326,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                     throw invalidSettingValueException(name, value);
                 }
             } else if (OUTPUT_FORMAT_KEY_SNAKE_CASE.equals(name) || OUTPUT_FORMAT_KEY_CAMEL_CASE.equals(name)) {
-                if (value.equalsIgnoreCase(DEFAULT)) {
+                if (value.equalsIgnoreCase(DEFAULT_VALUE)) {
                     unsetOutputFormat();
                 } else {
                     setOutputFormat((OutputFormat) _ObjectBuilderSettingEvaluator.eval(
@@ -2290,13 +2346,13 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 setRegisteredCustomOutputFormats(list);
             } else if (RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_SNAKE_CASE.equals(name)
                     || RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_CAMEL_CASE.equals(name)) {
-                if (value.equalsIgnoreCase(DEFAULT)) {
+                if (value.equalsIgnoreCase(DEFAULT_VALUE)) {
                     unsetRecognizeStandardFileExtensions();
                 } else {
                     setRecognizeStandardFileExtensions(_StringUtil.getYesNo(value));
                 }
             } else if (CACHE_STORAGE_KEY_SNAKE_CASE.equals(name) || CACHE_STORAGE_KEY_CAMEL_CASE.equals(name)) {
-                if (value.equalsIgnoreCase(DEFAULT)) {
+                if (value.equalsIgnoreCase(DEFAULT_VALUE)) {
                     unsetCacheStorage();
                 } if (value.indexOf('.') == -1) {
                     int strongSize = 0;
@@ -2397,7 +2453,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                     || INCOMPATIBLE_IMPROVEMENTS_KEY_CAMEL_CASE.equals(name)) {
                 setIncompatibleImprovements(new Version(value));
             } else if (TEMPLATE_LOADER_KEY_SNAKE_CASE.equals(name) || TEMPLATE_LOADER_KEY_CAMEL_CASE.equals(name)) {
-                if (value.equalsIgnoreCase(DEFAULT)) {
+                if (value.equalsIgnoreCase(DEFAULT_VALUE)) {
                     unsetTemplateLoader();
                 } else {
                     setTemplateLoader((TemplateLoader) _ObjectBuilderSettingEvaluator.eval(
@@ -2405,7 +2461,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 }
             } else if (TEMPLATE_LOOKUP_STRATEGY_KEY_SNAKE_CASE.equals(name)
                     || TEMPLATE_LOOKUP_STRATEGY_KEY_CAMEL_CASE.equals(name)) {
-                if (value.equalsIgnoreCase(DEFAULT)) {
+                if (value.equalsIgnoreCase(DEFAULT_VALUE)) {
                     unsetTemplateLookupStrategy();
                 } else {
                     setTemplateLookupStrategy((TemplateLookupStrategy) _ObjectBuilderSettingEvaluator.eval(
@@ -2413,7 +2469,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 }
             } else if (TEMPLATE_NAME_FORMAT_KEY_SNAKE_CASE.equals(name)
                     || TEMPLATE_NAME_FORMAT_KEY_CAMEL_CASE.equals(name)) {
-                if (value.equalsIgnoreCase(DEFAULT)) {
+                if (value.equalsIgnoreCase(DEFAULT_VALUE)) {
                     unsetTemplateNameFormat();
                 } else if (value.equalsIgnoreCase("default_2_3_0")) {
                     setTemplateNameFormat(DefaultTemplateNameFormatFM2.INSTANCE);
@@ -2424,7 +2480,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 }
             } else if (TEMPLATE_CONFIGURATIONS_KEY_SNAKE_CASE.equals(name)
                     || TEMPLATE_CONFIGURATIONS_KEY_CAMEL_CASE.equals(name)) {
-                if (value.equals(NULL)) {
+                if (value.equals(NULL_VALUE)) {
                     setTemplateConfigurations(null);
                 } else {
                     setTemplateConfigurations((TemplateConfigurationFactory) _ObjectBuilderSettingEvaluator.eval(
@@ -2450,14 +2506,14 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     }
 
     /**
-     * Returns the valid {@link Configuration} setting names. Naturally, this includes the {@link Configurable} setting
+     * Returns the valid {@link Configuration} setting names. Naturally, this includes the {@link MutableProcessingConfiguration} setting
      * names too.
      * 
      * @param camelCase
      *            If we want the setting names with camel case naming convention, or with snake case (legacy) naming
      *            convention.
      * 
-     * @see Configurable#getSettingNames(boolean)
+     * @see MutableProcessingConfiguration#getSettingNames(boolean)
      * 
      * @since 2.3.24
      */
@@ -2480,10 +2536,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     protected String getCorrectedNameForUnknownSetting(String name) {
         if ("encoding".equals(name) || "charset".equals(name) || "default_charset".equals(name)) {
             // [2.4] Default might changes to camel-case
-            return DEFAULT_ENCODING_KEY;
+            return ENCODING_KEY;
         }
         if ("defaultCharset".equals(name)) {
-            return DEFAULT_ENCODING_KEY_CAMEL_CASE;
+            return ENCODING_KEY_CAMEL_CASE;
         }
         if (name.equals("incompatible_enhancements")) {
             return INCOMPATIBLE_IMPROVEMENTS_KEY_SNAKE_CASE;
@@ -2503,13 +2559,12 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     }
 
     private void doAutoImports(Environment env, Template t) throws IOException, TemplateException {
-        Map<String, String> envAutoImports = env.getAutoImportsWithoutFallback();
-        Map<String, String> tAutoImports = t.getAutoImportsWithoutFallback();
+        Map<String, String> envAutoImports = env.isAutoImportsSet() ? env.getAutoImports() : null;
+        Map<String, String> tAutoImports = t.isAutoImportsSet() ? t.getAutoImports() : null;
         
-        boolean lazyAutoImports = env.getLazyAutoImports() != null ? env.getLazyAutoImports().booleanValue()
-                : env.getLazyImports();
+        boolean lazyAutoImports = env.getLazyAutoImports() != null ? env.getLazyAutoImports() : env.getLazyImports();
         
-        for (Map.Entry<String, String> autoImport : getAutoImportsWithoutFallback().entrySet()) {
+        for (Map.Entry<String, String> autoImport : getAutoImports().entrySet()) {
             String nsVarName = autoImport.getKey();
             if ((tAutoImports == null || !tAutoImports.containsKey(nsVarName))
                     && (envAutoImports == null || !envAutoImports.containsKey(nsVarName))) {
@@ -2536,11 +2591,11 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         // We can't store autoIncludes in LinkedHashSet-s because setAutoIncludes(List) allows duplicates,
         // unfortunately. Yet we have to prevent duplicates among Configuration levels, with the lowest levels having
         // priority. So we build some Set-s to do that, but we avoid the most common cases where they aren't needed.
+
+        List<String> tAutoIncludes = t.isAutoIncludesSet() ? t.getAutoIncludes() : null;
+        List<String> envAutoIncludes = env.isAutoIncludesSet() ? env.getAutoIncludes() : null;
         
-        List<String> tAutoIncludes = t.getAutoIncludesWithoutFallback();
-        List<String> envAutoIncludes = env.getAutoIncludesWithoutFallback();
-        
-        for (String templateName : getAutoIncludesWithoutFallback()) {
+        for (String templateName : getAutoIncludes()) {
             if ((tAutoIncludes == null || !tAutoIncludes.contains(templateName))
                     && (envAutoIncludes == null || !envAutoIncludes.contains(templateName))) {
                 env.include(getTemplate(templateName, env.getLocale()));

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/053afbf5/src/main/java/org/apache/freemarker/core/CustomAttribute.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/CustomAttribute.java b/src/main/java/org/apache/freemarker/core/CustomAttribute.java
deleted file mode 100644
index 37d7db9..0000000
--- a/src/main/java/org/apache/freemarker/core/CustomAttribute.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * 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.BugException;
-
-/**
- * A class that allows one to associate custom data with a {@link Configuration}, a {@link Template}, or
- * {@link Environment}.
- * 
- * <p>This API has similar approach to that of {@link ThreadLocal} (which allows one to associate
- * custom data with a thread). With an example:</p>
- * 
- * <pre>
- * // The object identity itself will serve as the attribute identifier; there's no attribute name String:
- * public static final CustomAttribute MY_ATTR = new CustomAttribute(CustomAttribute.SCOPE_CONFIGURATION);
- * ...
- *     // Set the attribute in this particular Configuration object:
- *     MY_ATTR.set(myAttrValue, cfg);
- *     ...
- *     // Read the attribute from this particular Configuration object:
- *     myAttrValue = MY_ATTR.get(cfg);
- * </pre>
- */
-// [2.4] Use generics; type parameter used for the type of the stored value 
-public class CustomAttribute {
-    
-    /**
-     * Constant used in the constructor specifying that this attribute is {@link Environment}-scoped.
-     */
-    public static final int SCOPE_ENVIRONMENT = 0;
-        
-    /**
-     * Constant used in the constructor specifying that this attribute is {@link Template}-scoped.
-     */
-    public static final int SCOPE_TEMPLATE = 1;
-        
-    /**
-     * Constant used in the constructor specifying that this attribute is {@link Configuration}-scoped.
-     */
-    public static final int SCOPE_CONFIGURATION = 2;
-
-    // We use an internal key instead of 'this' so that malicious subclasses 
-    // overriding equals() and hashCode() can't gain access to other attribute
-    // values. That's also the reason why get() and set() are marked final.
-    private final Object key = new Object();
-    private final int scope;
-    
-    /**
-     * Creates a new custom attribute with the specified scope
-     * @param scope one of <tt>SCOPE_</tt> constants. 
-     */
-    public CustomAttribute(int scope) {
-        if (scope != SCOPE_ENVIRONMENT && 
-           scope != SCOPE_TEMPLATE && 
-           scope != SCOPE_CONFIGURATION) {
-                throw new IllegalArgumentException();
-            }
-        this.scope = scope;
-    }
-    
-    /**
-     * This method is invoked when {@link #get()} is invoked without 
-     * {@link #set(Object)} being invoked before it to define the value in the 
-     * current scope. Override it to invoke the attribute value on-demand.
-     * @return the initial value for the custom attribute. By default returns null.
-     */
-    protected Object create() {
-        return null;
-    }
-    
-    /**
-     * Gets the attribute from the appropriate scope that's accessible through the specified {@link Environment}. If
-     * the attribute has {@link #SCOPE_ENVIRONMENT} scope, it will be get from the given {@link Environment} directly.
-     * If the attribute has {@link #SCOPE_TEMPLATE} scope, it will be get from the parent of the given
-     * {@link Environment} (that is, in {@link Environment#getParent()}) directly). If the attribute has
-     * {@link #SCOPE_CONFIGURATION} scope, it will be get from {@link Environment#getConfiguration()}.
-     * 
-     * @throws NullPointerException
-     *             If {@code env} is null
-     * 
-     * @return The new value of the attribute (possibly {@code null}), or {@code null} if the attribute doesn't exist.
-     * 
-     * @since 2.3.22
-     */
-    public final Object get(Environment env) {
-        return getScopeConfigurable(env).getCustomAttribute(key, this);
-    }
-
-    /**
-     * Same as {@link #get(Environment)}, but uses {@link Environment#getCurrentEnvironment()} to fill the 2nd argument.
-     * 
-     * @throws IllegalStateException
-     *             If there is no current {@link Environment}, which is usually the case when the current thread isn't
-     *             processing a template.
-     */
-    public final Object get() {
-        return getScopeConfigurable(getRequiredCurrentEnvironment()).getCustomAttribute(key, this);
-    }
-    
-    /**
-     * Gets the value of a {@link Template}-scope attribute from the given {@link Template}.
-     * 
-     * @throws UnsupportedOperationException
-     *             If this custom attribute has different scope than {@link #SCOPE_TEMPLATE}.
-     * @throws NullPointerException
-     *             If {@code template} is null
-     */
-    public final Object get(Template template) {
-        if (scope != SCOPE_TEMPLATE) {
-            throw new UnsupportedOperationException("This is not a template-scope attribute");
-        }
-        return template.getCustomAttribute(key, this);
-    }
-    
-    /**
-     * Same as {@link #get(Template)}, but applies to a {@link TemplateConfiguration}.  
-     * 
-     * @since 2.3.24
-     */
-    public Object get(TemplateConfiguration templateConfiguration) {
-        if (scope != SCOPE_TEMPLATE) {
-            throw new UnsupportedOperationException("This is not a template-scope attribute");
-        }
-        return templateConfiguration.getCustomAttribute(key, this);
-    }
-    
-    /**
-     * Gets the value of a {@link Configuration}-scope attribute from the given {@link Configuration}.
-     * 
-     * @throws UnsupportedOperationException
-     *             If this custom attribute has different scope than {@link #SCOPE_CONFIGURATION}.
-     * @throws NullPointerException
-     *             If {@code cfg} is null
-     * 
-     * @since 2.3.22
-     */
-    public final Object get(Configuration cfg) {
-        if (scope != SCOPE_CONFIGURATION) {
-            throw new UnsupportedOperationException("This is not a template-scope attribute");
-        }
-        return cfg.getCustomAttribute(key, this);
-    }
-    
-    /**
-     * Sets the attribute inside the appropriate scope that's accessible through the specified {@link Environment}. If
-     * the attribute has {@link #SCOPE_ENVIRONMENT} scope, it will be set in the given {@link Environment} directly. If
-     * the attribute has {@link #SCOPE_TEMPLATE} scope, it will be set in the parent of the given {@link Environment}
-     * (that is, in {@link Environment#getParent()}) directly). If the attribute has {@link #SCOPE_CONFIGURATION} scope,
-     * it will be set in {@link Environment#getConfiguration()}.
-     * 
-     * @param value
-     *            The new value of the attribute. Can be {@code null}.
-     * 
-     * @throws NullPointerException
-     *             If {@code env} is null
-     * 
-     * @since 2.3.22
-     */
-    public final void set(Object value, Environment env) {
-        getScopeConfigurable(env).setCustomAttribute(key, value);
-    }
-
-    /**
-     * Same as {@link #set(Object, Environment)}, but uses {@link Environment#getCurrentEnvironment()} to fill the 2nd
-     * argument.
-     * 
-     * @throws IllegalStateException
-     *             If there is no current {@link Environment}, which is usually the case when the current thread isn't
-     *             processing a template.
-     */
-    public final void set(Object value) {
-        getScopeConfigurable(getRequiredCurrentEnvironment()).setCustomAttribute(key, value);
-    }
-
-    /**
-     * Sets the value of a {@link Template}-scope attribute in the given {@link Template}.
-     * 
-     * @param value
-     *            The new value of the attribute. Can be {@code null}.
-     * 
-     * @throws UnsupportedOperationException
-     *             If this custom attribute has different scope than {@link #SCOPE_TEMPLATE}.
-     * @throws NullPointerException
-     *             If {@code template} is null
-     */
-    public final void set(Object value, Template template) {
-        if (scope != SCOPE_TEMPLATE) {
-            throw new UnsupportedOperationException("This is not a template-scope attribute");
-        }
-        template.setCustomAttribute(key, value);
-    }
-
-    /**
-     * Same as {@link #set(Object, Template)}, but applicable to a {@link TemplateConfiguration}. 
-     * 
-     * @since 2.3.24
-     */
-    public final void set(Object value, TemplateConfiguration templateConfiguration) {
-        if (scope != SCOPE_TEMPLATE) {
-            throw new UnsupportedOperationException("This is not a template-scope attribute");
-        }
-        templateConfiguration.setCustomAttribute(key, value);
-    }
-    
-    /**
-     * Sets the value of a {@link Configuration}-scope attribute in the given {@link Configuration}.
-     * 
-     * @param value
-     *            The new value of the attribute. Can be {@code null}.
-     * 
-     * @throws UnsupportedOperationException
-     *             If this custom attribute has different scope than {@link #SCOPE_CONFIGURATION}.
-     * @throws NullPointerException
-     *             If {@code cfg} is null
-     * 
-     * @since 2.3.22
-     */
-    public final void set(Object value, Configuration cfg) {
-        if (scope != SCOPE_CONFIGURATION) {
-            throw new UnsupportedOperationException("This is not a configuration-scope attribute");
-        }
-        cfg.setCustomAttribute(key, value);
-    }
-    
-    private Environment getRequiredCurrentEnvironment() {
-        Environment c = Environment.getCurrentEnvironment();
-        if (c == null) {
-            throw new IllegalStateException("No current environment");
-        }
-        return c;
-    }
-
-    private Configurable getScopeConfigurable(Environment env) throws Error {
-        switch (scope) {
-        case SCOPE_ENVIRONMENT:
-            return env;
-        case SCOPE_TEMPLATE:
-            return env.getParent();
-        case SCOPE_CONFIGURATION:
-            return env.getParent().getParent();
-        default:
-            throw new BugException();
-        }
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/053afbf5/src/main/java/org/apache/freemarker/core/CustomStateKey.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/CustomStateKey.java b/src/main/java/org/apache/freemarker/core/CustomStateKey.java
new file mode 100644
index 0000000..fd6f4d5
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/CustomStateKey.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+/**
+ * Used with {@link CustomStateScope}-s; each subclass must have exactly one instance, which should be stored in
+ * a static final field. So the usual usage is like this:
+ *
+ * <pre>
+ *     static final CustomStateKey MY_STATE = new CustomStateKey() {
+ *         @Override
+ *         protected Object create() {
+ *             return new ...;
+ *         }
+ *     };
+ * </pre>
+ */
+public abstract class CustomStateKey<T> {
+
+    /**
+     * This will be invoked when the state for this {@link CustomStateKey} is get via {@link
+     * CustomStateScope#getCustomState(CustomStateKey)}, but it doesn't yet exists in the given scope. Then the created
+     * object will be stored in the scope and then it's returned. Must not return {@code null}.
+     */
+    protected abstract T create();
+
+    /**
+     * Does identity comparison (like operator {@code ==}).
+     */
+    @Override
+    final public boolean equals(Object o) {
+        return o == this;
+    }
+
+    /**
+     * Returns {@link Object#hashCode()}.
+     */
+    @Override
+    final public int hashCode() {
+        return super.hashCode();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/053afbf5/src/main/java/org/apache/freemarker/core/CustomStateScope.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/CustomStateScope.java b/src/main/java/org/apache/freemarker/core/CustomStateScope.java
new file mode 100644
index 0000000..4067823
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/CustomStateScope.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * An object that's a scope that can store custom state objects.
+ */
+public interface CustomStateScope {
+
+    /**
+     * Gets the custom state belonging to the key, automatically creating it if it doesn't yet exists in the scope.
+     * If the scope is {@link Configuration} or {@link Template}, then this method is thread safe. If the scope is
+     * {@link Environment}, then this method is not thread safe ({@link Environment} is not thread safe either).
+     */
+    <T> T getCustomState(CustomStateKey<T> customStateKey);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/053afbf5/src/main/java/org/apache/freemarker/core/Environment.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/Environment.java b/src/main/java/org/apache/freemarker/core/Environment.java
index 7eac791..ce95b15 100644
--- a/src/main/java/org/apache/freemarker/core/Environment.java
+++ b/src/main/java/org/apache/freemarker/core/Environment.java
@@ -99,9 +99,9 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  * If you need to modify or read this object before or after the <tt>process</tt> call, use
  * {@link Template#createProcessingEnvironment(Object rootMap, Writer out, ObjectWrapper wrapper)}
  */
-public final class Environment extends Configurable {
+public final class Environment extends MutableProcessingConfiguration<Environment> implements CustomStateScope {
     
-    private static final ThreadLocal threadEnv = new ThreadLocal();
+    private static final ThreadLocal<Environment> TLS_ENVIRONMENT = new ThreadLocal();
 
     private static final Logger LOG = _CoreLogs.RUNTIME;
     private static final Logger LOG_ATTEMPT = _CoreLogs.ATTEMPT;
@@ -125,6 +125,7 @@ public final class Environment extends Configurable {
 
     private TemplateNumberFormat cachedTemplateNumberFormat;
     private Map<String, TemplateNumberFormat> cachedTemplateNumberFormats;
+    private Map<CustomStateKey, Object> customStateMap;
 
     /**
      * Stores the date/time/date-time formatters that are used when no format is explicitly given at the place of
@@ -196,11 +197,19 @@ public final class Environment extends Configurable {
      * current thread.
      */
     public static Environment getCurrentEnvironment() {
-        return (Environment) threadEnv.get();
+        return TLS_ENVIRONMENT.get();
+    }
+
+    public static Environment getCurrentEnvironmentNotNull() {
+        Environment currentEnvironment = getCurrentEnvironment();
+        if (currentEnvironment == null) {
+            throw new IllegalStateException("There's no FreeMarker Environemnt in this this thread.");
+        }
+        return currentEnvironment;
     }
 
     static void setCurrentEnvironment(Environment env) {
-        threadEnv.set(env);
+        TLS_ENVIRONMENT.set(env);
     }
 
     public Environment(Template template, final TemplateHashModel rootDataModel, Writer out) {
@@ -242,6 +251,14 @@ public final class Environment extends Configurable {
         return ln == 0 ? getMainTemplate() : instructionStack[ln - 1].getTemplate();
     }
 
+    public Template getCurrentTemplateNotNull() {
+        Template currentTemplate = getCurrentTemplate();
+        if (currentTemplate == null) {
+            throw new IllegalStateException("There's no current template at the moment.");
+        }
+        return currentTemplate;
+    }
+
     /**
      * Gets the currently executing <em>custom</em> directive's call place information, or {@code null} if there's no
      * executing custom directive. This currently only works for calls made from templates with the {@code <@...>}
@@ -281,8 +298,8 @@ public final class Environment extends Configurable {
      * Processes the template to which this environment belongs to.
      */
     public void process() throws TemplateException, IOException {
-        Object savedEnv = threadEnv.get();
-        threadEnv.set(this);
+        Environment savedEnv = TLS_ENVIRONMENT.get();
+        TLS_ENVIRONMENT.set(this);
         try {
             // Cached values from a previous execution are possibly outdated.
             clearCachedValues();
@@ -298,7 +315,7 @@ public final class Environment extends Configurable {
                 clearCachedValues();
             }
         } finally {
-            threadEnv.set(savedEnv);
+            TLS_ENVIRONMENT.set(savedEnv);
         }
     }
 
@@ -1534,15 +1551,15 @@ public final class Environment extends Configurable {
             String settingValue;
             switch (dateType) {
             case TemplateDateModel.TIME:
-                settingName = Configurable.TIME_FORMAT_KEY;
+                settingName = MutableProcessingConfiguration.TIME_FORMAT_KEY;
                 settingValue = getTimeFormat();
                 break;
             case TemplateDateModel.DATE:
-                settingName = Configurable.DATE_FORMAT_KEY;
+                settingName = MutableProcessingConfiguration.DATE_FORMAT_KEY;
                 settingValue = getDateFormat();
                 break;
             case TemplateDateModel.DATETIME:
-                settingName = Configurable.DATETIME_FORMAT_KEY;
+                settingName = MutableProcessingConfiguration.DATETIME_FORMAT_KEY;
                 settingValue = getDateTimeFormat();
                 break;
             default:
@@ -2647,44 +2664,22 @@ public final class Environment extends Configurable {
         return currentNamespace.getTemplate().getDefaultNS();
     }
 
-    private IdentityHashMap<Object, Object> customStateVariables;
-
-    /**
-     * Returns the value of a custom state variable, or {@code null} if it's missing; see
-     * {@link #setCustomState(Object, Object)} for more.
-     * 
-     * @since 2.3.24
-     */
-    public Object getCustomState(Object identityKey) {
-        if (customStateVariables == null) {
-            return null;
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T getCustomState(CustomStateKey<T> customStateKey) {
+        if (customStateMap == null) {
+            customStateMap = new IdentityHashMap<>();
         }
-        return customStateVariables.get(identityKey);
-    }
-
-    /**
-     * Sets the value of a custom state variable. Custom state variables meant to be used by
-     * {@link TemplateNumberFormatFactory}-es, {@link TemplateDateFormatFactory}-es, and similar user-implementable,
-     * pluggable objects, which want to maintain an {@link Environment}-scoped state (such as a cache).
-     * 
-     * @param identityKey
-     *            The key that identifies the variable, by its object identity (not by {@link Object#equals(Object)}).
-     *            This should be something like a {@code private static final Object CUSTOM_STATE_KEY = new Object();}
-     *            in the class that needs this state variable.
-     * @param value
-     *            The value of the variable. Can be anything, even {@code null}.
-     * 
-     * @return The previous value of the variable, or {@code null} if the variable didn't exist.
-     * 
-     * @since 2.3.24
-     */
-    public Object setCustomState(Object identityKey, Object value) {
-        IdentityHashMap<Object, Object> customStateVariables = this.customStateVariables;
-        if (customStateVariables == null) {
-            customStateVariables = new IdentityHashMap<>();
-            this.customStateVariables = customStateVariables;
+        T customState = (T) customStateMap.get(customStateKey);
+        if (customState == null) {
+            customState = customStateKey.create();
+            if (customState == null) {
+                throw new IllegalStateException("CustomStateKey.create() must not return null (for key: "
+                        + customStateKey + ")");
+            }
+            customStateMap.put(customStateKey, customState);
         }
-        return customStateVariables.put(identityKey, value);
+        return customState;
     }
 
     final class NestedElementTemplateDirectiveBody implements TemplateDirectiveBody {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/053afbf5/src/main/java/org/apache/freemarker/core/MutableProcessingAndParseConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/MutableProcessingAndParseConfiguration.java b/src/main/java/org/apache/freemarker/core/MutableProcessingAndParseConfiguration.java
new file mode 100644
index 0000000..2324dce
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/MutableProcessingAndParseConfiguration.java
@@ -0,0 +1,282 @@
+/*
+ * 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.InputStream;
+
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+// TODO This will be the superclass of TemplateConfiguration.Builder and Configuration.Builder
+public abstract class MutableProcessingAndParseConfiguration<
+        SelfT extends MutableProcessingAndParseConfiguration<SelfT>>
+        extends MutableProcessingConfiguration<SelfT>
+        implements ParserConfiguration {
+
+    private TemplateLanguage templateLanguage;
+    private Integer tagSyntax;
+    private Integer namingConvention;
+    private Boolean whitespaceStripping;
+    private Integer autoEscapingPolicy;
+    private Boolean recognizeStandardFileExtensions;
+    private OutputFormat outputFormat;
+    private String encoding;
+    private Integer tabSize;
+
+    protected MutableProcessingAndParseConfiguration(Version incompatibleImprovements) {
+        super(incompatibleImprovements);
+    }
+
+    protected MutableProcessingAndParseConfiguration(MutableProcessingConfiguration parent) {
+        super(parent);
+    }
+
+    /**
+     * See {@link Configuration#setTagSyntax(int)}.
+     */
+    public void setTagSyntax(int tagSyntax) {
+        Configuration.valideTagSyntaxValue(tagSyntax);
+        this.tagSyntax = tagSyntax;
+    }
+
+    /**
+     * The getter pair of {@link #setTagSyntax(int)}.
+     */
+    @Override
+    public int getTagSyntax() {
+        return tagSyntax != null ? tagSyntax : getDefaultTagSyntax();
+    }
+
+    protected abstract int getDefaultTagSyntax();
+
+    @Override
+    public boolean isTagSyntaxSet() {
+        return tagSyntax != null;
+    }
+
+    /**
+     * See {@link Configuration#getTemplateLanguage()}
+     */
+    @Override
+    public TemplateLanguage getTemplateLanguage() {
+        return templateLanguage != null ? templateLanguage : getDefaultTemplateLanguage();
+    }
+
+    protected abstract TemplateLanguage getDefaultTemplateLanguage();
+
+    /**
+     * See {@link Configuration#setTemplateLanguage(TemplateLanguage)}
+     */
+    public void setTemplateLanguage(TemplateLanguage templateLanguage) {
+        _NullArgumentException.check("templateLanguage", templateLanguage);
+        this.templateLanguage = templateLanguage;
+    }
+
+    public boolean isTemplateLanguageSet() {
+        return templateLanguage != null;
+    }
+
+    /**
+     * See {@link Configuration#setNamingConvention(int)}.
+     */
+    public void setNamingConvention(int namingConvention) {
+        Configuration.validateNamingConventionValue(namingConvention);
+        this.namingConvention = namingConvention;
+    }
+
+    /**
+     * The getter pair of {@link #setNamingConvention(int)}.
+     */
+    @Override
+    public int getNamingConvention() {
+        return namingConvention != null ? namingConvention
+                : getDefaultNamingConvention();
+    }
+
+    protected abstract int getDefaultNamingConvention();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     */
+    @Override
+    public boolean isNamingConventionSet() {
+        return namingConvention != null;
+    }
+
+    /**
+     * See {@link Configuration#setWhitespaceStripping(boolean)}.
+     */
+    public void setWhitespaceStripping(boolean whitespaceStripping) {
+        this.whitespaceStripping = Boolean.valueOf(whitespaceStripping);
+    }
+
+    /**
+     * The getter pair of {@link #getWhitespaceStripping()}.
+     */
+    @Override
+    public boolean getWhitespaceStripping() {
+        return whitespaceStripping != null ? whitespaceStripping.booleanValue()
+                : getDefaultWhitespaceStripping();
+    }
+
+    protected abstract boolean getDefaultWhitespaceStripping();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     */
+    @Override
+    public boolean isWhitespaceStrippingSet() {
+        return whitespaceStripping != null;
+    }
+
+    /**
+     * Sets the output format of the template; see {@link Configuration#setAutoEscapingPolicy(int)} for more.
+     */
+    public void setAutoEscapingPolicy(int autoEscapingPolicy) {
+        Configuration.validateAutoEscapingPolicyValue(autoEscapingPolicy);
+
+        this.autoEscapingPolicy = Integer.valueOf(autoEscapingPolicy);
+    }
+
+    /**
+     * The getter pair of {@link #setAutoEscapingPolicy(int)}.
+     */
+    @Override
+    public int getAutoEscapingPolicy() {
+        return autoEscapingPolicy != null ? autoEscapingPolicy.intValue()
+                : getDefaultAutoEscapingPolicy();
+    }
+
+    protected abstract int getDefaultAutoEscapingPolicy();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     */
+    @Override
+    public boolean isAutoEscapingPolicySet() {
+        return autoEscapingPolicy != null;
+    }
+
+    /**
+     * Sets the output format of the template; see {@link Configuration#setOutputFormat(OutputFormat)} for more.
+     */
+    public void setOutputFormat(OutputFormat outputFormat) {
+        _NullArgumentException.check("outputFormat", outputFormat);
+        this.outputFormat = outputFormat;
+    }
+
+    /**
+     * The getter pair of {@link #setOutputFormat(OutputFormat)}.
+     */
+    @Override
+    public OutputFormat getOutputFormat() {
+        return outputFormat != null ? outputFormat : getDefaultOutputFormat();
+    }
+
+    protected abstract OutputFormat getDefaultOutputFormat();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     */
+    @Override
+    public boolean isOutputFormatSet() {
+        return outputFormat != null;
+    }
+
+    /**
+     * See {@link Configuration#setRecognizeStandardFileExtensions(boolean)}.
+     */
+    public void setRecognizeStandardFileExtensions(boolean recognizeStandardFileExtensions) {
+        this.recognizeStandardFileExtensions = Boolean.valueOf(recognizeStandardFileExtensions);
+    }
+
+    /**
+     * Getter pair of {@link #setRecognizeStandardFileExtensions(boolean)}.
+     */
+    @Override
+    public boolean getRecognizeStandardFileExtensions() {
+        return recognizeStandardFileExtensions != null ? recognizeStandardFileExtensions.booleanValue()
+                : getDefaultRecognizeStandardFileExtensions();
+    }
+
+    protected abstract boolean getDefaultRecognizeStandardFileExtensions();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     */
+    @Override
+    public boolean isRecognizeStandardFileExtensionsSet() {
+        return recognizeStandardFileExtensions != null;
+    }
+
+    @Override
+    public String getEncoding() {
+        return encoding != null ? encoding : getDefaultEncoding();
+    }
+
+    protected abstract String getDefaultEncoding();
+
+    /**
+     * The charset to be used when reading the template "file" that the {@link TemplateLoader} returns as binary
+     * ({@link InputStream}). If the {@code #ftl} header sepcifies an encoding, that will override this.
+     */
+    public void setEncoding(String encoding) {
+        _NullArgumentException.check("encoding", encoding);
+        this.encoding = encoding;
+    }
+
+    public boolean isEncodingSet() {
+        return encoding != null;
+    }
+
+    /**
+     * See {@link Configuration#setTabSize(int)}.
+     *
+     * @since 2.3.25
+     */
+    public void setTabSize(int tabSize) {
+        this.tabSize = Integer.valueOf(tabSize);
+    }
+
+    /**
+     * Getter pair of {@link #setTabSize(int)}.
+     *
+     * @since 2.3.25
+     */
+    @Override
+    public int getTabSize() {
+        return tabSize != null ? tabSize.intValue()
+                : getDefailtTabSize();
+    }
+
+    protected abstract int getDefailtTabSize();
+
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     *
+     * @since 2.3.25
+     */
+    @Override
+    public boolean isTabSizeSet() {
+        return tabSize != null;
+    }
+
+}