freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [54/54] incubator-freemarker git commit: Added early draft of TemplateResolver, renamed TemplateCache to DefaultTemplateResolver. TemplateResolver is not yet directly used in Configuration. This was only added in a hurry, so that it's visible why the o.a
Date Thu, 16 Feb 2017 23:09:19 GMT
Added early draft of TemplateResolver, renamed TemplateCache to DefaultTemplateResolver. TemplateResolver is not yet directly used in Configuration. This was only added in a hurry, so that it's visible why the o.a.f.core.templateresolver subpackage name makes sense.


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

Branch: refs/heads/3
Commit: 94d39312b09629e81beffb0d9b2029e5829a6d10
Parents: ecb4e23
Author: ddekany <ddekany@apache.org>
Authored: Thu Feb 16 23:56:05 2017 +0100
Committer: ddekany <ddekany@apache.org>
Committed: Fri Feb 17 00:07:40 2017 +0100

----------------------------------------------------------------------
 .../apache/freemarker/core/Configuration.java   |  168 +--
 .../org/apache/freemarker/core/Template.java    |    4 +-
 .../apache/freemarker/core/ast/Environment.java |    7 +-
 .../core/ast/TemplateConfiguration.java         |    4 +-
 .../core/templateresolver/CacheStorage.java     |    2 +-
 .../DefaultTemplateResolver.java                | 1028 ++++++++++++++++
 .../templateresolver/GetTemplateResult.java     |   88 ++
 .../core/templateresolver/MruCacheStorage.java  |    4 +-
 .../core/templateresolver/TemplateCache.java    | 1111 ------------------
 .../core/templateresolver/TemplateLoader.java   |    6 +-
 .../templateresolver/TemplateLoaderSession.java |    4 +-
 .../templateresolver/TemplateLoadingResult.java |    4 +-
 .../templateresolver/TemplateNameFormat.java    |   32 +-
 .../core/templateresolver/TemplateResolver.java |  165 +++
 src/manual/en_US/FM3-CHANGE-LOG.txt             |    5 +-
 .../freemarker/core/ConfigurationTest.java      |    4 +-
 ...plateConfigurationWithTemplateCacheTest.java |  326 -----
 ...teConfigurationWithTemplateResolverTest.java |  326 +++++
 .../DefaultTemplateResolverTest.java            |  479 ++++++++
 .../templateresolver/TemplateCacheTest.java     |  488 --------
 20 files changed, 2196 insertions(+), 2059 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/Configuration.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/Configuration.java b/src/main/java/org/apache/freemarker/core/Configuration.java
index 84cbed2..7612820 100644
--- a/src/main/java/org/apache/freemarker/core/Configuration.java
+++ b/src/main/java/org/apache/freemarker/core/Configuration.java
@@ -88,13 +88,19 @@ import org.apache.freemarker.core.model.impl.beans.BeansWrapper;
 import org.apache.freemarker.core.model.impl.beans.BeansWrapperBuilder;
 import org.apache.freemarker.core.templateresolver.CacheStorage;
 import org.apache.freemarker.core.templateresolver.ClassTemplateLoader;
+import org.apache.freemarker.core.templateresolver.DefaultTemplateResolver;
 import org.apache.freemarker.core.templateresolver.FileTemplateLoader;
+import org.apache.freemarker.core.templateresolver.GetTemplateResult;
 import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
 import org.apache.freemarker.core.templateresolver.MruCacheStorage;
 import org.apache.freemarker.core.templateresolver.MultiTemplateLoader;
 import org.apache.freemarker.core.templateresolver.SoftCacheStorage;
-import org.apache.freemarker.core.templateresolver.TemplateCache;
-import org.apache.freemarker.core.templateresolver.TemplateCache.MaybeMissingTemplate;
+import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLookupContext;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.URLTemplateLoader;
 import org.apache.freemarker.core.util.CaptureOutput;
 import org.apache.freemarker.core.util.ClassUtil;
 import org.apache.freemarker.core.util.Constants;
@@ -105,12 +111,6 @@ import org.apache.freemarker.core.util.SecurityUtilities;
 import org.apache.freemarker.core.util.StandardCompress;
 import org.apache.freemarker.core.util.StringUtil;
 import org.apache.freemarker.core.util.XmlEscape;
