freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [01/32] incubator-freemarker git commit: Lazy imports: With the new boolean Configuration-level settings, lazy_imports and lazy_auto_imports, you can request imports (as in <#import "lib/utils.ftl" as u>) and/or auto-imports to be lazy. When the import i
Date Sat, 26 Mar 2016 15:41:15 GMT
Repository: incubator-freemarker
Updated Branches:
  refs/heads/2.3 b9ca91dd5 -> 8eada2e9d


Lazy imports: With the new boolean Configuration-level settings, lazy_imports and lazy_auto_imports, you can request imports (as in <#import "lib/utils.ftl" as u>) and/or auto-imports to be lazy. When the import is lazy, the namespace variable (u in this example) will be created immediately, just like before, but the template will be imported and processed only when (and if ever) the content of the imported namespace is accessed. The main application of this is with auto-imports, where you don't want the overhead of importing templates that aren't actually used in a template. (Also, a new Environment.importLib method overload was added, where you can specify if you want a lazy or an eager import.)


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

Branch: refs/heads/2.3
Commit: 6bdb716e6191d8dc8edad70aed12172f6f4e1698
Parents: 6166efe
Author: ddekany <ddekany@apache.org>
Authored: Sun Jan 10 15:00:16 2016 +0100
Committer: ddekany <ddekany@apache.org>
Committed: Sun Jan 10 20:09:57 2016 +0100

----------------------------------------------------------------------
 src/main/java/freemarker/cache/_CacheAPI.java   |  11 +-
 src/main/java/freemarker/core/Configurable.java |  10 +
 src/main/java/freemarker/core/Environment.java  | 333 ++++++++++++++++---
 src/main/java/freemarker/core/LibraryLoad.java  |  16 +-
 .../java/freemarker/template/Configuration.java |  93 +++++-
 src/manual/en_US/book.xml                       |  42 ++-
 .../freemarker/core/IncludeAndImportTest.java   | 173 +++++++++-
 .../freemarker/template/ConfigurationTest.java  |  22 ++
 8 files changed, 632 insertions(+), 68 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/6bdb716e/src/main/java/freemarker/cache/_CacheAPI.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/cache/_CacheAPI.java b/src/main/java/freemarker/cache/_CacheAPI.java
index 2643e81..fee6531 100644
--- a/src/main/java/freemarker/cache/_CacheAPI.java
+++ b/src/main/java/freemarker/cache/_CacheAPI.java
@@ -26,10 +26,10 @@ import freemarker.template.MalformedTemplateNameException;
  * This class is to work around the lack of module system in Java, i.e., so that other FreeMarker packages can
  * access things inside this package that users shouldn't. 
  */ 
-public class _CacheAPI {
+public final class _CacheAPI {
 
-    public _CacheAPI() {
-        // TODO Auto-generated constructor stub
+    private _CacheAPI() {
+        // Not meant to be instantiated
     }
     
     public static String toAbsoluteName(TemplateNameFormat templateNameFormat, String baseName, String targetName)
@@ -37,4 +37,9 @@ public class _CacheAPI {
         return templateNameFormat.toAbsoluteName(baseName, targetName);
     }
 
+    public static String normalizeAbsoluteName(TemplateNameFormat templateNameFormat, String name)
+            throws MalformedTemplateNameException {
+        return templateNameFormat.normalizeAbsoluteName(name);
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/6bdb716e/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 5594ba0..7be1e47 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -1724,6 +1724,16 @@ public class Configurable {
      *       <br>String value is something like:
      *       <br>{@code /include/common.ftl, "/include/evil name.ftl"}
      *       
+     *   <li><p>{@code "lazy_auto_imports"}:
+     *       See {@link Configuration#setLazyAutoImports(Boolean)}.
+     *       <br>String value: {@code "true"}, {@code "false"} (also the equivalents: {@code "yes"}, {@code "no"},
+     *       {@code "t"}, {@code "f"}, {@code "y"}, {@code "n"}), case insensitive. Also can be {@code "null"}.
+
+     *   <li><p>{@code "lazy_imports"}:
+     *       See {@link Configuration#setLazyImports(boolean)}.
+     *       <br>String value: {@code "true"}, {@code "false"} (also the equivalents: {@code "yes"}, {@code "no"},
+     *       {@code "t"}, {@code "f"}, {@code "y"}, {@code "n"}), case insensitive.
+     *       
      *   <li><p>{@code "default_encoding"}:
      *       See {@link Configuration#setDefaultEncoding(String)}.
      *       <br>As the default value is the system default, which can change

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/6bdb716e/src/main/java/freemarker/core/Environment.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 3d61aaa..20e910c 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -162,7 +162,7 @@ public final class Environment extends Configurable {
     private LocalContextStack localContextStack;
     private final Namespace mainNamespace;
     private Namespace currentNamespace, globalNamespace;
-    private HashMap loadedLibs;
+    private HashMap<String, Namespace> loadedLibs;
     private Configurable legacyParent;
 
     private boolean inAttemptBlock;
@@ -2170,7 +2170,7 @@ public final class Environment extends Configurable {
     public Namespace getNamespace(String name) {
         if (name.startsWith("/")) name = name.substring(1);
         if (loadedLibs != null) {
-            return (Namespace) loadedLibs.get(name);
+            return loadedLibs.get(name);
         } else {
             return null;
         }
@@ -2463,22 +2463,25 @@ public final class Environment extends Configurable {
      */
     public Template getTemplateForInclusion(String name, String encoding, boolean parseAsFTL, boolean ignoreMissing)
             throws IOException {
-        final Template inheritedTemplate = getTemplate();
+        return configuration.getTemplate(
+                name, getLocale(), getIncludedTemplateCustomLookupCondition(),
+                encoding != null ? encoding : getIncludedTemplateEncoding(),
+                parseAsFTL,
+                ignoreMissing);
+    }
+
+    private Object getIncludedTemplateCustomLookupCondition() {
+        return getTemplate().getCustomLookupCondition();
+    }
 
+    private String getIncludedTemplateEncoding() {
+        String encoding;
+        // This branch shouldn't exist, as it doesn't make much sense to inherit encoding. But we have to keep BC.
+        encoding = getTemplate().getEncoding();
         if (encoding == null) {
-            // This branch shouldn't exist, as it doesn't make much sense to inherit encoding. But we have to keep BC.
-            encoding = inheritedTemplate.getEncoding();
-            if (encoding == null) {
-                encoding = configuration.getEncoding(this.getLocale());
-            }
+            encoding = configuration.getEncoding(this.getLocale());
         }
-
-        Object customLookupCondition = inheritedTemplate.getCustomLookupCondition();
-
-        return configuration.getTemplate(
-                name, getLocale(), customLookupCondition,
-                encoding, parseAsFTL,
-                ignoreMissing);
+        return encoding;
     }
 
     /**
@@ -2513,22 +2516,63 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * Emulates <code>import</code> directive, except that <code>name</code> must be tempate root relative.
+     * Emulates <code>import</code> directive, except that <code>templateName</code> must be template root relative.
      *
      * <p>
-     * It's the same as <code>importLib(getTemplateForImporting(name), namespace)</code>. But, you may want to
+     * It's the same as <code>importLib(getTemplateForImporting(templateName), namespace)</code>. But, you may want to
      * separately call these two methods, so you can determine the source of exceptions more precisely, and thus achieve
      * more intelligent error handling.
+     * 
+     * <p>
+     * If it will be a lazy or an eager import is decided by the value of {@link Configuration#getLazyImports()}. You
+     * can also directly control that aspect by using {@link #importLib(String, String, boolean)} instead.
+     *
+     * @return Not {@code null}. This is possibly a lazily self-initializing namespace, which means that it will only
+     *         try to get and process the imported template when you access its content.
+     *
+     * @see #getTemplateForImporting(String templateName)
+     * @see #importLib(Template includedTemplate, String namespaceVarName)
+     * @see #importLib(String, String, boolean)
+     */
+    public Namespace importLib(String templateName, String targetNsVarName)
+            throws IOException, TemplateException {
+        return importLib(templateName, targetNsVarName, configuration.getLazyImports());
+    }
+
+    /**
+     * Does what the <code>#import</code> directive does, but with an already loaded template.
      *
+     * @param loadedTemplate
+     *            The template to import. Note that it does <em>not</em> need to be a template returned by
+     *            {@link #getTemplateForImporting(String name)}.
+     * @param targetNsVarName
+     *            The name of the FTL variable that will store the namespace.
+     *            
      * @see #getTemplateForImporting(String name)
-     * @see #importLib(Template includedTemplate, String namespace)
+     * @see #importLib(Template includedTemplate, String namespaceVarName)
      */
-    public Namespace importLib(String name, String namespace)
+    public Namespace importLib(Template loadedTemplate, String targetNsVarName)
             throws IOException, TemplateException {
-        return importLib(getTemplateForImporting(name), namespace);
+        return importLib(null, loadedTemplate, targetNsVarName);
     }
 
     /**
+     * Like {@link #importLib(String, String)}, but you can specify if you want a
+     * {@linkplain Configuration#setLazyImports(boolean) lazy import} or not.
+     * 
+     * @return Not {@code null}. This is possibly a lazily self-initializing namespace, which mean that it will only try
+     *         to get and process the imported template when you access its content.
+     * 
+     * @since 2.3.25
+     */
+    public Namespace importLib(String templateName, String targetNsVarName, boolean lazy)
+            throws IOException, TemplateException {
+        return lazy
+                ? importLib(templateName, null, targetNsVarName)
+                : importLib(null, getTemplateForImporting(templateName), targetNsVarName);
+    }
+    
+    /**
      * Gets a template for importing; used with {@link #importLib(Template importedTemplate, String namespace)}. The
      * advantage over simply using <code>config.getTemplate(...)</code> is that it chooses the encoding as the
      * <code>import</code> directive does.
@@ -2544,53 +2588,83 @@ public final class Environment extends Configurable {
     }
 
     /**
-     * Emulates <code>import</code> directive.
-     *
+     * @param templateName
+     *            Ignored if {@code loadedTemaplate} is set (so we do eager import), otherwise it can't be {@code null}.
+     *            Assumed to be template root directory relative (not relative to the current template).
      * @param loadedTemplate
-     *            the template to import. Note that it does <em>not</em> need to be a template returned by
-     *            {@link #getTemplateForImporting(String name)}.
+     *            {@code null} exactly if we want a lazy import
      */
-    public Namespace importLib(Template loadedTemplate, String namespace)
+    private Namespace importLib(
+            String templateName, final Template loadedTemplate, final String targetNsVarName)
             throws IOException, TemplateException {
+        final boolean lazyImport;
+        if (loadedTemplate != null) {
+            lazyImport = false;
+            // As we have an already normalized name, we use it. 2.3.x note: We should use the template.sourceName as
+            // namespace key, but historically we use the looked up name (template.name); check what lazy import does if
+            // that will be fixed, as that can't do the template lookup, yet the keys must be the same.
+            templateName = loadedTemplate.getName();
+        } else {
+            lazyImport = true;
+            // We can't cause a template lookup here (see TemplateLookupStrategy), as that can be expensive. We exploit
+            // that (at least in 2.3.x) the name used for eager import namespace key isn't the template.sourceName, but
+            // the looked up name (template.name), which we can get quickly:
+            TemplateNameFormat tnf = getConfiguration().getTemplateNameFormat();
+            templateName = _CacheAPI.normalizeAbsoluteName(tnf, _CacheAPI.toAbsoluteName(tnf, null, templateName));
+        }
+        
         if (loadedLibs == null) {
             loadedLibs = new HashMap();
         }
-        String templateName = loadedTemplate.getName();
-        Namespace existingNamespace = (Namespace) loadedLibs.get(templateName);
+        final Namespace existingNamespace = loadedLibs.get(templateName);
         if (existingNamespace != null) {
-            if (namespace != null) {
-                setVariable(namespace, existingNamespace);
+            if (targetNsVarName != null) {
+                setVariable(targetNsVarName, existingNamespace);
                 if (isIcI2324OrLater() && currentNamespace == mainNamespace) {
-                    globalNamespace.put(namespace, existingNamespace);
+                    globalNamespace.put(targetNsVarName, existingNamespace);
                 }
             }
+            if (!lazyImport && existingNamespace instanceof LazilyInitializedNamespace) {
+                ((LazilyInitializedNamespace) existingNamespace).ensureInitializedTME();
+            }
         } else {
-            Namespace newNamespace = new Namespace(loadedTemplate);
-            if (namespace != null) {
-                setVariable(namespace, newNamespace);
+            final Namespace newNamespace
+                    = lazyImport ? new LazilyInitializedNamespace(templateName) : new Namespace(loadedTemplate);
+            loadedLibs.put(templateName, newNamespace);
+            
+            if (targetNsVarName != null) {
+                setVariable(targetNsVarName, newNamespace);
                 if (currentNamespace == mainNamespace) {
-                    globalNamespace.put(namespace, newNamespace);
+                    globalNamespace.put(targetNsVarName, newNamespace);
                 }
             }
-            Namespace prevNamespace = this.currentNamespace;
-            this.currentNamespace = newNamespace;
-            loadedLibs.put(templateName, currentNamespace);
-            Writer prevOut = out;
-            this.out = NullWriter.INSTANCE;
-            try {
-                include(loadedTemplate);
-            } finally {
-                this.out = prevOut;
-                this.currentNamespace = prevNamespace;
+            
+            if (!lazyImport) {
+                initializeImportLibNamespace(newNamespace, loadedTemplate);
             }
         }
-        return (Namespace) loadedLibs.get(templateName);
+        return loadedLibs.get(templateName);
+    }
+
+    private void initializeImportLibNamespace(final Namespace newNamespace, Template loadedTemplate)
+            throws TemplateException, IOException {
+        Namespace prevNamespace = this.currentNamespace;
+        this.currentNamespace = newNamespace;
+        Writer prevOut = out;
+        this.out = NullWriter.INSTANCE;
+        try {
+            include(loadedTemplate);
+        } finally {
+            this.out = prevOut;
+            this.currentNamespace = prevNamespace;
+        }
     }
 
     /**
      * Resolves a reference to a template (like the one used in {@code #include} or {@code #import}), assuming a base
      * name. This gives a full (that is, absolute), even if non-normalized template name, that could be used for
-     * {@link Configuration#getTemplate(String)}. This is mostly used when a template refers to another template.
+     * {@link Configuration#getTemplate(String)}. This is mostly used when a 
+     * template refers to another template.
      * 
      * @param baseName
      *            The name to which relative {@code targetName}-s are relative to. Maybe {@code null}, which usually
@@ -2738,7 +2812,7 @@ public final class Environment extends Configurable {
 
     public class Namespace extends SimpleHash {
 
-        private final Template template;
+        private Template template;
 
         Namespace() {
             this.template = Environment.this.getTemplate();
@@ -2754,6 +2828,171 @@ public final class Environment extends Configurable {
         public Template getTemplate() {
             return template == null ? Environment.this.getTemplate() : template;
         }
+        
+        void setTemplate(Template template) {
+            this.template = template; 
+        }
+        
+    }
+    
+    private enum InitializationStatus {
+        UNINITIALIZED, INITIALIZING, INITIALIZED, FAILED
+    }
+    
+    class LazilyInitializedNamespace extends Namespace {
+        
+        private final String templateName;
+        private final Locale locale;
+        private final String encoding;
+        private final Object customLookupCondition;
+        
+        private InitializationStatus status = InitializationStatus.UNINITIALIZED;
+        
+        /**
+         * @param templateName
+         *            Must be root relative
+         */
+        private LazilyInitializedNamespace(String templateName) {
+            super(null);
+            
+            this.templateName = templateName;
+            // Make snapshot of all settings that influence template resolution:
+            this.locale = getLocale();
+            this.encoding = getIncludedTemplateEncoding();
+            this.customLookupCondition = getIncludedTemplateCustomLookupCondition();
+        }
+
+        private void ensureInitializedTME() throws TemplateModelException {
+            if (status != InitializationStatus.INITIALIZED && status != InitializationStatus.INITIALIZING) {
+                if (status == InitializationStatus.FAILED) {
+                    throw new TemplateModelException(
+                            "Lazy initialization of the imported namespace for "
+                            + StringUtil.jQuote(templateName)
+                            + " has already failed earlier; won't retry it.");
+                }
+                try {
+                    status = InitializationStatus.INITIALIZING;
+                    initialize();
+                    status = InitializationStatus.INITIALIZED;
+                } catch (Exception e) {
+                    // [FM3] Rethrow TemplateException-s as is
+                    throw new TemplateModelException(
+                            "Lazy initialization of the imported namespace for "
+                            + StringUtil.jQuote(templateName)
+                            + " has failed; see cause exception", e);
+                } finally {
+                    if (status != InitializationStatus.INITIALIZED) {
+                        status = InitializationStatus.FAILED;
+                    }
+                }
+            }
+        }
+        
+        private void ensureInitializedRTE() {
+            try {
+                ensureInitializedTME();
+            } catch (TemplateModelException e) {
+                throw new RuntimeException(e.getMessage(), e.getCause());
+            }
+        }
+
+        private void initialize() throws IOException, TemplateException {
+            setTemplate(configuration.getTemplate(
+                    templateName, locale, customLookupCondition, encoding,
+                    true, false));
+            Locale lastLocale = getLocale();
+            try {
+                setLocale(locale);
+                initializeImportLibNamespace(this, getTemplate());
+            } finally {
+                setLocale(lastLocale);
+            }
+        }
+
+        @Override
+        protected Map copyMap(Map map) {
+            ensureInitializedRTE();
+            return super.copyMap(map);
+        }
+
+        @Override
+        public Template getTemplate() {
+            ensureInitializedRTE();
+            return super.getTemplate();
+        }
+
+        @Override
+        public void put(String key, Object value) {
+            ensureInitializedRTE();
+            super.put(key, value);
+        }
+
+        @Override
+        public void put(String key, boolean b) {
+            ensureInitializedRTE();
+            super.put(key, b);
+        }
+
+        @Override
+        public TemplateModel get(String key) throws TemplateModelException {
+            ensureInitializedTME();
+            return super.get(key);
+        }
+
+        @Override
+        public boolean containsKey(String key) {
+            ensureInitializedRTE();
+            return super.containsKey(key);
+        }
+
+        @Override
+        public void remove(String key) {
+            ensureInitializedRTE();
+            super.remove(key);
+        }
+
+        @Override
+        public void putAll(Map m) {
+            ensureInitializedRTE();
+            super.putAll(m);
+        }
+
+        @Override
+        public Map toMap() throws TemplateModelException {
+            ensureInitializedTME();
+            return super.toMap();
+        }
+
+        @Override
+        public String toString() {
+            ensureInitializedRTE();
+            return super.toString();
+        }
+
+        @Override
+        public int size() {
+            ensureInitializedRTE();
+            return super.size();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            ensureInitializedRTE();
+            return super.isEmpty();
+        }
+
+        @Override
+        public TemplateCollectionModel keys() {
+            ensureInitializedRTE();
+            return super.keys();
+        }
+
+        @Override
+        public TemplateCollectionModel values() {
+            ensureInitializedRTE();
+            return super.values();
+        }
+        
     }
 
     private static final Writer EMPTY_BODY_WRITER = new Writer() {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/6bdb716e/src/main/java/freemarker/core/LibraryLoad.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/LibraryLoad.java b/src/main/java/freemarker/core/LibraryLoad.java
index 9305144..8d71b6a 100644
--- a/src/main/java/freemarker/core/LibraryLoad.java
+++ b/src/main/java/freemarker/core/LibraryLoad.java
@@ -35,17 +35,17 @@ import freemarker.template.TemplateException;
 public final class LibraryLoad extends TemplateElement {
 
     private Expression importedTemplateNameExp;
-    private String namespace;
+    private String targetNsVarName;
 
     /**
      * @param template the template that this <tt>Include</tt> is a part of.
      * @param templateName the name of the template to be included.
-     * @param namespace the namespace to assign this library to
+     * @param targetNsVarName the name of the  variable to assign this library's namespace to
      */
     LibraryLoad(Template template,
             Expression templateName,
-            String namespace) {
-        this.namespace = namespace;
+            String targetNsVarName) {
+        this.targetNsVarName = targetNsVarName;
         this.importedTemplateNameExp = templateName;
     }
 
@@ -61,16 +61,14 @@ public final class LibraryLoad extends TemplateElement {
                     e.getMalformednessDescription());
         }
         
-        final Template importedTemplate;
         try {
-            importedTemplate = env.getTemplateForImporting(fullImportedTemplateName);
+            env.importLib(fullImportedTemplateName, targetNsVarName);
         } catch (IOException e) {
             throw new _MiscTemplateException(e, env,
                     "Template importing failed (for parameter value ",
                     new _DelayedJQuote(importedTemplateName),
                     "):\n", new _DelayedGetMessage(e));
         }
-        env.importLib(importedTemplate, namespace);
         return null;
     }
 
@@ -82,7 +80,7 @@ public final class LibraryLoad extends TemplateElement {
         buf.append(' ');
         buf.append(importedTemplateNameExp.getCanonicalForm());
         buf.append(" as ");
-        buf.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(namespace));
+        buf.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(targetNsVarName));
         if (canonical) buf.append("/>");
         return buf.toString();
     }
@@ -101,7 +99,7 @@ public final class LibraryLoad extends TemplateElement {
     Object getParameterValue(int idx) {
         switch (idx) {
         case 0: return importedTemplateNameExp;
-        case 1: return namespace;
+        case 1: return targetNsVarName;
         default: throw new IndexOutOfBoundsException();
         }
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/6bdb716e/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 82c46b3..e756c3d 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -235,6 +235,20 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     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;
+
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+    public static final String LAZY_IMPORTS_KEY_SNAKE_CASE = "lazy_imports";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+    public static final String LAZY_IMPORTS_KEY_CAMEL_CASE = "lazyImports";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String LAZY_IMPORTS_KEY = LAZY_IMPORTS_KEY_SNAKE_CASE;
+
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+    public static final String LAZY_AUTO_IMPORTS_KEY_SNAKE_CASE = "lazy_auto_imports";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+    public static final String LAZY_AUTO_IMPORTS_KEY_CAMEL_CASE = "lazyAutoImports";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String LAZY_AUTO_IMPORTS_KEY = LAZY_AUTO_IMPORTS_KEY_SNAKE_CASE;
     
     /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
     public static final String TAG_SYNTAX_KEY_SNAKE_CASE = "tag_syntax";
@@ -300,6 +314,8 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         CACHE_STORAGE_KEY_SNAKE_CASE,
         DEFAULT_ENCODING_KEY_SNAKE_CASE,
         INCOMPATIBLE_IMPROVEMENTS_KEY_SNAKE_CASE,
+        LAZY_AUTO_IMPORTS_KEY_SNAKE_CASE,
+        LAZY_IMPORTS_KEY_SNAKE_CASE,
         LOCALIZED_LOOKUP_KEY_SNAKE_CASE,
         NAMING_CONVENTION_KEY_SNAKE_CASE,
         OUTPUT_FORMAT_KEY_SNAKE_CASE,
@@ -323,6 +339,8 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         CACHE_STORAGE_KEY_CAMEL_CASE,
         DEFAULT_ENCODING_KEY_CAMEL_CASE,
         INCOMPATIBLE_IMPROVEMENTS_KEY_CAMEL_CASE,
+        LAZY_AUTO_IMPORTS_KEY_CAMEL_CASE,
+        LAZY_IMPORTS_KEY_CAMEL_CASE,
         LOCALIZED_LOOKUP_KEY_CAMEL_CASE,
         NAMING_CONVENTION_KEY_CAMEL_CASE,
         OUTPUT_FORMAT_KEY_CAMEL_CASE,
@@ -504,6 +522,8 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     
     private LinkedHashMap<String, String> autoImports = new LinkedHashMap<String, String>(0);
     private ArrayList<String> autoIncludes = new ArrayList<String>(0);
+    private boolean lazyImpots;
+    private Boolean lazyAutoImport;
 
     /**
      * @deprecated Use {@link #Configuration(Version)} instead. Note that the version can be still modified later with
@@ -2876,6 +2896,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 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)) {
+                setLazyImports(StringUtil.getYesNo(value));
             } else if (TAG_SYNTAX_KEY_SNAKE_CASE.equals(name) || TAG_SYNTAX_KEY_CAMEL_CASE.equals(name)) {
                 if ("auto_detect".equals(value) || "autoDetect".equals(value)) {
                     setTagSyntax(AUTO_DETECT_TAG_SYNTAX);
@@ -2992,6 +3016,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * 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
@@ -3022,8 +3050,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     /**
      * 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, thus, it's not the best idea to use a {@link HashMap} (although the order of imports doesn't
-     * mater for properly designed libraries).
+     * 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
@@ -3048,8 +3075,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     protected void doAutoImportsAndIncludes(Environment env)
     throws TemplateException, IOException {
         for (Map.Entry<String, String> autoImport : autoImports.entrySet()) {
-            // Template name 1st, namespace var 2nd.
-            env.importLib(autoImport.getValue(), autoImport.getKey());
+            Boolean autoImportsLazy = getLazyAutoImports();
+            env.importLib(
+                    autoImport.getValue(), autoImport.getKey(),
+                    autoImportsLazy != null ? autoImportsLazy.booleanValue() : getLazyImports());
         }
         for (int i = 0; i < autoIncludes.size(); i++) {
             String templateName = autoIncludes.get(i);
@@ -3103,6 +3132,62 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
             autoIncludes.remove(templateName);
         }
     }
+    
+    /**
+     * The getter pair of {@link #setLazyImports(boolean)}.
+     * 
+     * @since 2.3.25
+     */
+    public boolean getLazyImports() {
+        return lazyImpots;
+    }
+
+    /**
+     * Specifies if {@code <#import ...>} (and {@link Environment#importLib(String, String)}) should delay the loading
+     * and processing of the imported templates until the content of the imported namespace is actually accessed. This
+     * makes the overhead of <em>unused</em> imports negligible. A drawback is that importing a missing or otherwise
+     * broken template will be successful, and the problem will remain hidden until (and if) the namespace content is
+     * actually used. Also, you lose the strict control over when the namespace initializing code in the imported
+     * template will be executed, though it shouldn't mater for well written imported templates anyway. Note that the
+     * namespace initializing code will run with the same {@linkplain Configurable#getLocale() locale} as it was at the
+     * point of the {@code <#import ...>} call (other settings won't be handled specially like that).
+     * 
+     * <p>
+     * The default is {@code false} (and thus imports are eager) for backward compatibility, which can cause
+     * perceivable overhead if you have many imports and only a few of them is used.
+     * 
+     * <p>
+     * This setting also affects {@linkplain #setAutoImports(Map) auto-imports}, unless you have set a non-{@code null}
+     * value just for that via {@link #setLazyAutoImports(Boolean)}.
+     * 
+     * @see #setLazyAutoImports(Boolean)
+     * 
+     * @since 2.3.25
+     */
+    public void setLazyImports(boolean importsLazy) {
+        this.lazyImpots = importsLazy;
+    }
+
+    /**
+     * The getter pair of {@link #setLazyAutoImports(Boolean)}.
+     * 
+     * @since 2.3.25
+     */
+    public Boolean getLazyAutoImports() {
+        return lazyAutoImport;
+    }
+
+    /**
+     * Specifies if {@linkplain #setAutoImports(Map) auto-imports} will be
+     * {@link #setLazyImports(boolean) lazy imports}. This is useful to make the overhead of <em>unused</em>
+     * auto-imports negligible. If this is set to {@code null}, {@link #getLazyImports()} specifies the behavior of
+     * auto-imports too. The default value is {@code null}.
+     * 
+     * @since 2.3.25
+     */
+    public void setLazyAutoImports(Boolean autoImportsLazy) {
+        this.lazyAutoImport = autoImportsLazy;
+    }
 
     /**
      * Returns FreeMarker version number string. 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/6bdb716e/src/manual/en_US/book.xml
----------------------------------------------------------------------
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 0ea368d..35ca1a5 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -26409,6 +26409,44 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
     <appendix xml:id="app_versions">
       <title>Version history</title>
 
+      <section xml:id="versions_2_3_25">
+        <title>2.3.25</title>
+
+        <section>
+          <title>Changes on the FTL side</title>
+
+          <itemizedlist>
+            <listitem>
+              <para>[TODO]</para>
+            </listitem>
+          </itemizedlist>
+        </section>
+
+        <section>
+          <title>Changes on the Java side</title>
+
+          <itemizedlist>
+            <listitem>
+              <para>Lazy imports: With the new boolean
+              <literal>Configuration</literal>-level settings,
+              <literal>lazy_imports</literal> and
+              <literal>lazy_auto_imports</literal>, you can request imports
+              (as in <literal>&lt;#import "lib/utils.ftl" as u&gt;</literal>)
+              and/or auto-imports to be lazy. When the import is lazy, the
+              namespace variable (<literal>u</literal> in this example) will
+              be created immediately, just like before, but the template will
+              be imported and processed only when (and if ever) the content of
+              the imported namespace is accessed. The main application of this
+              is with auto-imports, where you don't want the overhead of
+              importing templates that aren't actually used in a template.
+              (Also, a new <literal>Environment.importLib</literal> method
+              overload was added, where you can specify if you want a lazy or
+              an eager import.)</para>
+            </listitem>
+          </itemizedlist>
+        </section>
+      </section>
+
       <section xml:id="versions_2_3_24">
         <title>2.3.24 Release Candidate 1</title>
 
@@ -26434,8 +26472,8 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
           making process have stabilized in a manner consistent with other
           successful ASF projects. While incubation status is not necessarily
           a reflection of the completeness or stability of the code, it does
-          indicate that the project has yet to be fully endorsed by the ASF.
-          </para>
+          indicate that the project has yet to be fully endorsed by the
+          ASF.</para>
         </section>
 
         <section>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/6bdb716e/src/test/java/freemarker/core/IncludeAndImportTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/IncludeAndImportTest.java b/src/test/java/freemarker/core/IncludeAndImportTest.java
index 6123e9a..28161e5 100644
--- a/src/test/java/freemarker/core/IncludeAndImportTest.java
+++ b/src/test/java/freemarker/core/IncludeAndImportTest.java
@@ -18,16 +18,26 @@
  */
 package freemarker.core;
 
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
 import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 
 import org.junit.Before;
 import org.junit.Test;
 
 import freemarker.cache.StringTemplateLoader;
+import freemarker.core.Environment.LazilyInitializedNamespace;
+import freemarker.core.Environment.Namespace;
 import freemarker.template.Configuration;
 import freemarker.template.TemplateException;
+import freemarker.template.TemplateNotFoundException;
+import freemarker.template.WrappingTemplateModel;
 import freemarker.test.TemplateTest;
 
+@SuppressWarnings("boxing")
 public class IncludeAndImportTest extends TemplateTest {
 
     @Override
@@ -45,8 +55,19 @@ public class IncludeAndImportTest extends TemplateTest {
         addTemplate("lib1.ftl", "<#global lib1Cnt = (lib1Cnt!0) + 1><#global history = (history!) + 'L1'>"
                 + "<#macro m>In lib1</#macro>");
         addTemplate("lib2.ftl", "<#global history = (history!) + 'L2'>"
+                + "<#macro m>In lib2</#macro>");
+        addTemplate("lib3.ftl", "<#global history = (history!) + 'L3'>"
+                + "<#macro m>In lib3</#macro>");
+        
+        addTemplate("lib2CallsLib1.ftl", "<#global history = (history!) + 'L2'>"
                 + "<#macro m>In lib2 (<@lib1.m/>)</#macro>");
-        addTemplate("lib3.ftl", "<#import 'lib1.ftl' as lib1>");
+        addTemplate("lib3ImportsLib1.ftl", "<#import 'lib1.ftl' as lib1><#global history = (history!) + 'L3'>"
+                + "<#macro m>In lib3 (<@lib1.m/>)</#macro>");
+        
+        addTemplate("lib_de.ftl", "<#global history = (history!) + 'LDe'><#assign initLocale=.locale>"
+                + "<#macro m>de</#macro>");
+        addTemplate("lib_en.ftl", "<#global history = (history!) + 'LEn'><#assign initLocale=.locale>"
+                + "<#macro m>en</#macro>");
     }
 
     @Test
@@ -74,7 +95,7 @@ public class IncludeAndImportTest extends TemplateTest {
     public void importInMainCreatesGlobalBugfix() throws IOException, TemplateException {
         // An import in the main namespace should create a global variable, but there's a bug where that doesn't happen
         // if the imported library was already initialized elsewhere.
-        String ftl = "<#import 'lib3.ftl' as lib3>${lib1Cnt} ${.main.lib1???c} ${.globals.lib1???c}, "
+        String ftl = "<#import 'lib3ImportsLib1.ftl' as lib3>${lib1Cnt} ${.main.lib1???c} ${.globals.lib1???c}, "
         + "<#import 'lib1.ftl' as lib1>${lib1Cnt} ${.main.lib1???c} ${.globals.lib1???c}";
         assertOutput(ftl, "1 false false, 1 true false");
         // Activate bugfix:
@@ -90,10 +111,156 @@ public class IncludeAndImportTest extends TemplateTest {
         getConfiguration().addAutoInclude("inc1.ftl");
         getConfiguration().addAutoInclude("inc2.ftl");
         getConfiguration().addAutoImport("lib1", "lib1.ftl");
-        getConfiguration().addAutoImport("lib2", "lib2.ftl");
+        getConfiguration().addAutoImport("lib2", "lib2CallsLib1.ftl");
         assertOutput(
                 "<#include 'inc3.ftl'>[main] ${inc1Cnt}, ${history}, <@lib1.m/>, <@lib2.m/>",
                 "[inc1][inc2][inc3][main] 1, L1L2I, In lib1, In lib2 (In lib1)");
     }
     
+    /**
+     * Demonstrates design issue in FreeMarker 2.3.x where the lookupStrategy is not factored in when identifying
+     * already existing namespaces.
+     */
+    @Test
+    public void lookupSrategiesAreNotConsideredProperly() throws IOException, TemplateException {
+        // As only the name of the template is used for the finding the already existing namespace, the settings that
+        // influence the lookup are erroneously ignored.
+        assertOutput(
+                "<#setting locale='en_US'><#import 'lib.ftl' as ns1>"
+                + "<#setting locale='de_DE'><#import 'lib.ftl' as ns2>"
+                + "<@ns1.m/> <@ns2.m/> ${history}",
+                "en en LEn");
+        
+        // The opposite of the prevous, where differn names refer to the same template after a lookup: 
+        assertOutput(
+                "<#setting locale='en_US'>"
+                + "<#import '*/lib.ftl' as ns1>"
+                + "<#import 'lib.ftl' as ns2>"
+                + "<@ns1.m/> <@ns2.m/> ${history}",
+                "en en LEnLEn");
+    }
+    
+    @Test
+    public void lazyImportBasics() throws IOException, TemplateException {
+        String ftlImports = "<#import 'lib1.ftl' as l1><#import 'lib2.ftl' as l2><#import 'lib3ImportsLib1.ftl' as l3>";
+        String ftlCalls = "<@l2.m/>, <@l1.m/>; ${history}";
+        String ftl = ftlImports + ftlCalls;
+        
+        assertOutput(ftl, "In lib2, In lib1; L1L2L3");
+        
+        getConfiguration().setLazyImports(true);
+        assertOutput(ftl, "In lib2, In lib1; L2L1");
+        
+        assertOutput(ftlImports + "<@l3.m/>, " + ftlCalls, "In lib3 (In lib1), In lib2, In lib1; L3L1L2");
+    }
+
+    @Test
+    public void lazyImportAndLocale() throws IOException, TemplateException {
+        getConfiguration().setLazyImports(true);
+        assertOutput("<#setting locale = 'de_DE'><#import 'lib.ftl' as lib>"
+                + "[${history!}] "
+                + "<#setting locale = 'en'>"
+                + "<@lib.m/> ${lib.initLocale} [${history}]",
+                "[] de de_DE [LDe]");
+    }
+
+    @Test
+    public void lazyAutoImportSettings() throws IOException, TemplateException {
+        Configuration cfg = getConfiguration();
+        cfg.addAutoImport("l1", "lib1.ftl");
+        cfg.addAutoImport("l2", "lib2.ftl");
+        cfg.addAutoImport("l3", "lib3.ftl");
+        
+        String ftl = "<@l2.m/>, <@l1.m/>; ${history}";
+        String expectedEagerOutput = "In lib2, In lib1; L1L2L3";
+        String expecedLazyOutput = "In lib2, In lib1; L2L1";
+        
+        assertOutput(ftl, expectedEagerOutput);
+        cfg.setLazyImports(true);
+        assertOutput(ftl, expecedLazyOutput);
+        cfg.setLazyImports(false);
+        assertOutput(ftl, expectedEagerOutput);
+        cfg.setLazyAutoImports(true);
+        assertOutput(ftl, expecedLazyOutput);
+        cfg.setLazyAutoImports(null);
+        assertOutput(ftl, expectedEagerOutput);
+        cfg.setLazyImports(true);
+        cfg.setLazyAutoImports(false);
+        assertOutput(ftl, expectedEagerOutput);
+    }
+    
+    @Test
+    public void lazyAutoImportMixedWithManualImport() throws IOException, TemplateException {
+        Configuration cfg = getConfiguration();
+        cfg.addAutoImport("l1", "lib1.ftl");
+        cfg.addAutoImport("l2", "/./lib2.ftl");
+        cfg.addAutoImport("l3", "lib3.ftl");
+        cfg.setLazyAutoImports(true);
+
+        String ftl = "<@l2.m/>, <@l1.m/>; ${history}";
+        String expectOutputWithoutHistory = "In lib2, In lib1; ";
+        String expecedOutput = expectOutputWithoutHistory + "L2L1";
+        
+        assertOutput(ftl, expecedOutput);
+        assertOutput("<#import 'lib1.ftl' as l1>" + ftl, expectOutputWithoutHistory + "L1L2");
+        assertOutput("<#import './x/../lib1.ftl' as l1>" + ftl, expectOutputWithoutHistory + "L1L2");
+        assertOutput("<#import 'lib2.ftl' as l2>" + ftl, expecedOutput);
+        assertOutput("<#import 'lib3.ftl' as l3>" + ftl, expectOutputWithoutHistory + "L3L2L1");
+        cfg.setLazyImports(true);
+        assertOutput("<#import 'lib1.ftl' as l1>" + ftl, expecedOutput);
+        assertOutput("<#import './x/../lib1.ftl' as l1>" + ftl, expecedOutput);
+        assertOutput("<#import 'lib2.ftl' as l2>" + ftl, expecedOutput);
+        assertOutput("<#import 'lib3.ftl' as l3>" + ftl, expecedOutput);
+    }
+
+    @Test
+    public void lazyImportErrors() throws IOException, TemplateException {
+        Configuration cfg = getConfiguration();
+        cfg.setLazyImports(true);
+        
+        assertOutput("<#import 'noSuchTemplate.ftl' as wrong>x", "x");
+        
+        cfg.addAutoImport("wrong", "noSuchTemplate.ftl");
+        assertOutput("x", "x");
+
+        try {
+            assertOutput("${wrong.x}", "");
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(),
+                    allOf(containsString("Lazy initialization"), containsString("noSuchTemplate.ftl")));
+            assertThat(e.getCause(), instanceOf(TemplateNotFoundException.class));
+        }
+        
+        addTemplate("containsError.ftl", "${noSuchVar}");
+        try {
+            assertOutput("<#import 'containsError.ftl' as lib>${lib.x}", "");
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(),
+                    allOf(containsString("Lazy initialization"), containsString("containsError.ftl")));
+            assertThat(e.getCause(), instanceOf(InvalidReferenceException.class));
+            assertThat(e.getCause().getMessage(), containsString("noSuchVar"));
+        }
+    }
+    
+    /**
+     * Ensures that all methods are overridden so that they will do the lazy initialization.
+     */
+    @Test
+    public void lazilyInitializingNamespaceOverridesAll() throws SecurityException, NoSuchMethodException {
+        for (Method m : Namespace.class.getMethods()) {
+            Class<?> declClass = m.getDeclaringClass();
+            if (declClass == Object.class || declClass == WrappingTemplateModel.class
+                    || (m.getModifiers() & Modifier.STATIC) != 0
+                    || m.getName().equals("synchronizedWrapper")) {
+                continue;
+            }
+            Method lazyM = LazilyInitializedNamespace.class.getMethod(m.getName(), m.getParameterTypes());
+            if (lazyM.getDeclaringClass() != LazilyInitializedNamespace.class) {
+                fail("The " + lazyM + " method wasn't overidden in " + LazilyInitializedNamespace.class.getName());
+            }
+        }
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/6bdb716e/src/test/java/freemarker/template/ConfigurationTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/template/ConfigurationTest.java b/src/test/java/freemarker/template/ConfigurationTest.java
index 39ee7b6..28b5d9a 100644
--- a/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/src/test/java/freemarker/template/ConfigurationTest.java
@@ -1529,6 +1529,28 @@ public class ConfigurationTest extends TestCase {
         cfg.setSetting("naming_convention", "auto_detect");
         assertEquals(Configuration.AUTO_DETECT_NAMING_CONVENTION, cfg.getNamingConvention());
     }
+
+    public void testLazyImportsSetSetting() throws TemplateException {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
+
+        assertFalse(cfg.getLazyImports());
+        cfg.setSetting("lazy_imports", "true");
+        assertTrue(cfg.getLazyImports());
+        cfg.setSetting("lazyImports", "false");
+        assertFalse(cfg.getLazyImports());
+    }
+    
+    public void testLazyAutoImportsSetSetting() throws TemplateException {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
+
+        assertNull(cfg.getLazyAutoImports());
+        cfg.setSetting("lazy_auto_imports", "true");
+        assertEquals(Boolean.TRUE, cfg.getLazyAutoImports());
+        cfg.setSetting("lazyAutoImports", "false");
+        assertEquals(Boolean.FALSE, cfg.getLazyAutoImports());
+        cfg.setSetting("lazyAutoImports", "null");
+        assertNull(cfg.getLazyAutoImports());
+    }
     
     @Test
     public void testGetSettingNamesAreSorted() throws Exception {


Mime
View raw message