freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject incubator-freemarker git commit: 1. It's now possible to set the auto_imports and auto_includes settings on a per template basis (like templates with a certain name pattern has different auto-imports). This is now possible as these settings were moved fr
Date Tue, 07 Jun 2016 22:35:58 GMT
Repository: incubator-freemarker
Updated Branches:
  refs/heads/2.3-gae 6eb6ed7a8 -> 346797e45


1. It's now possible to set the auto_imports and auto_includes settings on a per template basis (like templates with a certain name pattern has different auto-imports). This is now possible as these settings were moved from the Configuration level down to the more generic Configurable level, and so are inherited by TemplateConfigurer and Environment too.

2. Bug fixed: There was a regression with 2.3.24, where Configuration.setAutoImports() haven't removed auto-imports added earlier. (Though it's unlikely that an application uses that method and also adds auto-imports earlier.)


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/346797e4
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/346797e4
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/346797e4

Branch: refs/heads/2.3-gae
Commit: 346797e45fe34edb76afa7f862c9a929688e08c9
Parents: 6eb6ed7
Author: ddekany <ddekany@apache.org>
Authored: Sun Jun 5 17:39:03 2016 +0200
Committer: ddekany <ddekany@apache.org>
Committed: Wed Jun 8 00:35:44 2016 +0200