-import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory;
-import org.apache.freemarker.core.templateresolver.TemplateLoader;
-import org.apache.freemarker.core.templateresolver.TemplateLookupContext;
-import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
-import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
-import org.apache.freemarker.core.templateresolver.URLTemplateLoader;
 
 /**
  * <b>The main entry point into the FreeMarker API</b>; encapsulates the configuration settings of FreeMarker,
@@ -120,8 +120,8 @@ import org.apache.freemarker.core.templateresolver.URLTemplateLoader;
  * the application life-cycle, set its {@link #setSetting(String, String) configuration settings} there (either with the
  * setter methods like {@link #setTemplateLoader(TemplateLoader)} or by loading a {@code .properties} file), and then
  * use that single instance everywhere in your application. Frequently re-creating {@link Configuration} is a typical
- * and grave mistake from performance standpoint, as the {@link Configuration} holds the template cache, and often also
- * the class introspection cache, which then will be lost. (Note that, naturally, having multiple long-lived instances,
+ * and grave mistake from performance standpoint, as the {@link Configuration} holds the template templateResolver, and often also
+ * the class introspection templateResolver, which then will be lost. (Note that, naturally, having multiple long-lived instances,
  * like one per component that internally uses FreeMarker is fine.)  
  * 
  * <p>The basic usage pattern is like:
@@ -508,7 +508,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     private int namingConvention = AUTO_DETECT_NAMING_CONVENTION;
     private int tabSize = 8;  // Default from JavaCC 3.x 
 
-    private TemplateCache cache;
+    private DefaultTemplateResolver templateResolver;
     
     private boolean templateLoaderExplicitlySet;
     private boolean templateLookupStrategyExplicitlySet;
@@ -601,8 +601,8 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      *         won't remain hidden now. As the old default is a singleton too, potentially shared by independently
      *         developed components, most of them expects the out-of-the-box behavior from it (and the others are
      *         necessarily buggy). Also, then concurrency glitches can occur (and even pollute the class introspection
-     *         cache) because the singleton is modified after publishing to other threads.)
-     *         Furthermore the new default object wrapper shares class introspection cache with other
+     *         templateResolver) because the singleton is modified after publishing to other threads.)
+     *         Furthermore the new default object wrapper shares class introspection templateResolver with other
      *         {@link BeansWrapper}-s created with {@link BeansWrapperBuilder}, which has an impact as
      *         {@link BeansWrapper#clearClassIntrospecitonCache()} will be disallowed; see more about it there.
      *       </li>
@@ -842,7 +842,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         NullArgumentException.check("incompatibleImprovements", incompatibleImprovements);
         this.incompatibleImprovements = incompatibleImprovements;
         
-        createTemplateCache();
+        createTemplateResolver();
         loadBuiltInSharedVariables();
     }
 
@@ -854,33 +854,33 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         }
     }
     
-    private void createTemplateCache() {
-        cache = new TemplateCache(
+    private void createTemplateResolver() {
+        templateResolver = new DefaultTemplateResolver(
                 null,
                 getDefaultCacheStorage(),
                 getDefaultTemplateLookupStrategy(),
                 getDefaultTemplateNameFormat(),
                 null,
                 this);
-        cache.clear(); // for fully BC behavior
-        cache.setDelay(5000);
+        templateResolver.clearTemplateCache(); // for fully BC behavior
+        templateResolver.setTemplateUpdateDelayMilliseconds(5000);
     }
     
-    private void recreateTemplateCacheWith(
+    private void recreateTemplateResolverWith(
             TemplateLoader loader, CacheStorage storage,
             TemplateLookupStrategy templateLookupStrategy, TemplateNameFormat templateNameFormat,
             TemplateConfigurationFactory templateConfigurations) {
-        TemplateCache oldCache = cache;
-        cache = new TemplateCache(
+        DefaultTemplateResolver oldCache = templateResolver;
+        templateResolver = new DefaultTemplateResolver(
                 loader, storage, templateLookupStrategy, templateNameFormat, templateConfigurations, this);
-        cache.clear(false);
-        cache.setDelay(oldCache.getDelay());
-        cache.setLocalizedLookup(localizedLookup);
+        templateResolver.clearTemplateCache(false);
+        templateResolver.setTemplateUpdateDelayMilliseconds(oldCache.getTemplateUpdateDelayMilliseconds());
+        templateResolver.setLocalizedLookup(localizedLookup);
     }
     
-    private void recreateTemplateCache() {
-        recreateTemplateCacheWith(cache.getTemplateLoader(), cache.getCacheStorage(),
-                cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(),
+    private void recreateTemplateResolver() {
+        recreateTemplateResolverWith(templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(),
+                templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(),
                 getTemplateConfigurations());
     }
     
@@ -947,10 +947,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
             Configuration copy = (Configuration) super.clone();
             copy.sharedVariables = new HashMap(sharedVariables);
             copy.localeToCharsetMap = new ConcurrentHashMap(localeToCharsetMap);
-            copy.recreateTemplateCacheWith(
-                    cache.getTemplateLoader(), cache.getCacheStorage(),
-                    cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(),
-                    cache.getTemplateConfigurations());
+            copy.recreateTemplateResolverWith(
+                    templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(),
+                    templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(),
+                    templateResolver.getTemplateConfigurations());
             return copy;
         } catch (CloneNotSupportedException e) {
             throw new BugException("Cloning failed", e);
@@ -1117,7 +1117,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     
     /**
      * Sets a {@link TemplateLoader} that is used to look up and load templates;
-     * as a side effect the template cache will be emptied.
+     * as a side effect the template templateResolver will be emptied.
      * By providing your own {@link TemplateLoader} implementation, you can load templates from whatever kind of
      * storages, like from relational databases, NoSQL-storages, etc.
      * 
@@ -1137,10 +1137,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     public void setTemplateLoader(TemplateLoader templateLoader) {
         // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
         synchronized (this) {
-            if (cache.getTemplateLoader() != templateLoader) {
-                recreateTemplateCacheWith(templateLoader, cache.getCacheStorage(),
-                        cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(),
-                        cache.getTemplateConfigurations());
+            if (templateResolver.getTemplateLoader() != templateLoader) {
+                recreateTemplateResolverWith(templateLoader, templateResolver.getCacheStorage(),
+                        templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(),
+                        templateResolver.getTemplateConfigurations());
             }
             templateLoaderExplicitlySet = true;
         }
@@ -1173,23 +1173,23 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * The getter pair of {@link #setTemplateLoader(TemplateLoader)}.
      */
     public TemplateLoader getTemplateLoader() {
-        if (cache == null) {
+        if (templateResolver == null) {
             return null;
         }
-        return cache.getTemplateLoader();
+        return templateResolver.getTemplateLoader();
     }
     
     /**
      * Sets a {@link TemplateLookupStrategy} that is used to look up templates based on the requested name; as a side
-     * effect the template cache will be emptied. The default value is {@link TemplateLookupStrategy#DEFAULT_2_3_0}.
+     * effect the template templateResolver will be emptied. The default value is {@link TemplateLookupStrategy#DEFAULT_2_3_0}.
      * 
      * @since 2.3.22
      */
     public void setTemplateLookupStrategy(TemplateLookupStrategy templateLookupStrategy) {
-        if (cache.getTemplateLookupStrategy() != templateLookupStrategy) {
-            recreateTemplateCacheWith(cache.getTemplateLoader(), cache.getCacheStorage(),
-                    templateLookupStrategy, cache.getTemplateNameFormat(),
-                    cache.getTemplateConfigurations());
+        if (templateResolver.getTemplateLookupStrategy() != templateLookupStrategy) {
+            recreateTemplateResolverWith(templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(),
+                    templateLookupStrategy, templateResolver.getTemplateNameFormat(),
+                    templateResolver.getTemplateConfigurations());
         }
         templateLookupStrategyExplicitlySet = true;
     }
@@ -1222,10 +1222,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * The getter pair of {@link #setTemplateLookupStrategy(TemplateLookupStrategy)}.
      */
     public TemplateLookupStrategy getTemplateLookupStrategy() {
-        if (cache == null) {
+        if (templateResolver == null) {
             return null;
         }
-        return cache.getTemplateLookupStrategy();
+        return templateResolver.getTemplateLookupStrategy();
     }
     
     /**
@@ -1235,10 +1235,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * @since 2.3.22
      */
     public void setTemplateNameFormat(TemplateNameFormat templateNameFormat) {
-        if (cache.getTemplateNameFormat() != templateNameFormat) {
-            recreateTemplateCacheWith(cache.getTemplateLoader(), cache.getCacheStorage(),
-                    cache.getTemplateLookupStrategy(), templateNameFormat,
-                    cache.getTemplateConfigurations());
+        if (templateResolver.getTemplateNameFormat() != templateNameFormat) {
+            recreateTemplateResolverWith(templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(),
+                    templateResolver.getTemplateLookupStrategy(), templateNameFormat,
+                    templateResolver.getTemplateConfigurations());
         }
         templateNameFormatExplicitlySet = true;
     }
@@ -1270,10 +1270,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * The getter pair of {@link #setTemplateNameFormat(TemplateNameFormat)}.
      */
     public TemplateNameFormat getTemplateNameFormat() {
-        if (cache == null) {
+        if (templateResolver == null) {
             return null;
         }
-        return cache.getTemplateNameFormat();
+        return templateResolver.getTemplateNameFormat();
     }
     
     /**
@@ -1291,12 +1291,12 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * @since 2.3.24
      */
     public void setTemplateConfigurations(TemplateConfigurationFactory templateConfigurations) {
-        if (cache.getTemplateConfigurations() != templateConfigurations) {
+        if (templateResolver.getTemplateConfigurations() != templateConfigurations) {
             if (templateConfigurations != null) {
                 templateConfigurations.setConfiguration(this);
             }
-            recreateTemplateCacheWith(cache.getTemplateLoader(), cache.getCacheStorage(),
-                    cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(),
+            recreateTemplateResolverWith(templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(),
+                    templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(),
                     templateConfigurations);
         }
     }
@@ -1305,31 +1305,31 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * The getter pair of {@link #setTemplateConfigurations(TemplateConfigurationFactory)}.
      */
     public TemplateConfigurationFactory getTemplateConfigurations() {
-        if (cache == null) {
+        if (templateResolver == null) {
             return null;
         }
-        return cache.getTemplateConfigurations();
+        return templateResolver.getTemplateConfigurations();
     }
 
     /**
      * Sets the {@link CacheStorage} used for caching {@link Template}-s;
-     * the earlier content of the template cache will be dropt.
+     * the earlier content of the template templateResolver will be dropt.
      * 
      * The default is a {@link SoftCacheStorage}. If the total size of the {@link Template}
      * objects is significant but most templates are used rarely, using a
      * {@link MruCacheStorage} instead might be advisable. If you don't want caching at
      * all, use {@link org.apache.freemarker.core.templateresolver.NullCacheStorage} (you can't use {@code null}).
      * 
-     * <p>Note that setting the cache storage will re-create the template cache, so
+     * <p>Note that setting the templateResolver storage will re-create the template templateResolver, so
      * all its content will be lost.
      */
     public void setCacheStorage(CacheStorage cacheStorage) {
         // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
         synchronized (this) {
             if (getCacheStorage() != cacheStorage) {
-                recreateTemplateCacheWith(cache.getTemplateLoader(), cacheStorage,
-                        cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(),
-                        cache.getTemplateConfigurations());
+                recreateTemplateResolverWith(templateResolver.getTemplateLoader(), cacheStorage,
+                        templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(),
+                        templateResolver.getTemplateConfigurations());
             }
             cacheStorageExplicitlySet = true;
         }
@@ -1366,10 +1366,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     public CacheStorage getCacheStorage() {
         // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration
         synchronized (this) {
-            if (cache == null) {
+            if (templateResolver == null) {
                 return null;
             }
-            return cache.getCacheStorage();
+            return templateResolver.getCacheStorage();
         }
     }
 
@@ -1487,7 +1487,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      */
     @Deprecated
     public void setTemplateUpdateDelay(int seconds) {
-        cache.setDelay(1000L * seconds);
+        templateResolver.setTemplateUpdateDelayMilliseconds(1000L * seconds);
     }
 
     /**
@@ -1496,16 +1496,16 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * 
      * <p>
      * When you get a template via {@link #getTemplate(String)} (or some of its overloads). FreeMarker will try to get
-     * the template from the template cache. If the template is found, and at least this amount of time was elapsed
+     * the template from the template templateResolver. If the template is found, and at least this amount of time was elapsed
      * since the template last modification date was checked, FreeMarker will re-check the last modification date (this
-     * could mean I/O), possibly reloading the template and updating the cache as a consequence (can mean even more
+     * could mean I/O), possibly reloading the template and updating the templateResolver as a consequence (can mean even more
      * I/O). The {@link #getTemplate(String)} (or some of its overloads) call will only return after this all is
      * done, so it will return the fresh template.
      * 
      * @since 2.3.23
      */
     public void setTemplateUpdateDelayMilliseconds(long millis) {
-        cache.setDelay(millis);
+        templateResolver.setTemplateUpdateDelayMilliseconds(millis);
     }
     
     /**
@@ -1514,7 +1514,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * @since 2.3.23
      */
     public long getTemplateUpdateDelayMilliseconds() {
-        return cache.getDelay();
+        return templateResolver.getTemplateUpdateDelayMilliseconds();
     }
     
     @Override
@@ -1624,7 +1624,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * was never set in this {@link Configuration} object through the public API, its value will be set to the default
      * value appropriate for the new {@code incompatibleImprovements}. (This adjustment of a setting value doesn't
      * count as setting that setting, so setting {@code incompatibleImprovements} for multiple times also works as
-     * expected.) Note that if the {@code template_loader} have to be changed because of this, the template cache will
+     * expected.) Note that if the {@code template_loader} have to be changed because of this, the template templateResolver will
      * be emptied.
      * 
      * @throws IllegalArgumentException
@@ -1674,7 +1674,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 unsetObjectWrapper();
             }
             
-            recreateTemplateCache();
+            recreateTemplateResolver();
         }
     }
 
@@ -2228,7 +2228,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     }
     
     /**
-     * Retrieves the template with the given name from the template cache, loading it into the cache first if it's
+     * Retrieves the template with the given name from the template templateResolver, loading it into the templateResolver first if it's
      * missing/staled.
      * 
      * <p>
@@ -2292,7 +2292,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     
     /**
      * Retrieves the template with the given name (and according the specified further parameters) from the template
-     * cache, loading it into the cache first if it's missing/staled.
+     * templateResolver, loading it into the templateResolver first if it's missing/staled.
      * 
      * <p>
      * This method is thread-safe.
@@ -2303,7 +2303,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * @param name
      *            The name or path of the template, which is not a real path, but interpreted inside the current
      *            {@link TemplateLoader}. Can't be {@code null}. The exact syntax of the name depends on the underlying
-     *            {@link TemplateLoader}, but the cache makes some assumptions. First, the name is expected to be a
+     *            {@link TemplateLoader}, but the templateResolver makes some assumptions. First, the name is expected to be a
      *            hierarchical path, with path components separated by a slash character (not with backslash!). The path
      *            (the name) given here must <em>not</em> begin with slash; it's always interpreted relative to the
      *            "template root directory". Then, the {@code ..} and {@code .} path meta-elements will be resolved. For
@@ -2314,7 +2314,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      *            contain at most one path element whose name is {@code *} (asterisk). This path meta-element triggers
      *            the <i>acquisition mechanism</i>. If the template is not found in the location described by the
      *            concatenation of the path left to the asterisk (called base path) and the part to the right of the
-     *            asterisk (called resource path), the cache will attempt to remove the rightmost path component from
+     *            asterisk (called resource path), the templateResolver will attempt to remove the rightmost path component from
      *            the base path ("go up one directory") and concatenate that with the resource path. The process is
      *            repeated until either a template is found, or the base path is completely exhausted.
      *
@@ -2337,7 +2337,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * @param customLookupCondition
      *            This value can be used by a custom {@link TemplateLookupStrategy}; has no effect with the default one.
      *            Can be {@code null} (though it's up to the custom {@link TemplateLookupStrategy} if it allows that).
-     *            This object will be used as part of the cache key, so it must to have a proper
+     *            This object will be used as part of the templateResolver key, so it must to have a proper
      *            {@link Object#equals(Object)} and {@link Object#hashCode()} method. It also should have reasonable
      *            {@link Object#toString()}, as it's possibly quoted in error messages. The expected type is up to the
      *            custom {@link TemplateLookupStrategy}. See also:
@@ -2391,7 +2391,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
             encoding = getEncoding(locale);
         }
         
-        final MaybeMissingTemplate maybeTemp = cache.getTemplate(name, locale, customLookupCondition, encoding, parseAsFTL);
+        final GetTemplateResult maybeTemp = templateResolver.getTemplate(name, locale, customLookupCondition, encoding, parseAsFTL);
         final Template temp = maybeTemp.getTemplate();
         if (temp == null) {
             if (ignoreMissing) {
@@ -2685,13 +2685,13 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     }
     
     /**
-     * Removes all entries from the template cache, thus forcing reloading of templates
+     * Removes all entries from the template templateResolver, thus forcing reloading of templates
      * on subsequent <code>getTemplate</code> calls.
      * 
      * <p>This method is thread-safe and can be called while the engine processes templates.
      */
     public void clearTemplateCache() {
-        cache.clear();
+        templateResolver.clearTemplateCache();
     }
     
     /**
@@ -2728,9 +2728,9 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     }
     
     /**
-     * Removes a template from the template cache, hence forcing the re-loading
+     * Removes a template from the template templateResolver, hence forcing the re-loading
      * of it when it's next time requested. This is to give the application
-     * finer control over cache updating than {@link #setTemplateUpdateDelay(int)}
+     * finer control over templateResolver updating than {@link #setTemplateUpdateDelay(int)}
      * alone does.
      * 
      * <p>For the meaning of the parameters, see
@@ -2743,7 +2743,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     public void removeTemplateFromCache(
             String name, Locale locale, String encoding, boolean parse)
     throws IOException {
-        cache.removeTemplate(name, locale, encoding, parse);
+        templateResolver.removeTemplateFromCache(name, locale, encoding, parse);
     }    
     
     /**
@@ -2752,7 +2752,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * <p>This method is thread-safe and can be called while the engine works.
      */
     public boolean getLocalizedLookup() {
-        return cache.getLocalizedLookup();
+        return templateResolver.getLocalizedLookup();
     }
     
     /**
@@ -2767,7 +2767,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      * template names, use {@link #setTemplateLookupStrategy(TemplateLookupStrategy)} with your custom
      * {@link TemplateLookupStrategy}.
      * 
-     * <p>Note that changing the value of this setting causes the template cache to be emptied so that old lookup
+     * <p>Note that changing the value of this setting causes the template templateResolver to be emptied so that old lookup
      * results won't be reused (since 2.3.22). 
      * 
      * <p>
@@ -2777,7 +2777,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      */
     public void setLocalizedLookup(boolean localizedLookup) {
         this.localizedLookup = localizedLookup;
-        cache.setLocalizedLookup(localizedLookup);
+        templateResolver.setLocalizedLookup(localizedLookup);
     }
     
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/Template.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/Template.java b/src/main/java/org/apache/freemarker/core/Template.java
index 9f1ded1..c78e66c 100644
--- a/src/main/java/org/apache/freemarker/core/Template.java
+++ b/src/main/java/org/apache/freemarker/core/Template.java
@@ -57,7 +57,7 @@ import org.apache.freemarker.core.model.TemplateHashModel;
 import org.apache.freemarker.core.model.TemplateModel;
 import org.apache.freemarker.core.model.TemplateNodeModel;
 import org.apache.freemarker.core.model.impl.SimpleHash;
-import org.apache.freemarker.core.templateresolver.TemplateCache;
+import org.apache.freemarker.core.templateresolver.DefaultTemplateResolver;
 import org.apache.freemarker.core.templateresolver.TemplateLoader;
 import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
 import org.apache.freemarker.core.util.NullArgumentException;
@@ -652,7 +652,7 @@ public class Template extends Configurable {
      * Mostly only used internally; setter pair of {@link #getCustomLookupCondition()}. This meant to be called directly
      * after instantiating the template with its constructor, after a successfull lookup that used this condition. So
      * this should only be called from code that deals with creating new {@code Template} objects, like from
-     * {@link TemplateCache}.
+     * {@link DefaultTemplateResolver}.
      * 
      * @since 2.3.22
      */

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/ast/Environment.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/Environment.java b/src/main/java/org/apache/freemarker/core/ast/Environment.java
index 34f2c3c..cb7dcfc 100644
--- a/src/main/java/org/apache/freemarker/core/ast/Environment.java
+++ b/src/main/java/org/apache/freemarker/core/ast/Environment.java
@@ -67,12 +67,13 @@ import org.apache.freemarker.core.model.impl.SimpleHash;
 import org.apache.freemarker.core.model.impl.SimpleSequence;
 import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
 import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.TemplateResolver;
 import org.apache.freemarker.core.templateresolver._CacheAPI;
 import org.apache.freemarker.core.util.DateUtil;
+import org.apache.freemarker.core.util.DateUtil.DateToISO8601CalendarFactory;
 import org.apache.freemarker.core.util.NullWriter;
 import org.apache.freemarker.core.util.StringUtil;
 import org.apache.freemarker.core.util.UndeclaredThrowableException;
-import org.apache.freemarker.core.util.DateUtil.DateToISO8601CalendarFactory;
 import org.slf4j.Logger;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -2449,7 +2450,7 @@ public final class Environment extends Configurable {
      * @param name
      *            the name of the template, relatively to the template root directory (not the to the directory of the
      *            currently executing template file). (Note that you can use
-     *            {@link org.apache.freemarker.core.templateresolver.TemplateCache#getFullTemplatePath} to convert paths to template root relative
+     *            {@link TemplateResolver#toRootBasedName(String, String)} to convert paths to template root based
      *            paths.) For more details see the identical parameter of
      *            {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)}
      * 
@@ -2592,7 +2593,7 @@ public final class Environment extends Configurable {
      * @param name
      *            the name of the template, relatively to the template root directory (not the to the directory of the
      *            currently executing template file!). (Note that you can use
-     *            {@link org.apache.freemarker.core.templateresolver.TemplateCache#getFullTemplatePath} to convert paths to template root relative
+     *            {@link TemplateResolver#toRootBasedName(String, String)} to convert paths to template root based
      *            paths.)
      */
     public Template getTemplateForImporting(String name) throws IOException {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java b/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java
index 55f125d..f4bb357 100644
--- a/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java
+++ b/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java
@@ -28,14 +28,14 @@ import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.Template;
 import org.apache.freemarker.core.Version;
 import org.apache.freemarker.core._TemplateAPI;
-import org.apache.freemarker.core.templateresolver.TemplateCache;
+import org.apache.freemarker.core.templateresolver.DefaultTemplateResolver;
 import org.apache.freemarker.core.util.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.
+ * and {@link DefaultTemplateResolver}), 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 (

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java b/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java
index 5ff11c2..1dfe01a 100644
--- a/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java
+++ b/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java
@@ -24,7 +24,7 @@ package org.apache.freemarker.core.templateresolver;
  * an object with a key, retrieval and removal via the key. It is actually a
  * small subset of the {@link java.util.Map} interface. 
  * The implementations can be coded in a non-threadsafe manner as the natural
- * user of the cache storage, {@link TemplateCache} does the necessary
+ * user of the cache storage, {@link DefaultTemplateResolver} does the necessary
  * synchronization.
  *
  * @see org.apache.freemarker.core.Configuration#setCacheStorage(CacheStorage)

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolver.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolver.java b/src/main/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolver.java
new file mode 100644
index 0000000..e6b9ea1
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolver.java
@@ -0,0 +1,1028 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.templateresolver;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core._TemplateAPI;
+import org.apache.freemarker.core.ast.BugException;
+import org.apache.freemarker.core.ast.MarkReleaserTemplateSpecifiedEncodingHandler;
+import org.apache.freemarker.core.ast.TemplateConfiguration;
+import org.apache.freemarker.core.ast.TemplateSpecifiedEncodingHandler;
+import org.apache.freemarker.core.util.NullArgumentException;
+import org.apache.freemarker.core.util.StringUtil;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+import org.slf4j.Logger;
+
+/**
+ * Performs caching and on-demand loading of the templates.
+ * The actual template "file" loading is delegated to a {@link TemplateLoader} that you can specify in the constructor.
+ * Some aspects of caching is delegated to a {@link CacheStorage} that you can also specify in the constructor.
+ * 
+ * <p>Typically you don't instantiate or otherwise use this class directly. The {@link Configuration} embeds an
+ * instance of this class, that you access indirectly through {@link Configuration#getTemplate(String)} and other
+ * {@link Configuration} API-s. Then {@link TemplateLoader} and {@link CacheStorage} can be set with
+ * {@link Configuration#setTemplateLoader(TemplateLoader)} and
+ * {@link Configuration#setCacheStorage(CacheStorage)}.
+ */
+public class DefaultTemplateResolver extends TemplateResolver {
+    
+    /**
+     * The default template update delay; see {@link Configuration#setTemplateUpdateDelayMilliseconds(long)}.
+     * 
+     * @since 2.3.23
+     */
+    public static final long DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS = 5000L;
+    
+    private static final String ASTERISKSTR = "*";
+    private static final char ASTERISK = '*';
+    private static final char SLASH = '/';
+    private static final String LOCALE_PART_SEPARATOR = "_";
+    private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER;
+
+    /** Maybe {@code null}. */
+    private final TemplateLoader templateLoader;
+    
+    /** Here we keep our cached templates */
+    private final CacheStorage cacheStorage;
+    private final TemplateLookupStrategy templateLookupStrategy;
+    private final TemplateNameFormat templateNameFormat;
+    private final TemplateConfigurationFactory templateConfigurations;
+    
+    private final boolean isCacheStorageConcurrent;
+    /** {@link Configuration#setTemplateUpdateDelayMilliseconds(long)} */
+    private long templateUpdateDelayMilliseconds = DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS;
+    /** {@link Configuration#setLocalizedLookup(boolean)} */
+    private boolean localizedLookup = true;
+
+    private Configuration config;
+
+    /**
+     * Same as {@link #DefaultTemplateResolver(TemplateLoader, CacheStorage, Configuration)} with a new {@link SoftCacheStorage}
+     * as the 2nd parameter.
+     * 
+     * @since 2.3.21
+     */
+    public DefaultTemplateResolver(TemplateLoader templateLoader, Configuration config) {
+        this(templateLoader, _TemplateAPI.createDefaultCacheStorage(Configuration.VERSION_2_3_0), config);
+    }
+    
+    /**
+     * Same as
+     * {@link #DefaultTemplateResolver(TemplateLoader, CacheStorage, TemplateLookupStrategy, TemplateNameFormat, Configuration)}
+     * with {@link TemplateLookupStrategy#DEFAULT_2_3_0} and {@link TemplateNameFormat#DEFAULT_2_3_0}.
+     * 
+     * @since 2.3.21
+     */
+    public DefaultTemplateResolver(TemplateLoader templateLoader, CacheStorage cacheStorage, Configuration config) {
+        this(templateLoader, cacheStorage,
+                _TemplateAPI.getDefaultTemplateLookupStrategy(Configuration.VERSION_2_3_0),
+                _TemplateAPI.getDefaultTemplateNameFormat(Configuration.VERSION_2_3_0),
+                config);
+    }
+    
+    /**
+     * Same as
+     * {@link #DefaultTemplateResolver(TemplateLoader, CacheStorage, TemplateLookupStrategy, TemplateNameFormat,
+     * TemplateConfigurationFactory, Configuration)} with {@code null} for {@code templateConfigurations}-s.
+     * 
+     * @since 2.3.22
+     */
+    public DefaultTemplateResolver(TemplateLoader templateLoader, CacheStorage cacheStorage,
+            TemplateLookupStrategy templateLookupStrategy, TemplateNameFormat templateNameFormat,
+            Configuration config) {
+        this(templateLoader, cacheStorage, templateLookupStrategy, templateNameFormat, null, config);
+    }
+
+    /**
+     * @param templateLoader
+     *            The {@link TemplateLoader} to use. Can be {@code null}, though then every request will result in
+     *            {@link TemplateNotFoundException}.
+     * @param cacheStorage
+     *            The {@link CacheStorage} to use. Can't be {@code null}.
+     * @param templateLookupStrategy
+     *            The {@link TemplateLookupStrategy} to use. Can't be {@code null}.
+     * @param templateNameFormat
+     *            The {@link TemplateNameFormat} to use. Can't be {@code null}.
+     * @param templateConfigurations
+     *            The {@link TemplateConfigurationFactory} to use. Can be {@code null} (then all templates will use the
+     *            settings coming from the {@link Configuration} as is, except in the very rare case where a
+     *            {@link TemplateLoader} itself specifies a {@link TemplateConfiguration}).
+     * @param config
+     *            The {@link Configuration} this cache will be used for. Can't be {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    public DefaultTemplateResolver(TemplateLoader templateLoader, CacheStorage cacheStorage,
+            TemplateLookupStrategy templateLookupStrategy, TemplateNameFormat templateNameFormat,
+            TemplateConfigurationFactory templateConfigurations,
+            Configuration config) {
+        super(config);
+        
+        this.templateLoader = templateLoader;
+        
+        NullArgumentException.check("cacheStorage", cacheStorage);
+        this.cacheStorage = cacheStorage;
+        isCacheStorageConcurrent = cacheStorage instanceof ConcurrentCacheStorage &&
+                ((ConcurrentCacheStorage) cacheStorage).isConcurrent();
+        
+        NullArgumentException.check("templateLookupStrategy", templateLookupStrategy);
+        this.templateLookupStrategy = templateLookupStrategy;
+
+        NullArgumentException.check("templateNameFormat", templateNameFormat);
+        this.templateNameFormat = templateNameFormat;
+
+        // Can be null
+        this.templateConfigurations = templateConfigurations;
+        
+        NullArgumentException.check("config", config);
+        this.config = config;
+    }
+    
+    /**
+     * Returns the configuration for internal usage.
+     */
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    public TemplateLoader getTemplateLoader() {
+        return templateLoader;
+    }
+
+    public CacheStorage getCacheStorage() {
+        return cacheStorage;
+    }
+    
+    /**
+     * @since 2.3.22
+     */
+    public TemplateLookupStrategy getTemplateLookupStrategy() {
+        return templateLookupStrategy;
+    }
+    
+    /**
+     * @since 2.3.22
+     */
+    public TemplateNameFormat getTemplateNameFormat() {
+        return templateNameFormat;
+    }
+    
+    /**
+     * @since 2.3.24
+     */
+    public TemplateConfigurationFactory getTemplateConfigurations() {
+        return templateConfigurations;
+    }
+
+    /**
+     * Retrieves the template with the given name (and according the specified further parameters) from the template
+     * cache, loading it into the cache first if it's missing/staled.
+     * 
+     * <p>
+     * All parameters must be non-{@code null}, except {@code customLookupCondition}. For the meaning of the parameters
+     * see {@link Configuration#getTemplate(String, Locale, String, boolean)}.
+     *
+     * @return A {@link GetTemplateResult} object that contains the {@link Template}, or a
+     *         {@link GetTemplateResult} object that contains {@code null} as the {@link Template} and information
+     *         about the missing template. The return value itself is never {@code null}. Note that exceptions occurring
+     *         during template loading will not be classified as a missing template, so they will cause an exception to
+     *         be thrown by this method instead of returning a {@link GetTemplateResult}. The idea is that having a
+     *         missing template is normal (not exceptional), providing that the backing storage mechanism could indeed
+     *         check that it's missing.
+     * 
+     * @throws MalformedTemplateNameException
+     *             If the {@code name} was malformed according the current {@link TemplateNameFormat}. However, if the
+     *             {@link TemplateNameFormat} is {@link TemplateNameFormat#DEFAULT_2_3_0} and
+     *             {@link Configuration#getIncompatibleImprovements()} is less than 2.4.0, then instead of throwing this
+     *             exception, a {@link GetTemplateResult} will be returned, similarly as if the template were missing
+     *             (the {@link GetTemplateResult#getMissingTemplateReason()} will describe the real error).
+     * 
+     * @throws IOException
+     *             If reading the template has failed from a reason other than the template is missing. This method
+     *             should never be a {@link TemplateNotFoundException}, as that condition is indicated in the return
+     *             value.
+     * 
+     * @since 2.3.22
+     */
+    @Override
+    public GetTemplateResult getTemplate(String name, Locale locale, Object customLookupCondition,
+            String encoding, boolean parseAsFTL)
+    throws IOException {
+        NullArgumentException.check("name", name);
+        NullArgumentException.check("locale", locale);
+        NullArgumentException.check("encoding", encoding);
+        
+        try {
+            name = templateNameFormat.normalizeRootBasedName(name);
+        } catch (MalformedTemplateNameException e) {
+            // If we don't have to emulate backward compatible behavior, then just rethrow it: 
+            if (templateNameFormat != TemplateNameFormat.DEFAULT_2_3_0
+                    || config.getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_4_0) {
+                throw e;
+            }
+            return new GetTemplateResult(null, e);
+        }
+        
+        if (templateLoader == null) {
+            return new GetTemplateResult(name, "The TemplateLoader (and TemplateLoader2) was null.");
+        }
+        
+        Template template = getTemplateInternal(name, locale, customLookupCondition, encoding, parseAsFTL);
+        return template != null ? new GetTemplateResult(template) : new GetTemplateResult(name, (String) null);
+    }
+
+    private Template getTemplateInternal(
+            final String name, final Locale locale, final Object customLookupCondition,
+            final String encoding, final boolean parseAsFTL)
+    throws IOException {
+        final boolean debug = LOG.isDebugEnabled();
+        final String debugPrefix = debug
+                ? getDebugPrefix("getTemplate", name, locale, customLookupCondition, encoding, parseAsFTL)
+                : null;
+        final CachedResultKey cacheKey = new CachedResultKey(name, locale, customLookupCondition, encoding, parseAsFTL);
+        
+        CachedResult oldCachedResult;
+        if (isCacheStorageConcurrent) {
+            oldCachedResult = (CachedResult) cacheStorage.get(cacheKey);
+        } else {
+            synchronized (cacheStorage) {
+                oldCachedResult = (CachedResult) cacheStorage.get(cacheKey);
+            }
+        }
+        
+        final long now = System.currentTimeMillis();
+        
+        boolean rethrownCachedException = false;
+        boolean suppressFinallyException = false;
+        TemplateLookupResult newLookupResult = null;
+        CachedResult newCachedResult = null;
+        TemplateLoaderSession session = null;
+        try {
+            if (oldCachedResult != null) {
+                // If we're within the refresh delay, return the cached result
+                if (now - oldCachedResult.lastChecked < templateUpdateDelayMilliseconds) {
+                    if (debug) {
+                        LOG.debug(debugPrefix + "Cached copy not yet stale; using cached.");
+                    }
+                    Object t = oldCachedResult.templateOrException;
+                    // t can be null, indicating a cached negative lookup
+                    if (t instanceof Template || t == null) {
+                        return (Template) t;
+                    } else if (t instanceof RuntimeException) {
+                        rethrowCachedException((RuntimeException) t);
+                    } else if (t instanceof IOException) {
+                        rethrownCachedException = true;
+                        rethrowCachedException((IOException) t);
+                    }
+                    throw new BugException("Unhandled class for t: " + t.getClass().getName());
+                }
+                // The freshness of the cache result must be checked.
+                
+                // Clone, as the instance in the cache store must not be modified to ensure proper concurrent behavior.
+                newCachedResult = oldCachedResult.clone();
+                newCachedResult.lastChecked = now;
+
+                session = templateLoader.createSession();
+                if (debug && session != null) {
+                    LOG.debug(debugPrefix + "Session created.");
+                }
+                
+                // Find the template source, load it if it doesn't correspond to the cached result.
+                newLookupResult = lookupAndLoadTemplateIfChanged(
+                        name, locale, customLookupCondition, oldCachedResult.source, oldCachedResult.version, session);
+
+                // Template source was removed (TemplateLoader2ResultStatus.NOT_FOUND, or no TemplateLoader2Result)
+                if (!newLookupResult.isPositive()) { 
+                    if (debug) {
+                        LOG.debug(debugPrefix + "No source found.");
+                    } 
+                    setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null);
+                    return null;
+                }
+
+                final TemplateLoadingResult newTemplateLoaderResult = newLookupResult.getTemplateLoaderResult();
+                if (newTemplateLoaderResult.getStatus() == TemplateLoadingResultStatus.NOT_MODIFIED) {
+                    // Return the cached version.
+                    if (debug) {
+                        LOG.debug(debugPrefix + ": Using cached template "
+                                + "(source: " + newTemplateLoaderResult.getSource() + ")"
+                                + " as it hasn't been changed on the backing store.");
+                    }
+                    putIntoCache(cacheKey, newCachedResult);
+                    return (Template) newCachedResult.templateOrException;
+                } else {
+                    if (newTemplateLoaderResult.getStatus() != TemplateLoadingResultStatus.OPENED) {
+                        // TemplateLoader2ResultStatus.NOT_FOUND was already handler earlier
+                        throw new BugException("Unxpected status: " + newTemplateLoaderResult.getStatus());
+                    }
+                    if (debug) {
+                        StringBuilder debugMsg = new StringBuilder();
+                        debugMsg.append(debugPrefix)
+                                .append("Reloading template instead of using the cached result because ");
+                        if (newCachedResult.templateOrException instanceof Throwable) {
+                            debugMsg.append("it's a cached error (retrying).");
+                        } else {
+                            Object newSource = newTemplateLoaderResult.getSource();
+                            if (!nullSafeEquals(newSource, oldCachedResult.source)) {
+                                debugMsg.append("the source has been changed: ")
+                                        .append("cached.source=").append(StringUtil.jQuoteNoXSS(oldCachedResult.source))
+                                        .append(", current.source=").append(StringUtil.jQuoteNoXSS(newSource));
+                            } else {
+                                Serializable newVersion = newTemplateLoaderResult.getVersion();
+                                if (!nullSafeEquals(oldCachedResult.version, newVersion)) {
+                                    debugMsg.append("the version has been changed: ")
+                                            .append("cached.version=").append(oldCachedResult.version) 
+                                            .append(", current.version=").append(newVersion);
+                                } else {
+                                    debugMsg.append("??? (unknown reason)");
+                                }
+                            }
+                        }
+                        LOG.debug(debugMsg.toString());
+                    }
+                }
+            } else { // if there was no cached result
+                if (debug) {
+                    LOG.debug(debugPrefix + "No cached result was found; will try to load template.");
+                }
+                
+                newCachedResult = new CachedResult();
+                newCachedResult.lastChecked = now;
+            
+                session = templateLoader.createSession();
+                if (debug && session != null) {
+                    LOG.debug(debugPrefix + "Session created.");
+                } 
+                
+                newLookupResult = lookupAndLoadTemplateIfChanged(
+                        name, locale, customLookupCondition, null, null, session);
+                
+                if (!newLookupResult.isPositive()) {
+                    setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null);
+                    return null;
+                }
+            }
+            // We have newCachedResult and newLookupResult initialized at this point.
+
+            TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult();
+            newCachedResult.source = templateLoaderResult.getSource();
+            
+            // If we get here, then we need to (re)load the template
+            if (debug) {
+                LOG.debug(debugPrefix + "Reading template content (source: "
+                        + StringUtil.jQuoteNoXSS(newCachedResult.source) + ")");
+            }
+            
+            Template template = loadTemplate(
+                    templateLoaderResult,
+                    name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition,
+                    encoding, parseAsFTL);
+            if (session != null) {
+                session.close();
+                if (debug) {
+                    LOG.debug(debugPrefix + "Session closed.");
+                } 
+            }
+            newCachedResult.templateOrException = template;
+            newCachedResult.version = templateLoaderResult.getVersion();
+            putIntoCache(cacheKey, newCachedResult);
+            return template;
+        } catch (RuntimeException e) {
+            if (newCachedResult != null) {
+                setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e);
+            }
+            suppressFinallyException = true;
+            throw e;
+        } catch (IOException e) {
+            // Rethrown cached exceptions are wrapped into IOException-s, so we only need this condition here.
+            if (!rethrownCachedException) {
+                setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e);
+            }
+            suppressFinallyException = true;
+            throw e;
+        } finally {
+            try {
+                // Close streams first:
+                
+                if (newLookupResult != null && newLookupResult.isPositive()) {
+                    TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult();
+                    Reader reader = templateLoaderResult.getReader();
+                    if (reader != null) {
+                        try {
+                            reader.close();
+                        } catch (IOException e) { // [FM3] Exception e
+                            if (suppressFinallyException) {
+                                if (LOG.isWarnEnabled()) { 
+                                    LOG.warn("Failed to close template content Reader for: " + name, e);
+                                }
+                            } else {
+                                suppressFinallyException = true;
+                                throw e;
+                            }
+                        }
+                    } else if (templateLoaderResult.getInputStream() != null) {
+                        try {
+                            templateLoaderResult.getInputStream().close();
+                        } catch (IOException e) { // [FM3] Exception e
+                            if (suppressFinallyException) {
+                                if (LOG.isWarnEnabled()) { 
+                                    LOG.warn("Failed to close template content InputStream for: " + name, e);
+                                }
+                            } else {
+                                suppressFinallyException = true;
+                                throw e;
+                            }
+                        }
+                    }
+                }
+            } finally {
+                // Then close streams:
+                
+                if (session != null && !session.isClosed()) {
+                    try {
+                        session.close();
+                        if (debug) {
+                            LOG.debug(debugPrefix + "Session closed.");
+                        } 
+                    } catch (IOException e) { // [FM3] Exception e
+                        if (suppressFinallyException) {
+                            if (LOG.isWarnEnabled()) { 
+                                LOG.warn("Failed to close template loader session for" + name, e);
+                            }
+                        } else {
+                            suppressFinallyException = true;
+                            throw e;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    
+    
+    private static final Method INIT_CAUSE = getInitCauseMethod();
+    
+    private static final Method getInitCauseMethod() {
+        try {
+            return Throwable.class.getMethod("initCause", new Class[] { Throwable.class });
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+    
+    /**
+     * Creates an {@link IOException} that has a cause exception.
+     */
+    // [Java 6] Remove
+    private IOException newIOException(String message, Throwable cause) {
+        if (cause == null) {
+            return new IOException(message);
+        }
+        
+        IOException ioe;
+        if (INIT_CAUSE != null) {
+            ioe = new IOException(message);
+            try {
+                INIT_CAUSE.invoke(ioe, cause);
+            } catch (RuntimeException ex) {
+                throw ex;
+            } catch (Exception ex) {
+                throw new UndeclaredThrowableException(ex);
+            }
+        } else {
+            ioe = new IOException(message + "\nCaused by: " + cause.getClass().getName() + 
+            ": " + cause.getMessage());
+        }
+        return ioe;
+    }
+    
+    private void rethrowCachedException(Throwable e) throws IOException {
+        throw newIOException("There was an error loading the " +
+                "template on an earlier attempt; see cause exception.", e);
+    }
+
+    private void setToNegativeAndPutIntoCache(CachedResultKey cacheKey, CachedResult cachedResult, Exception e) {
+        cachedResult.templateOrException = e;
+        cachedResult.source = null;
+        cachedResult.version = null;
+        putIntoCache(cacheKey, cachedResult);
+    }
+
+    private void putIntoCache(CachedResultKey tk, CachedResult cachedTemplate) {
+        if (isCacheStorageConcurrent) {
+            cacheStorage.put(tk, cachedTemplate);
+        } else {
+            synchronized (cacheStorage) {
+                cacheStorage.put(tk, cachedTemplate);
+            }
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private Template loadTemplate(
+            TemplateLoadingResult templateLoaderResult,
+            final String name, final String sourceName, Locale locale, final Object customLookupCondition,
+            String initialEncoding, final boolean parseAsFTL) throws IOException {
+        TemplateConfiguration tc;
+        {
+            TemplateConfiguration cfgTC;
+            try {
+                cfgTC = templateConfigurations != null
+                        ? templateConfigurations.get(sourceName, templateLoaderResult.getSource()) : null;
+            } catch (TemplateConfigurationFactoryException e) {
+                throw newIOException("Error while getting TemplateConfiguration; see cause exception.", e);
+            }
+            TemplateConfiguration resultTC = templateLoaderResult.getTemplateConfiguration();
+            if (resultTC != null) {
+                TemplateConfiguration mergedTC = new TemplateConfiguration();
+                if (cfgTC != null) {
+                    mergedTC.merge(cfgTC);
+                }
+                if (resultTC != null) {
+                    mergedTC.merge(resultTC);
+                }
+                mergedTC.setParentConfiguration(config);
+                
+                tc = mergedTC;
+            } else {
+                tc = cfgTC;
+            }
+        }
+        
+        if (tc != null) {
+            // TC.{encoding,locale} is stronger than the cfg.getTemplate arguments by design.
+            if (tc.isEncodingSet()) {
+                initialEncoding = tc.getEncoding();
+            }
+            if (tc.isLocaleSet()) {
+                locale = tc.getLocale();
+            }
+        }
+        
+        Template template;
+        {
+            Reader reader = templateLoaderResult.getReader();
+            InputStream inputStream = templateLoaderResult.getInputStream();
+            TemplateSpecifiedEncodingHandler templateSpecifiedEncodingHandler;
+            if (reader != null) {
+                if (inputStream != null) {
+                    throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName()
+                            + ", both getReader() and getInputStream() has returned non-null.");
+                }
+                initialEncoding = null;  // No charset decoding has happened
+                templateSpecifiedEncodingHandler = TemplateSpecifiedEncodingHandler.DEFAULT; 
+            } else if (inputStream != null) {
+                if (parseAsFTL) {
+                    // We need mark support, to restart if the charset suggested by <#ftl encoding=...> differs
+                    // from that we use initially.
+                    if (!inputStream.markSupported()) {
+                        inputStream = new BufferedInputStream(inputStream);
+                    }
+                    inputStream.mark(Integer.MAX_VALUE); // Mark is released after the 1st FTL tag
+                    templateSpecifiedEncodingHandler = new MarkReleaserTemplateSpecifiedEncodingHandler(inputStream);
+                } else {
+                    templateSpecifiedEncodingHandler = null; 
+                }
+                // Regarding buffering worries: On the Reader side we should only read in chunks (like through a
+                // BufferedReader), so there shouldn't be a problem if the InputStream is not buffered. (Also, at least
+                // on Oracle JDK and OpenJDK 7 the InputStreamReader itself has an internal ~8K buffer.)
+                reader = new InputStreamReader(inputStream, initialEncoding);
+            } else {
+                throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName()
+                        + ", both getReader() and getInputStream() has returned null.");
+            }
+            
+            try {
+                if (parseAsFTL) {
+                    try {
+                        template = new Template(name, sourceName, reader, config, tc,
+                                initialEncoding, templateSpecifiedEncodingHandler);
+                    } catch (Template.WrongEncodingException wee) {
+                        final String templateSpecifiedEncoding = wee.getTemplateSpecifiedEncoding();
+                        
+                        if (inputStream != null) {
+                            // We restart InputStream to re-decode it with the new charset.
+                            inputStream.reset();
+                            
+                            // Don't close `reader`; it's an InputStreamReader that would close the wrapped InputStream.
+                            reader = new InputStreamReader(inputStream, templateSpecifiedEncoding);
+                        } else {
+                            // Should be impossible to get here
+                            throw new BugException();
+                        }
+                        
+                        template = new Template(name, sourceName, reader, config, tc,
+                                templateSpecifiedEncoding, templateSpecifiedEncodingHandler);
+                    }
+                } else {
+                    // Read the contents into a StringWriter, then construct a single-text-block template from it.
+                    final StringBuilder sb = new StringBuilder();
+                    final char[] buf = new char[4096];
+                    int charsRead;
+                    while ((charsRead = reader.read(buf)) > 0) {
+                        sb.append(buf, 0, charsRead);
+                    }
+                    template = Template.getPlainTextTemplate(name, sourceName, sb.toString(), config);
+                    template.setEncoding(initialEncoding);
+                }
+            } finally {
+                reader.close();
+            }
+        }
+
+        if (tc != null) {
+            tc.apply(template);
+        }
+        
+        template.setLocale(locale);
+        template.setCustomLookupCondition(customLookupCondition);
+        return template;
+    }
+
+    /**
+     * Gets the delay in milliseconds between checking for newer versions of a
+     * template source.
+     * @return the current value of the delay
+     */
+    public long getTemplateUpdateDelayMilliseconds() {
+        // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
+        synchronized (this) {
+            return templateUpdateDelayMilliseconds;
+        }
+    }
+
+    /**
+     * Sets the delay in milliseconds between checking for newer versions of a
+     * template sources.
+     * @param templateUpdateDelayMilliseconds the new value of the delay
+     */
+    public void setTemplateUpdateDelayMilliseconds(long templateUpdateDelayMilliseconds) {
+        // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
+        synchronized (this) {
+            this.templateUpdateDelayMilliseconds = templateUpdateDelayMilliseconds;
+        }
+    }
+
+    /**
+     * Returns if localized template lookup is enabled or not.
+     */
+    public boolean getLocalizedLookup() {
+        // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
+        synchronized (this) {
+            return localizedLookup;
+        }
+    }
+
+    /**
+     * Setis if localized template lookup is enabled or not.
+     */
+    public void setLocalizedLookup(boolean localizedLookup) {
+        // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
+        synchronized (this) {
+            if (this.localizedLookup != localizedLookup) {
+                this.localizedLookup = localizedLookup;
+                clearTemplateCache();
+            }
+        }
+    }
+
+    /**
+     * Removes all entries from the cache, forcing reloading of templates on subsequent
+     * {@link #getTemplate(String, Locale, Object, String, boolean)} calls.
+     * 
+     * @param resetTemplateLoader
+     *            Whether to call {@link TemplateLoader#resetState()}. on the template loader.
+     */
+    public void clearTemplateCache(boolean resetTemplateLoader) {
+        synchronized (cacheStorage) {
+            cacheStorage.clear();
+            if (templateLoader != null && resetTemplateLoader) {
+                templateLoader.resetState();
+            }
+        }
+    }
+    
+    /**
+     * Same as {@link #clearTemplateCache(boolean)} with {@code true} {@code resetTemplateLoader} argument.
+     */
+    @Override
+    public void clearTemplateCache() {
+        synchronized (cacheStorage) {
+            cacheStorage.clear();
+            if (templateLoader != null) {
+                templateLoader.resetState();
+            }
+        }
+    }
+
+    /**
+     * Same as {@link #removeTemplateFromCache(String, Locale, Object, String, boolean)} with {@code null}
+     * {@code customLookupCondition}.
+     */
+    @Override
+    public void removeTemplateFromCache(
+            String name, Locale locale, String encoding, boolean parse) throws IOException {
+        removeTemplateFromCache(name, locale, null, encoding, parse);
+    }
+    
+    /**
+     * Removes an entry from the cache, hence forcing the re-loading of it when it's next time requested. (It doesn't
+     * delete the template file itself.) This is to give the application finer control over cache updating than
+     * {@link #setTemplateUpdateDelayMilliseconds(long)} alone does.
+     * 
+     * For the meaning of the parameters, see
+     * {@link Configuration#getTemplate(String, Locale, Object, String, boolean, boolean)}
+     */
+    public void removeTemplateFromCache(
+            String name, Locale locale, Object customLookupCondition, String encoding, boolean parse)
+    throws IOException {
+        if (name == null) {
+            throw new IllegalArgumentException("Argument \"name\" can't be null");
+        }
+        if (locale == null) {
+            throw new IllegalArgumentException("Argument \"locale\" can't be null");
+        }
+        if (encoding == null) {
+            throw new IllegalArgumentException("Argument \"encoding\" can't be null");
+        }
+        name = templateNameFormat.normalizeRootBasedName(name);
+        if (name != null && templateLoader != null) {
+            boolean debug = LOG.isDebugEnabled();
+            String debugPrefix = debug
+                    ? getDebugPrefix("removeTemplate", name, locale, customLookupCondition, encoding, parse)
+                    : null;
+            CachedResultKey tk = new CachedResultKey(name, locale, customLookupCondition, encoding, parse);
+            
+            if (isCacheStorageConcurrent) {
+                cacheStorage.remove(tk);
+            } else {
+                synchronized (cacheStorage) {
+                    cacheStorage.remove(tk);
+                }
+            }
+            if (debug) {
+                LOG.debug(debugPrefix + "Template was removed from the cache, if it was there");
+            }
+        }
+    }
+
+    private String getDebugPrefix(String operation, String name, Locale locale, Object customLookupCondition, String encoding,
+            boolean parse) {
+        return operation + " " + StringUtil.jQuoteNoXSS(name) + "("
+                + StringUtil.jQuoteNoXSS(locale)
+                + (customLookupCondition != null ? ", cond=" + StringUtil.jQuoteNoXSS(customLookupCondition) : "")
+                + ", " + encoding
+                + (parse ? ", parsed)" : ", unparsed]")
+                + ": ";
+    }    
+
+    /**
+     * Looks up according the {@link TemplateLookupStrategy} and then starts reading the template, if it was changed
+     * compared to the cached result, or if there was no cached result yet.
+     */
+    private TemplateLookupResult lookupAndLoadTemplateIfChanged(
+            String name, Locale locale, Object customLookupCondition,
+            TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion,
+            TemplateLoaderSession session) throws IOException {
+        final TemplateLookupResult lookupResult = templateLookupStrategy.lookup(
+                new DefaultTemplateResolverTemplateLookupContext(
+                        name, locale, customLookupCondition,
+                        cachedResultSource, cachedResultVersion,
+                        session));
+        if (lookupResult == null) {
+            throw new NullPointerException("Lookup result shouldn't be null");
+        }
+        return lookupResult;
+    }
+
+    private String concatPath(List<String> pathSteps, int from, int to) {
+        StringBuilder buf = new StringBuilder((to - from) * 16);
+        for (int i = from; i < to; ++i) {
+            buf.append(pathSteps.get(i));
+            if (i < pathSteps.size() - 1) {
+                buf.append('/');
+            }
+        }
+        return buf.toString();
+    }
+    
+    // Replace with Objects.equals in Java 7
+    private static boolean nullSafeEquals(Object o1, Object o2) {
+        if (o1 == o2) return true;
+        if (o1 == null || o2 == null) return false;
+        return o1.equals(o2);
+    }
+    
+    /**
+     * Used as cache key to look up a {@link CachedResult}. 
+     */
+    @SuppressWarnings("serial")
+    private static final class CachedResultKey implements Serializable {
+        private final String name;
+        private final Locale locale;
+        private final Object customLookupCondition;
+        private final String encoding;
+        private final boolean parse;
+
+        CachedResultKey(String name, Locale locale, Object customLookupCondition, String encoding, boolean parse) {
+            this.name = name;
+            this.locale = locale;
+            this.customLookupCondition = customLookupCondition;
+            this.encoding = encoding;
+            this.parse = parse;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof CachedResultKey) {
+                CachedResultKey tk = (CachedResultKey) o;
+                return
+                    parse == tk.parse &&
+                    name.equals(tk.name) &&
+                    locale.equals(tk.locale) &&
+                    nullSafeEquals(customLookupCondition, tk.customLookupCondition) &&
+                    encoding.equals(tk.encoding);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return
+                name.hashCode() ^
+                locale.hashCode() ^
+                encoding.hashCode() ^
+                (customLookupCondition != null ? customLookupCondition.hashCode() : 0) ^
+                Boolean.valueOf(!parse).hashCode();
+        }
+    }
+
+    /**
+     * Hold the a cached {@link #getTemplate(String, Locale, Object, String, boolean)} result and the associated
+     * information needed to check if the cached value is up to date.
+     * 
+     * <p>
+     * Note: this class is Serializable to allow custom 3rd party CacheStorage implementations to serialize/replicate
+     * them; FreeMarker code itself doesn't rely on its serializability.
+     * 
+     * @see CachedResultKey
+     */
+    private static final class CachedResult implements Cloneable, Serializable {
+        private static final long serialVersionUID = 1L;
+
+        Object templateOrException;
+        TemplateLoadingSource source;
+        Serializable version;
+        long lastChecked;
+        
+        @Override
+        public CachedResult clone() {
+            try {
+                return (CachedResult) super.clone();
+            } catch (CloneNotSupportedException e) {
+                throw new UndeclaredThrowableException(e);
+            }
+        }
+    }
+    
+    private class DefaultTemplateResolverTemplateLookupContext extends TemplateLookupContext {
+
+        private final TemplateLoaderSession session; 
+        
+        DefaultTemplateResolverTemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition,
+                TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion,
+                TemplateLoaderSession session) {
+            super(templateName, localizedLookup ? templateLocale : null, customLookupCondition,
+                    cachedResultSource, cachedResultVersion);
+            this.session = session;
+        }
+
+        @Override
+        public TemplateLookupResult lookupWithAcquisitionStrategy(String path) throws IOException {
+            // Only one of the possible ways of making a name non-normalized, but is the easiest mistake to do:
+            if (path.startsWith("/")) {
+                throw new IllegalArgumentException("Non-normalized name, starts with \"/\": " + path);
+            }
+            
+            int asterisk = path.indexOf(ASTERISK);
+            // Shortcut in case there is no acquisition
+            if (asterisk == -1) {
+                return TemplateLookupResult.from(
+                        path,
+                        templateLoader.load(path, getCachedResultSource(), getCachedResultVersion(), session));
+            }
+            StringTokenizer pathTokenizer = new StringTokenizer(path, "/");
+            int lastAsterisk = -1;
+            List<String> pathSteps = new ArrayList<String>();
+            while (pathTokenizer.hasMoreTokens()) {
+                String pathStep = pathTokenizer.nextToken();
+                if (pathStep.equals(ASTERISKSTR)) {
+                    if (lastAsterisk != -1) {
+                        pathSteps.remove(lastAsterisk);
+                    }
+                    lastAsterisk = pathSteps.size();
+                }
+                pathSteps.add(pathStep);
+            }
+            if (lastAsterisk == -1) {  // if there was no real "*" step after all
+                return TemplateLookupResult.from(
+                        path,
+                        templateLoader.load(path, getCachedResultSource(), getCachedResultVersion(), session));
+            }
+            String basePath = concatPath(pathSteps, 0, lastAsterisk);
+            String postAsteriskPath = concatPath(pathSteps, lastAsterisk + 1, pathSteps.size());
+            StringBuilder buf = new StringBuilder(path.length()).append(basePath);
+            int basePathLen = basePath.length();
+            while (true) {
+                String fullPath = buf.append(postAsteriskPath).toString();
+                TemplateLoadingResult templateLoaderResult = templateLoader.load(
+                        fullPath, getCachedResultSource(), getCachedResultVersion(), session);
+                if (templateLoaderResult.getStatus() == TemplateLoadingResultStatus.OPENED) {
+                    return TemplateLookupResult.from(fullPath, templateLoaderResult);
+                }
+                if (basePathLen == 0) {
+                    return TemplateLookupResult.createNegativeResult();
+                }
+                basePathLen = basePath.lastIndexOf(SLASH, basePathLen - 2) + 1;
+                buf.setLength(basePathLen);
+            }
+        }
+
+        @Override
+        public TemplateLookupResult lookupWithLocalizedThenAcquisitionStrategy(final String templateName,
+                final Locale templateLocale) throws IOException {
+            
+                if (templateLocale == null) {
+                    return lookupWithAcquisitionStrategy(templateName);
+                }
+                
+                int lastDot = templateName.lastIndexOf('.');
+                String prefix = lastDot == -1 ? templateName : templateName.substring(0, lastDot);
+                String suffix = lastDot == -1 ? "" : templateName.substring(lastDot);
+                String localeName = LOCALE_PART_SEPARATOR + templateLocale.toString();
+                StringBuilder buf = new StringBuilder(templateName.length() + localeName.length());
+                buf.append(prefix);
+                tryLocaleNameVariations: while (true) {
+                    buf.setLength(prefix.length());
+                    String path = buf.append(localeName).append(suffix).toString();
+                    TemplateLookupResult lookupResult = lookupWithAcquisitionStrategy(path);
+                    if (lookupResult.isPositive()) {
+                        return lookupResult;
+                    }
+                    
+                    int lastUnderscore = localeName.lastIndexOf('_');
+                    if (lastUnderscore == -1) {
+                        break tryLocaleNameVariations;
+                    }
+                    localeName = localeName.substring(0, lastUnderscore);
+                }
+                return createNegativeLookupResult();
+        }
+        
+    }
+
+    @Override
+    public String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException {
+        return templateNameFormat.toRootBasedName(baseName, targetName);
+    }
+
+    @Override
+    public String normalizeRootBasedName(String name) throws MalformedTemplateNameException {
+        return templateNameFormat.normalizeRootBasedName(name);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java b/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java
new file mode 100644
index 0000000..845c2a2
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core.templateresolver;
+
+import java.util.Locale;
+
+import org.apache.freemarker.core.Template;
+
+/**
+ * Used for the return value of {@link TemplateResolver#getTemplate(String, Locale, Object, String, boolean)}.
+ * 
+ * @since 3.0.0
+ */
+//TODO DRAFT only [FM3]
+public final class GetTemplateResult {
+    
+    private final Template template;
+    private final String missingTemplateNormalizedName;
+    private final String missingTemplateReason;
+    private final Exception missingTemplateCauseException;
+    
+    public GetTemplateResult(Template template) {
+        this.template = template;
+        this.missingTemplateNormalizedName = null;
+        this.missingTemplateReason = null;
+        this.missingTemplateCauseException = null;
+    }
+    
+    public GetTemplateResult(String normalizedName, Exception missingTemplateCauseException) {
+        this.template = null;
+        this.missingTemplateNormalizedName = normalizedName;
+        this.missingTemplateReason = null;
+        this.missingTemplateCauseException = missingTemplateCauseException;
+    }
+    
+    public GetTemplateResult(String normalizedName, String missingTemplateReason) {
+        this.template = null;
+        this.missingTemplateNormalizedName = normalizedName;
+        this.missingTemplateReason = missingTemplateReason;
+        this.missingTemplateCauseException = null;
+    }
+    
+    /**
+     * The {@link Template} if it wasn't missing, otherwise {@code null}.
+     */
+    public Template getTemplate() {
+        return template;
+    }
+
+    /**
+     * When the template was missing, this <em>possibly</em> contains the explanation, or {@code null}. If the
+     * template wasn't missing (i.e., when {@link #getTemplate()} return non-{@code null}) this is always
+     * {@code null}.
+     */
+    public String getMissingTemplateReason() {
+        return missingTemplateReason != null
+                ? missingTemplateReason
+                : (missingTemplateCauseException != null
+                        ? missingTemplateCauseException.getMessage()
+                        : null);
+    }
+    
+    /**
+     * When the template was missing, this <em>possibly</em> contains its normalized name. If the template wasn't
+     * missing (i.e., when {@link #getTemplate()} return non-{@code null}) this is always {@code null}. When the
+     * template is missing, it will be {@code null} for example if the normalization itself was unsuccessful.
+     */
+    public String getMissingTemplateNormalizedName() {
+        return missingTemplateNormalizedName;
+    }
+    
+}
\ No newline at end of file



Mime
View raw message