----------------------------------------------------------------------
 src/main/java/freemarker/core/Configurable.java | 289 +++++++++++++++++++
 .../freemarker/core/TemplateConfiguration.java  |  45 ++-
 src/main/java/freemarker/core/_CoreAPI.java     |   1 +
 ..._ParserConfigurationWithInheritedFormat.java |   1 +
 .../java/freemarker/template/Configuration.java | 189 ++++--------
 src/main/java/freemarker/template/Template.java |   4 +-
 .../java/freemarker/template/_TemplateAPI.java  |   2 +
 src/manual/en_US/book.xml                       |  20 ++
 .../core/AutoImportAndIncludeTest.java          | 131 +++++++++
 .../core/TemplateConfigurationTest.java         |  62 +++-
 src/test/java/freemarker/test/TemplateTest.java |   5 +
 11 files changed, 608 insertions(+), 141 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/main/java/freemarker/core/Configurable.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index ebfb312..40e873e 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -237,6 +237,20 @@ public class Configurable {
     public static final String LOG_TEMPLATE_EXCEPTIONS_KEY_CAMEL_CASE = "logTemplateExceptions";
     /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. @since 2.3.22 */
     public static final String LOG_TEMPLATE_EXCEPTIONS_KEY = LOG_TEMPLATE_EXCEPTIONS_KEY_SNAKE_CASE;
+
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+    public static final String AUTO_IMPORT_KEY_SNAKE_CASE = "auto_import";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+    public static final String AUTO_IMPORT_KEY_CAMEL_CASE = "autoImport";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String AUTO_IMPORT_KEY = AUTO_IMPORT_KEY_SNAKE_CASE;
+    
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+    public static final String AUTO_INCLUDE_KEY_SNAKE_CASE = "auto_include";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+    public static final String AUTO_INCLUDE_KEY_CAMEL_CASE = "autoInclude";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String AUTO_INCLUDE_KEY = AUTO_INCLUDE_KEY_SNAKE_CASE;
     
     /** @deprecated Use {@link #STRICT_BEAN_MODELS_KEY} instead. */
     @Deprecated
@@ -247,6 +261,8 @@ public class Configurable {
         API_BUILTIN_ENABLED_KEY_SNAKE_CASE,
         ARITHMETIC_ENGINE_KEY_SNAKE_CASE,
         AUTO_FLUSH_KEY_SNAKE_CASE,
+        AUTO_IMPORT_KEY_SNAKE_CASE,
+        AUTO_INCLUDE_KEY_SNAKE_CASE,
         BOOLEAN_FORMAT_KEY_SNAKE_CASE,
         CLASSIC_COMPATIBLE_KEY_SNAKE_CASE,
         CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE,
@@ -273,6 +289,8 @@ public class Configurable {
         API_BUILTIN_ENABLED_KEY_CAMEL_CASE,
         ARITHMETIC_ENGINE_KEY_CAMEL_CASE,
         AUTO_FLUSH_KEY_CAMEL_CASE,
+        AUTO_IMPORT_KEY_CAMEL_CASE,
+        AUTO_INCLUDE_KEY_CAMEL_CASE,
         BOOLEAN_FORMAT_KEY_CAMEL_CASE,
         CLASSIC_COMPATIBLE_KEY_CAMEL_CASE,
         CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
@@ -324,6 +342,8 @@ public class Configurable {
     private Boolean logTemplateExceptions;
     private Map<String, ? extends TemplateDateFormatFactory> customDateFormats;
     private Map<String, ? extends TemplateNumberFormatFactory> customNumberFormats;
+    private LinkedHashMap<String, String> autoImports;
+    private ArrayList<String> autoIncludes;
     
     /**
      * Creates a top-level configurable, one that doesn't inherit from a parent, and thus stores the default values.
@@ -426,6 +446,12 @@ public class Configurable {
         Configurable copy = (Configurable) super.clone();
         copy.properties = new Properties(properties);
         copy.customAttributes = (HashMap) customAttributes.clone();
+        if (autoImports != null) {
+            copy.autoImports = (LinkedHashMap<String, String>) autoImports.clone();
+        }
+        if (autoIncludes != null) {
+            copy.autoIncludes = (ArrayList<String>) autoIncludes.clone();
+        }
         return copy;
     }
     
@@ -1530,6 +1556,260 @@ public class Configurable {
         return logTemplateExceptions != null;
     }
     
+    /**
+     * Adds an invisible <code>#import <i>templateName</i> as <i>namespaceVarName</i></code> at the beginning of the
+     * main template (that's the top-level template that wasn't included/imported from another template). While it only
+     * affects the main template directly, as the imports will create a global variable there, the imports will be
+     * visible from the further imported templates too (note that {@link Configuration#getIncompatibleImprovements()}
+     * set to 2.3.24 fixes a rarely surfacing bug with that).
+     * 
+     * <p>
+     * It's recommended to set the {@code auto_impots_lazy} setting ({@link Configuration#setLazyAutoImports(Boolean)})
+     * to {@code true} when using this, so that auto-imports that are unused in a template won't degrade performance by
+     * unnecessary loading and initializing the imported library.
+     * 
+     * <p>
+     * If the imports aren't lazy, the order of the imports will be the same as the order in which they were added with
+     * this method. (Calling this method with an already added {@code namespaceVarName} will move that to the end
+     * of the auto-import order.)
+     * 
+     * <p>
+     * The auto-import is added directly to the {@link Configurable} on which this method is called (not to the parents
+     * or children), but when the main template is processed, the auto-imports are collected from all the
+     * {@link Configurable} levels, in parent-to-child order: {@link Configuration}, {@link Template} (the main
+     * template), {@link Environment}. If the same {@code namespaceVarName} occurs on multiple levels, the one on the
+     * child level is used, and the clashing import from the parent level is skipped.
+     * 
+     * <p>If there are also auto-includes (see {@link #addAutoInclude(String)}), those will be executed after
+     * the auto-imports.
+     * 
+     * @see #setAutoImports(Map)
+     */
+    public void addAutoImport(String namespaceVarName, String templateName) {
+        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
+        synchronized (this) {
+            if (autoImports == null) {
+                autoImports = new LinkedHashMap<String, String>(4);
+            } else {
+                // This was a List earlier, so re-inserted items must go to the end, hence we remove() before put().
+                autoImports.remove(namespaceVarName);
+            }
+            autoImports.put(namespaceVarName, templateName);
+        }
+    }
+    
+    /**
+     * Removes an auto-import from this {@link Configurable} level (not from the parents or children);
+     * see {@link #addAutoImport(String, String)}. Does nothing if the auto-import doesn't exist.
+     */
+    public void removeAutoImport(String namespaceVarName) {
+        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
+        synchronized (this) {
+            if (autoImports != null) {
+                autoImports.remove(namespaceVarName);
+            }
+        }
+    }
+    
+    /**
+     * Removes all auto-imports, then calls {@link #addAutoImport(String, String)} for each {@link Map}-entry (the entry
+     * key is the {@code namespaceVarName}). The order of the auto-imports will be the same as {@link Map#keySet()}
+     * returns the keys (but the order of imports doesn't mater for properly designed libraries anyway).
+     * 
+     * @param map Maps the namepsace variable names to the template names; not {@code null}
+     */
+    public void setAutoImports(Map map) {
+        NullArgumentException.check("map", map);
+        
+        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
+        synchronized (this) {
+            if (autoImports != null) {
+                autoImports.clear();
+            }
+            for (Map.Entry<?, ?> entry : ((Map<?, ?>) map).entrySet()) {
+                Object key = entry.getKey();
+                if (!(key instanceof String)) {
+                    throw new IllegalArgumentException(
+                            "Key in Map wasn't a String, but a(n) " + key.getClass().getName() + ".");
+                }
+                
+                Object value = entry.getValue();
+                if (!(value instanceof String)) {
+                    throw new IllegalArgumentException(
+                            "Value in Map wasn't a String, but a(n) " + key.getClass().getName() + ".");
+                }
+                
+                addAutoImport((String) key, (String) value);
+            }
+        }
+    }
+    
+    /**
+     * Getter pair of {@link #setAutoImports(Map)}; do not modify the returned {@link Map}! To be consistent with other
+     * setting getters, if this setting was set directly on this {@link Configurable} object, this simply returns that
+     * value, otherwise it returns the value from the parent {@link Configurable}. So beware, the returned value doesn't
+     * reflect the {@link Map} key granularity fallback logic that FreeMarker actually uses for this setting. The
+     * returned value is not the same {@link Map} object that was set with {@link #setAutoImports(Map)}, only its
+     * content is the same. The returned value isn't a snapshot; it may or may not shows the changes later made to this
+     * setting on this {@link Configurable} level (but usually it's well defined if until what point settings are
+     * possibly modified).
+     * 
+     * <p>
+     * The return value is never {@code null}; called on the {@link Configuration} (top) level, it defaults to an empty
+     * {@link Map}.
+     * 
+     * @see #getAutoImportsWithoutFallback()
+     * 
+     * @since 2.3.25
+     */
+    public Map<String, String> getAutoImports() {
+        return autoImports != null ? autoImports
+                : parent != null ? parent.getAutoImports()
+                : Collections.<String, String>emptyMap();
+    }
+    
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     * 
+     * @since 2.3.25
+     */
+    public boolean isAutoImportsSet() {
+        return autoImports != null;
+    }
+
+    /**
+     * Like {@link #getAutoImports()}, but doesn't fall back to the parent {@link Configurable}, nor does it provide
+     * a non-{@code null} default when called as the method of a {@link Configuration}.
+     *  
+     * @since 2.3.25
+     */
+    public Map<String, String> getAutoImportsWithoutFallback() {
+        return autoImports;
+    }
+    
+    /**
+     * Adds an invisible <code>#include <i>templateName</i></code> at the beginning of the main template (that's the
+     * top-level template that wasn't included/imported from another template).
+     * 
+     * <p>
+     * The order of the inclusions will be the same as the order in which they were added with this method.
+     * 
+     * <p>
+     * The auto-include is added directly to the {@link Configurable} on which this method is called (not to the parents
+     * or children), but when the main template is processed, the auto-includes are collected from all the
+     * {@link Configurable} levels, in parent-to-child order: {@link Configuration}, {@link Template} (the main
+     * template), {@link Environment}.
+     * 
+     * <p>
+     * If there are also auto-imports ({@link #addAutoImport(String, String)}), those imports will be executed before
+     * the auto-includes, hence the namespace variables are accessible for the auto-included templates.
+     * 
+     * <p>
+     * You should avoid adding the same auto-include for multiple times, as you can easily end up including the same
+     * template for multiple times. Calling {@link #addAutoInclude(String)} with an already added template name will
+     * just move that to the end of the auto-include list, but the same won't happen when using
+     * {@link #setAutoIncludes(List)}, nor when the same string occurs on a different {@link Configurable} level.
+     * 
+     * @see #setAutoIncludes(List)
+     */
+    public void addAutoInclude(String templateName) {
+        addAutoInclude(templateName, true);
+    }
+
+    /**
+     * @param removeDuplicate
+     *            Used for emulating legacy glitch, where duplicates weren't removed if the inclusion was added via
+     *            {@link #setAutoIncludes(List)}, but were if it was added via {@link #addAutoInclude(String)}.
+     */
+    private void addAutoInclude(String templateName, boolean removeDuplicate) {
+        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
+        synchronized (this) {
+            if (autoIncludes == null) {
+                autoIncludes = new ArrayList<String>(4);
+            } else {
+                if (removeDuplicate) {
+                    autoIncludes.remove(templateName);
+                }
+            }
+            autoIncludes.add(templateName);
+        }
+    }
+    
+    /**
+     * Removes all auto-includes, then calls {@link #addAutoInclude(String)} for each {@link List} items.
+     */
+    public void setAutoIncludes(List templateNames) {
+        NullArgumentException.check("templateNames", templateNames);
+        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
+        synchronized (this) {
+            if (autoIncludes != null) {
+                autoIncludes.clear();
+            }
+            for (Object templateName : templateNames) {
+                if (!(templateName instanceof String)) {
+                    throw new IllegalArgumentException("List items must be String-s.");
+                }
+                addAutoInclude((String) templateName, false);
+            }
+        }
+    }
+    
+    /**
+     * Getter pair of {@link #setAutoIncludes(List)}; do not modify the returned {@link List}! To be consistent with other
+     * setting getters, if this setting was set directly on this {@link Configurable} object, this simply returns that
+     * value, otherwise it returns the value from the parent {@link Configurable}. So beware, the returned value doesn't
+     * reflect the {@link List} concatenation logic that FreeMarker actually uses for this setting. The
+     * returned value is not the same {@link List} object that was set with {@link #setAutoIncludes(List)}, only its
+     * content is the same (except that duplicate are removed). The returned value isn't a snapshot; it may or may not shows the changes later made to this
+     * setting on this {@link Configurable} level (but usually it's well defined if until what point settings are
+     * possibly modified).
+     * 
+     * <p>
+     * The return value is never {@code null}; called on the {@link Configuration} (top) level, it defaults to an empty
+     * {@link List}.
+     * 
+     * @see #getAutoIncludesWithoutFallback()
+     * 
+     * @since 2.3.25
+     */
+    public List<String> getAutoIncludes() {
+        return autoIncludes != null ? autoIncludes
+                : parent != null ? parent.getAutoIncludes()
+                : Collections.<String>emptyList();
+    }
+    
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     * 
+     * @since 2.3.25
+     */
+    public boolean isAutoIncludesSet() {
+        return autoIncludes != null;
+    }
+    
+    /**
+     * Like {@link #getAutoIncludes()}, but doesn't fall back to the parent {@link Configurable}, nor does it provide
+     * a non-{@code null} default when called as the method of a {@link Configuration}.
+     *  
+     * @since 2.3.25
+     */
+    public List<String> getAutoIncludesWithoutFallback() {
+        return autoIncludes;
+    }
+    
+    /**
+     * Removes the auto-include from this {@link Configurable} level (not from the parents or children); see
+     * {@link #addAutoInclude(String)}. Does nothing if the template is not there.
+     */
+    public void removeAutoInclude(String templateName) {
+        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
+        synchronized (this) {
+            if (autoIncludes != null) {
+                autoIncludes.remove(templateName);
+            }
+        }
+    }
+    
     private static final String ALLOWED_CLASSES = "allowed_classes";
     private static final String TRUSTED_TEMPLATES = "trusted_templates";
     
@@ -2097,6 +2377,11 @@ public class Configurable {
             } else if (LOG_TEMPLATE_EXCEPTIONS_KEY_SNAKE_CASE.equals(name)
                     || LOG_TEMPLATE_EXCEPTIONS_KEY_CAMEL_CASE.equals(name)) {
                 setLogTemplateExceptions(StringUtil.getYesNo(value));
+            } else if (AUTO_INCLUDE_KEY_SNAKE_CASE.equals(name)
+                    || AUTO_INCLUDE_KEY_CAMEL_CASE.equals(name)) {
+                setAutoIncludes(parseAsList(value));
+            } else if (AUTO_IMPORT_KEY_SNAKE_CASE.equals(name) || AUTO_IMPORT_KEY_CAMEL_CASE.equals(name)) {
+                setAutoImports(parseAsImportList(value));
             } else {
                 unknown = true;
             }
@@ -2405,6 +2690,10 @@ public class Configurable {
         return retval;
     }
     
+    /**
+     * Executes the auto-imports and auto-includes for the main template of this environment.
+     * This is not meant to be called or overridden by code outside of FreeMarker. 
+     */
     protected void doAutoImportsAndIncludes(Environment env)
     throws TemplateException, IOException {
         if (parent != null) parent.doAutoImportsAndIncludes(env);

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/main/java/freemarker/core/TemplateConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/TemplateConfiguration.java b/src/main/java/freemarker/core/TemplateConfiguration.java
index 30484dc..9fb9cb3 100644
--- a/src/main/java/freemarker/core/TemplateConfiguration.java
+++ b/src/main/java/freemarker/core/TemplateConfiguration.java
@@ -19,7 +19,9 @@
 package freemarker.core;
 
 import java.io.Reader;
+import java.util.ArrayList;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import freemarker.cache.TemplateCache;
@@ -32,8 +34,8 @@ import freemarker.template.utility.NullArgumentException;
 /**
  * Used for customizing the configuration settings for individual {@link Template}-s (or rather groups of templates),
  * relatively to the common setting values coming from the {@link Configuration}. This was designed with the standard
- * template loading mechanism of FreeMarker in mind ({@link Configuration#getTemplate(String)} and {@link TemplateCache}
- * ), though can also be reused for custom template loading and caching solutions.
+ * template loading mechanism of FreeMarker in mind ({@link Configuration#getTemplate(String)}
+ * and {@link TemplateCache}), though can also be reused for custom template loading and caching solutions.
  * 
  * <p>
  * Note on the {@code locale} setting: When used with the standard template loading/caching mechanism (
@@ -242,6 +244,12 @@ public final class TemplateConfiguration extends Configurable implements ParserC
         if (tc.isTabSizeSet()) {
             setTabSize(tc.getTabSize());
         }
+        if (tc.isAutoImportsSet()) {
+            setAutoImports(mergeMaps(getAutoImports(), tc.getAutoImports()));
+        }
+        if (tc.isAutoIncludesSet()) {
+            setAutoIncludes(mergeLists(getAutoIncludes(), tc.getAutoIncludes()));
+        }
         
         tc.copyDirectCustomAttributes(this, true);
     }
@@ -338,6 +346,16 @@ public final class TemplateConfiguration extends Configurable implements ParserC
         if (isURLEscapingCharsetSet() && !template.isURLEscapingCharsetSet()) {
             template.setURLEscapingCharset(getURLEscapingCharset());
         }
+        if (isAutoImportsSet()) {
+            // Regarding the order of the maps in the merge:
+            // - Existing template-level imports have precedence over those coming from the TC (just as with the others
+            //   apply()-ed settings), thus for clashing import prefixes they must win.
+            // - Template-level imports count as more specific, and so come after the more generic ones from TC.
+            template.setAutoImports(mergeMaps(getAutoImports(), template.getAutoImportsWithoutFallback()));
+        }
+        if (isAutoIncludesSet()) {
+            template.setAutoIncludes(mergeLists(getAutoIncludes(), template.getAutoIncludesWithoutFallback()));
+        }
         
         copyDirectCustomAttributes(template, false);
     }
@@ -555,7 +573,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC
      */
     public boolean isTabSizeSet() {
         return tabSize != null;
-    }    
+    }
     
     /**
      * Returns {@link Configuration#getIncompatibleImprovements()} from the parent {@link Configuration}. This mostly
@@ -580,6 +598,8 @@ public final class TemplateConfiguration extends Configurable implements ParserC
                 isAPIBuiltinEnabledSet()
                 || isArithmeticEngineSet()
                 || isAutoFlushSet()
+                || isAutoImportsSet()
+                || isAutoIncludesSet()
                 || isBooleanFormatSet()
                 || isClassicCompatibleSet()
                 || isCustomDateFormatsSet()
@@ -603,12 +623,25 @@ public final class TemplateConfiguration extends Configurable implements ParserC
     private Map mergeMaps(Map m1, Map m2) {
         if (m1 == null) return m2;
         if (m2 == null) return m1;
-        if (m1.isEmpty()) return m2;
-        if (m2.isEmpty()) return m1;
+        if (m1.isEmpty()) return m2 != null ? m2 : m1;
+        if (m2.isEmpty()) return m1 != null ? m1 : m2;
         
-        LinkedHashMap mergedM = new LinkedHashMap(m1);
+        LinkedHashMap mergedM = new LinkedHashMap((m1.size() + m2.size()) * 4 / 3 + 1, 0.75f);
+        mergedM.putAll(m1);
         mergedM.putAll(m2);
         return mergedM;
     }
+
+    private List<String> mergeLists(List<String> list1, List<String> list2) {
+        if (list1 == null) return list2;
+        if (list2 == null) return list1;
+        if (list1.isEmpty()) return list2 != null ? list2 : list1;
+        if (list2.isEmpty()) return list1 != null ? list1 : list2;
+        
+        ArrayList<String> mergedList = new ArrayList<String>(list1.size() + list2.size());
+        mergedList.addAll(list1);
+        mergedList.addAll(list2);
+        return mergedList;
+    }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/main/java/freemarker/core/_CoreAPI.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index df95196..3fb39e3 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -22,6 +22,7 @@ package freemarker.core;
 import java.io.Writer;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java b/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java
index 9acaa54..8248b5c 100644
--- a/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java
+++ b/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java
@@ -75,4 +75,5 @@ public final class _ParserConfigurationWithInheritedFormat implements ParserConf
     public int getTabSize() {
         return wrappedPCfg.getTabSize();
     }
+    
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/main/java/freemarker/template/Configuration.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index 35a7d3a..1a7381a 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -222,11 +222,20 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
     public static final String TEMPLATE_UPDATE_DELAY_KEY = TEMPLATE_UPDATE_DELAY_KEY_SNAKE_CASE;
     
-    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    /**
+     * Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23
+     * @deprecated Use {@link Configurable#AUTO_IMPORT_KEY_SNAKE_CASE} instead.
+     */
     public static final String AUTO_IMPORT_KEY_SNAKE_CASE = "auto_import";
-    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    /**
+     * Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23
+     * @deprecated Use {@link Configurable#AUTO_IMPORT_KEY_CAMEL_CASE} instead.
+     */
     public static final String AUTO_IMPORT_KEY_CAMEL_CASE = "autoImport";
-    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    /**
+     * Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints.
+     * @deprecated Use {@link Configurable#AUTO_IMPORT_KEY_SNAKE_CASE} instead.
+     */
     public static final String AUTO_IMPORT_KEY = AUTO_IMPORT_KEY_SNAKE_CASE;
     
     /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
@@ -316,8 +325,6 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     private static final String[] SETTING_NAMES_SNAKE_CASE = new String[] {
         // Must be sorted alphabetically!
         AUTO_ESCAPING_POLICY_KEY_SNAKE_CASE,
-        AUTO_IMPORT_KEY_SNAKE_CASE,
-        AUTO_INCLUDE_KEY_SNAKE_CASE,
         CACHE_STORAGE_KEY_SNAKE_CASE,
         DEFAULT_ENCODING_KEY_SNAKE_CASE,
         INCOMPATIBLE_IMPROVEMENTS_KEY_SNAKE_CASE,
@@ -342,8 +349,6 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     private static final String[] SETTING_NAMES_CAMEL_CASE = new String[] {
         // Must be sorted alphabetically!
         AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE,
-        AUTO_IMPORT_KEY_CAMEL_CASE,
-        AUTO_INCLUDE_KEY_CAMEL_CASE,
         CACHE_STORAGE_KEY_CAMEL_CASE,
         DEFAULT_ENCODING_KEY_CAMEL_CASE,
         INCOMPATIBLE_IMPROVEMENTS_KEY_CAMEL_CASE,
@@ -530,8 +535,6 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     private String defaultEncoding = SecurityUtilities.getSystemProperty("file.encoding", "utf-8");
     private ConcurrentMap localeToCharsetMap = new ConcurrentHashMap();
     
-    private LinkedHashMap<String, String> autoImports = new LinkedHashMap<String, String>(0);
-    private ArrayList<String> autoIncludes = new ArrayList<String>(0);
     private boolean lazyImpots;
     private Boolean lazyAutoImports;
 
@@ -975,8 +978,6 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
             Configuration copy = (Configuration) super.clone();
             copy.sharedVariables = new HashMap(sharedVariables);
             copy.localeToCharsetMap = new ConcurrentHashMap(localeToCharsetMap);
-            copy.autoImports = (LinkedHashMap<String, String>) autoImports.clone();
-            copy.autoIncludes = (ArrayList<String>) autoIncludes.clone();
             copy.recreateTemplateCacheWith(
                     cache.getTemplateLoader(), cache.getCacheStorage(),
                     cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(),
@@ -2941,11 +2942,6 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                     valueWithoutUnit = value;
                 }
                 setTemplateUpdateDelayMilliseconds(Integer.parseInt(valueWithoutUnit) * multipier);
-            } else if (AUTO_INCLUDE_KEY_SNAKE_CASE.equals(name)
-                    || AUTO_INCLUDE_KEY_CAMEL_CASE.equals(name)) {
-                setAutoIncludes(parseAsList(value));
-            } else if (AUTO_IMPORT_KEY_SNAKE_CASE.equals(name) || AUTO_IMPORT_KEY_CAMEL_CASE.equals(name)) {
-                setAutoImports(parseAsImportList(value));
             } else if (LAZY_AUTO_IMPORTS_KEY_SNAKE_CASE.equals(name) || LAZY_AUTO_IMPORTS_KEY_CAMEL_CASE.equals(name)) {
                 setLazyAutoImports(value.equals(NULL) ? null : Boolean.valueOf(StringUtil.getYesNo(value)));
             } else if (LAZY_IMPORTS_KEY_SNAKE_CASE.equals(name) || LAZY_IMPORTS_KEY_CAMEL_CASE.equals(name)) {
@@ -3061,130 +3057,69 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         return super.getCorrectedNameForUnknownSetting(name);
     }
     
-    /**
-     * Adds an invisible <code>#import <i>templateName</i> as <i>namespaceVarName</i></code> at the beginning of all
-     * top-level templates (that is, to all templates that weren't included/imported from another template). While it
-     * only affects the top-level (main) template directly, as the imports there will create a global variable, the
-     * imports will be visible from the further imported templates too (though note that
-     * {@link #getIncompatibleImprovements()} set to 2.3.24 fixes a rarely surfacing bug here).
-     * 
-     * <p>It's recommended to set the {@code auto_impots_lazy} setting ({@link #setLazyAutoImports(Boolean)}) to
-     * {@code true} when using this, so that auto-imports that are unused in a template won't degrade performance by
-     * unnecessary loading and initializing the imported library.
-     * 
-     * <p>
-     * The order of the imports will be the same as the order in which they were added with this method. Note that if
-     * there are also auto-includes ({@link #addAutoInclude(String)}), those inclusions will be executed after the
-     * auto-includes.
-     * 
-     * @see #setAutoImports(Map)
-     */
-    public void addAutoImport(String namespaceVarName, String templateName) {
-        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
-        synchronized (this) {
-            // This was a List earlier, so re-inserted items must go to the end, hence we remove() before put().
-            autoImports.remove(namespaceVarName);
-            autoImports.put(namespaceVarName, templateName);
-        }
-    }
-    
-    /**
-     * Removes an auto-import; see {@link #addAutoImport(String, String)}. Does nothing if the auto-import doesn't
-     * exist.
-     */
-    public void removeAutoImport(String namespaceVarName) {
-        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
-        synchronized (this) {
-            autoImports.remove(namespaceVarName);
-        }
+    @Override
+    protected void doAutoImportsAndIncludes(Environment env) throws TemplateException, IOException {
+        Template t = env.getMainTemplate();
+        doAutoImports(env, t);
+        doAutoIncludes(env, t);
     }
-    
-    /**
-     * Removes all auto-imports, then calls {@link #addAutoImport(String, String)} for each {@link Map}-entry (the entry
-     * key is the {@code namespaceVarName}). The order of the auto-imports will be the same as {@link Map#keySet()}
-     * returns the keys (but the order of imports doesn't mater for properly designed libraries anyway).
-     */
-    public void setAutoImports(Map map) {
-        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
-        synchronized (this) {
-            for (Map.Entry entry : ((Map<?, ?>) map).entrySet()) {
-                Object key = entry.getKey();
-                if (!(key instanceof String)) {
-                    throw new IllegalArgumentException(
-                            "Key in map wasn't a String, but a(n) " + key.getClass().getName() + ".");
-                }
-                Object value = entry.getValue();
-                if (!(value instanceof String)) {
-                    throw new IllegalArgumentException(
-                            "Value in map wasn't a String, but a(n) " + key.getClass().getName() + ".");
+
+    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> cfgAutoImports = getAutoImportsWithoutFallback();
+        
+        boolean lazyAutoImports = getLazyAutoImports() != null ? getLazyAutoImports().booleanValue() : getLazyImports();
+        
+        if (cfgAutoImports != null) {
+            for (Map.Entry<String, String> autoImport : cfgAutoImports.entrySet()) {
+                String nsVarName = autoImport.getKey();
+                if ((tAutoImports == null || !tAutoImports.containsKey(nsVarName))
+                        && (envAutoImports == null || !envAutoImports.containsKey(nsVarName))) {
+                    env.importLib(autoImport.getValue(), nsVarName, lazyAutoImports);
                 }
-                addAutoImport((String) key, (String) value);
             }
         }
-    }
-    
-    @Override
-    protected void doAutoImportsAndIncludes(Environment env)
-    throws TemplateException, IOException {
-        for (Map.Entry<String, String> autoImport : autoImports.entrySet()) {
-            Boolean autoImportsLazy = getLazyAutoImports();
-            env.importLib(
-                    autoImport.getValue(), autoImport.getKey(),
-                    autoImportsLazy != null ? autoImportsLazy.booleanValue() : getLazyImports());
+        if (tAutoImports != null) {
+            for (Map.Entry<String, String> autoImport : tAutoImports.entrySet()) {
+                String nsVarName = autoImport.getKey();
+                if (envAutoImports == null || !envAutoImports.containsKey(nsVarName)) {
+                    env.importLib(autoImport.getValue(), nsVarName, lazyAutoImports);
+                }
+            }
         }
-        for (int i = 0; i < autoIncludes.size(); i++) {
-            String templateName = autoIncludes.get(i);
-            Template template = getTemplate(templateName, env.getLocale());
-            env.include(template);
+        if (envAutoImports != null) {
+            for (Map.Entry<String, String> autoImport : envAutoImports.entrySet()) {
+                String nsVarName = autoImport.getKey();
+                env.importLib(autoImport.getValue(), nsVarName, lazyAutoImports);
+            }
         }
     }
     
-    /**
-     * Adds an invisible <code>#include <i>templateName</i> as <i>namespaceVarName</i></code> at the beginning of all
-     * top-level templates (that is, to all templates that weren't included/imported from another template).
-     * 
-     * <p>
-     * The order of the inclusions will be the same as the order in which they were added with this method. Note that if
-     * there are also auto-imports ({@link #addAutoImport(String, String)}), those imports will be executed before the
-     * auto-includes, hence the library variables are accessible for the auto-includes.
-     * 
-     * @see #setAutoIncludes(List)
-     */
-    public void addAutoInclude(String templateName) {
-        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
-        synchronized (this) {
-            autoIncludes.remove(templateName);
-            autoIncludes.add(templateName);
+    private void doAutoIncludes(Environment env, Template t) throws TemplateException, IOException,
+            TemplateNotFoundException, MalformedTemplateNameException, ParseException {
+        List<String> cfgAutoIncludes = getAutoIncludesWithoutFallback();
+        if (cfgAutoIncludes != null) {
+            for (String templateName : cfgAutoIncludes) {
+                env.include(getTemplate(templateName, env.getLocale()));
+            }
         }
-    }
-
-    /**
-     * Removes all auto-includes, then calls {@link #addAutoInclude(String)} for each {@link List} items.
-     */
-    public void setAutoIncludes(List templateNames) {
-        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
-        synchronized (this) {
-            autoIncludes.clear();
-            for (Object templateName : templateNames) {
-                if (!(templateName instanceof String)) {
-                    throw new IllegalArgumentException("List items must be String-s.");
-                }
-                autoIncludes.add((String) templateName);
+        
+        List<String> tAutoIncludes = t.getAutoIncludesWithoutFallback();
+        if (tAutoIncludes != null) {
+            for (String templateName : tAutoIncludes) {
+                env.include(getTemplate(templateName, env.getLocale()));
             }
         }
-    }
-    
-    /**
-     * Removes a template from the auto-include list; see {@link #addAutoInclude(String)}. Does nothing if the template
-     * is not there.
-     */
-    public void removeAutoInclude(String templateName) {
-        // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
-        synchronized (this) {
-            autoIncludes.remove(templateName);
+        
+        List<String> envAutoIncludes = env.getAutoIncludesWithoutFallback();
+        if (envAutoIncludes != null) {
+            for (String templateName : envAutoIncludes) {
+                env.include(getTemplate(templateName, env.getLocale()));
+            }
         }
     }
-    
+
     /**
      * The getter pair of {@link #setLazyImports(boolean)}.
      * 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/main/java/freemarker/template/Template.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java
index 1da3aaf..51095e5 100644
--- a/src/main/java/freemarker/template/Template.java
+++ b/src/main/java/freemarker/template/Template.java
@@ -73,7 +73,7 @@ import freemarker.debug.impl.DebuggerService;
  * changing FreeMarker settings. Those must not be used while the template is being processed, or if the template object
  * is already accessible from multiple threads. If some templates need different settings that those coming from the
  * shared {@link Configuration}, and you are using {@link Configuration#getTemplate(String)} (or its overloads), then
- * see {@link Configuration#setTemplateConfigurations(freemarker.cache.TemplateConfigurationFactory)}.
+ * use {@link Configuration#setTemplateConfigurations(freemarker.cache.TemplateConfigurationFactory)} to achieve that.
  */
 public class Template extends Configurable {
     public static final String DEFAULT_NAMESPACE_PREFIX = "D";
@@ -700,7 +700,7 @@ public class Template extends Configurable {
     void setAutoEscaping(boolean autoEscaping) {
         this.autoEscaping = autoEscaping;
     }
-
+    
     /**
      * Dump the raw template in canonical form.
      */

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/main/java/freemarker/template/_TemplateAPI.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 765383e..c86c943 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -19,6 +19,8 @@
 
 package freemarker.template;
 
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import freemarker.cache.CacheStorage;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/manual/en_US/book.xml
----------------------------------------------------------------------
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 1965625..6d56358 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -26608,6 +26608,18 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
             </listitem>
 
             <listitem>
+              <para>It's now possible to set the
+              <literal>auto_imports</literal> and
+              <literal>auto_includes</literal> settings on a per template
+              basis (like templates with a certain name pattern has different
+              auto-imports). This is now possible as these settings were moved
+              from the <literal>Configuration</literal> level down to the more
+              generic <literal>Configurable</literal> level, and so are
+              inherited by <literal>TemplateConfigurer</literal> and
+              <literal>Environment</literal> too. </para>
+            </listitem>
+
+            <listitem>
               <para>New <literal>Configuration</literal> (and
               <literal>TemplateConfiguration</literal>) setting,
               <literal>tab_size</literal>. This only influences how the column
@@ -26665,6 +26677,14 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
             </listitem>
 
             <listitem>
+              <para>Bug fixed: There was a regression with 2.3.24, where
+              <literal>Configuration.setAutoImports()</literal> haven't
+              removed auto-imports added earlier. (Though it's unlikely that
+              an application uses that method and also adds auto-imports
+              earlier.)</para>
+            </listitem>
+
+            <listitem>
               <para>Removed FindBugs <literal>@SuppressFBWarnings</literal>
               annotations from the binary (<literal>freemarker.jar</literal>),
               as they have caused warnings like this when compiling dependant

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/test/java/freemarker/core/AutoImportAndIncludeTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/AutoImportAndIncludeTest.java b/src/test/java/freemarker/core/AutoImportAndIncludeTest.java
new file mode 100644
index 0000000..ea50078
--- /dev/null
+++ b/src/test/java/freemarker/core/AutoImportAndIncludeTest.java
@@ -0,0 +1,131 @@
+package freemarker.core;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.StringWriter;
+
+import org.junit.Test;
+
+import freemarker.cache.ConditionalTemplateConfigurationFactory;
+import freemarker.cache.FileNameGlobMatcher;
+import freemarker.cache.StringTemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.test.TemplateTest;
+
+public class AutoImportAndIncludeTest extends TemplateTest {
+
+    @Test
+    public void test3LayerImportNoClashes() throws Exception {
+        Configuration cfg = getConfiguration();
+        cfg.addAutoImport("t1", "t1.ftl");
+
+        TemplateConfiguration tc = new TemplateConfiguration();
+        tc.addAutoImport("t2", "t2.ftl");
+        cfg.setTemplateConfigurations(
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("main.ftl"), tc));
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3.ftl");
+    
+            env.process();
+            assertEquals("In main: t1;t2;t3;", sw.toString());
+        }
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("In main: t1;t2;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3.ftl");
+    
+            env.process();
+            assertEquals("In main2: t1;t3;", sw.toString());
+        }
+        
+        cfg.removeAutoImport("t1");
+        
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3.ftl");
+    
+            env.process();
+            assertEquals("In main: t2;t3;", sw.toString());
+        }
+    }
+    
+    @Test
+    public void test3LayerImportClashes() throws Exception {
+        Configuration cfg = getConfiguration();
+        cfg.addAutoImport("t1", "t1.ftl");
+        cfg.addAutoImport("t2", "t2.ftl");
+        cfg.addAutoImport("t3", "t3.ftl");
+
+        TemplateConfiguration tc = new TemplateConfiguration();
+        tc.addAutoImport("t2", "t2b.ftl");
+        cfg.setTemplateConfigurations(
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("main.ftl"), tc));
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3b.ftl");
+    
+            env.process();
+            assertEquals("In main: t1;t2b;t3b;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3b.ftl");
+    
+            env.process();
+            assertEquals("In main2: t1;t2;t3b;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("In main: t1;t3;t2b;", sw.toString());
+        }
+    }
+
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_24);
+        cfg.setTemplateLoader(new StringTemplateLoader());
+        return cfg;
+    }
+
+    @Override
+    protected void addCommonTemplates() {
+        addTemplate("main.ftl", "In main: ${loaded}");
+        addTemplate("main2.ftl", "In main2: ${loaded}");
+        addTemplate("t1.ftl", "<#global loaded = (loaded!) + 't1;'>T1;");
+        addTemplate("t2.ftl", "<#global loaded = (loaded!) + 't2;'>T2;");
+        addTemplate("t3.ftl", "<#global loaded = (loaded!) + 't3;'>T3;");
+        addTemplate("t1b.ftl", "<#global loaded = (loaded!) + 't1b;'>T1b;");
+        addTemplate("t2b.ftl", "<#global loaded = (loaded!) + 't2b;'>T2b;");
+        addTemplate("t3b.ftl", "<#global loaded = (loaded!) + 't3b;'>T3b;");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/test/java/freemarker/core/TemplateConfigurationTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/TemplateConfigurationTest.java b/src/test/java/freemarker/core/TemplateConfigurationTest.java
index ad3aaca..5b05e48 100644
--- a/src/test/java/freemarker/core/TemplateConfigurationTest.java
+++ b/src/test/java/freemarker/core/TemplateConfigurationTest.java
@@ -35,16 +35,20 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
 
+import org.apache.commons.collections.ListUtils;
 import org.junit.Test;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
+import freemarker.cache.StringTemplateLoader;
 import freemarker.template.Configuration;
 import freemarker.template.SimpleObjectWrapper;
 import freemarker.template.Template;
@@ -97,6 +101,13 @@ public class TemplateConfigurationTest {
     private static final Version ICI = Configuration.VERSION_2_3_22;
 
     private static final Configuration DEFAULT_CFG = new Configuration(ICI);
+    static {
+        StringTemplateLoader stl = new StringTemplateLoader();
+        stl.putTemplate("t1.ftl", "<#global loaded = (loaded!) + 't1;'>In t1;");
+        stl.putTemplate("t2.ftl", "<#global loaded = (loaded!) + 't2;'>In t2;");
+        stl.putTemplate("t3.ftl", "<#global loaded = (loaded!) + 't3;'>In t3;");
+        DEFAULT_CFG.setTemplateLoader(stl);
+    }
 
     private static final TimeZone NON_DEFAULT_TZ;
     static {
@@ -180,6 +191,8 @@ public class TemplateConfigurationTest {
         
         // Special settings:
         SETTING_ASSIGNMENTS.put("encoding", NON_DEFAULT_ENCODING);
+        SETTING_ASSIGNMENTS.put("autoImports", ImmutableMap.of("a", "/lib/a.ftl"));
+        SETTING_ASSIGNMENTS.put("autoIncludes", ImmutableList.of("/lib/b.ftl"));
     }
     
     public static String getIsSetMethodName(String readMethodName) {
@@ -294,11 +307,13 @@ public class TemplateConfigurationTest {
                 propDesc2.getWriteMethod().invoke(tc2, value2);
 
                 tc1.merge(tc2);
-                Object mValue1 = propDesc1.getReadMethod().invoke(tc1);
-                Object mValue2 = propDesc2.getReadMethod().invoke(tc1);
-
-                assertEquals("For " + propDesc1.getName(), value1, mValue1);
-                assertEquals("For " + propDesc2.getName(), value2, mValue2);
+                if (propDesc1.getName().equals(propDesc2.getName()) && value1 instanceof List) {
+                    assertEquals("For " + propDesc1.getName(),
+                            ListUtils.union((List) value1, (List) value1), propDesc1.getReadMethod().invoke(tc1));
+                } else { // Values of the same setting merged
+                    assertEquals("For " + propDesc1.getName(), value1, propDesc1.getReadMethod().invoke(tc1));
+                    assertEquals("For " + propDesc2.getName(), value2, propDesc2.getReadMethod().invoke(tc1));
+                }
             }
         }
     }
@@ -312,6 +327,7 @@ public class TemplateConfigurationTest {
         tc1.setCustomNumberFormats(ImmutableMap.of(
                 "hex", HexTemplateNumberFormatFactory.INSTANCE,
                 "x", LocaleSensitiveTemplateNumberFormatFactory.INSTANCE));
+        tc1.setAutoImports(ImmutableMap.of("a", "a1.ftl", "b", "b1.ftl"));
         
         TemplateConfiguration tc2 = new TemplateConfiguration();
         tc2.setCustomDateFormats(ImmutableMap.of(
@@ -320,6 +336,7 @@ public class TemplateConfigurationTest {
         tc2.setCustomNumberFormats(ImmutableMap.of(
                 "loc", LocaleSensitiveTemplateNumberFormatFactory.INSTANCE,
                 "x", BaseNTemplateNumberFormatFactory.INSTANCE));
+        tc2.setAutoImports(ImmutableMap.of("b", "b2.ftl", "c", "c2.ftl"));
         
         tc1.merge(tc2);
         
@@ -332,6 +349,11 @@ public class TemplateConfigurationTest {
         assertEquals(HexTemplateNumberFormatFactory.INSTANCE, mergedCustomNumberFormats.get("hex"));
         assertEquals(LocaleSensitiveTemplateNumberFormatFactory.INSTANCE, mergedCustomNumberFormats.get("loc"));
         assertEquals(BaseNTemplateNumberFormatFactory.INSTANCE, mergedCustomNumberFormats.get("x"));
+
+        Map<String, String> mergedAutoImports = tc1.getAutoImports();
+        assertEquals("a1.ftl", mergedAutoImports.get("a"));
+        assertEquals("b2.ftl", mergedAutoImports.get("b"));
+        assertEquals("c2.ftl", mergedAutoImports.get("c"));
         
         // Empty map merging optimization:
         tc1.merge(new TemplateConfiguration());
@@ -346,6 +368,19 @@ public class TemplateConfigurationTest {
     }
     
     @Test
+    public void testMergeListSettings() throws Exception {
+        TemplateConfiguration tc1 = new TemplateConfiguration();
+        tc1.setAutoIncludes(ImmutableList.of("a.ftl", "b.ftl"));
+        
+        TemplateConfiguration tc2 = new TemplateConfiguration();
+        tc2.setAutoIncludes(ImmutableList.of("c.ftl", "d.ftl"));
+        
+        tc1.merge(tc2);
+        
+        assertEquals(ImmutableList.of("a.ftl", "b.ftl", "c.ftl", "d.ftl"), tc1.getAutoIncludes());
+    }
+    
+    @Test
     public void testMergePriority() throws Exception {
         TemplateConfiguration tc1 = new TemplateConfiguration();
         tc1.setDateFormat("1");
@@ -578,7 +613,6 @@ public class TemplateConfigurationTest {
             testedProps.add(Configuration.INCOMPATIBLE_IMPROVEMENTS_KEY_CAMEL_CASE);
         }
 
-
         {
             TemplateConfiguration tc = new TemplateConfiguration();
             tc.setParentConfiguration(new Configuration(new Version(2, 3, 0)));
@@ -653,6 +687,22 @@ public class TemplateConfigurationTest {
     }
 
     @Test
+    public void testAutoImport() throws TemplateException, IOException {
+        TemplateConfiguration tc = new TemplateConfiguration();
+        tc.setAutoImports(ImmutableMap.of("t1", "t1.ftl", "t2", "t2.ftl"));
+        tc.setParent(DEFAULT_CFG);
+        assertOutputWithoutAndWithTC(tc, "<#import 't3.ftl' as t3>${loaded}", "t3;", "t1;t2;t3;");
+    }
+
+    @Test
+    public void testAutoIncludes() throws TemplateException, IOException {
+        TemplateConfiguration tc = new TemplateConfiguration();
+        tc.setAutoIncludes(ImmutableList.of("t1.ftl", "t2.ftl"));
+        tc.setParent(DEFAULT_CFG);
+        assertOutputWithoutAndWithTC(tc, "<#include 't3.ftl'>", "In t3;", "In t1;In t2;In t3;");
+    }
+    
+    @Test
     public void testStringInterpolate() throws TemplateException, IOException {
         TemplateConfiguration tc = new TemplateConfiguration();
         tc.setParentConfiguration(DEFAULT_CFG);

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/346797e4/src/test/java/freemarker/test/TemplateTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/test/TemplateTest.java b/src/test/java/freemarker/test/TemplateTest.java
index 75a3e07..6904b80 100644
--- a/src/test/java/freemarker/test/TemplateTest.java
+++ b/src/test/java/freemarker/test/TemplateTest.java
@@ -57,6 +57,7 @@ public abstract class TemplateTest {
         if (configuration == null) {
             try {
                 configuration = createConfiguration();
+                addCommonTemplates();
             } catch (Exception e) {
                 throw new RuntimeException("Failed to set up configuration for the test", e);
             }
@@ -128,6 +129,10 @@ public abstract class TemplateTest {
     protected Configuration createConfiguration() throws Exception {
         return new Configuration(Configuration.VERSION_2_3_0);
     }
+    
+    protected void addCommonTemplates() {
+        //
+    }
 
     protected Object getDataModel() {
         if (!dataModelCreated) {


Mime
View raw message