- Totally redesigned TemplateLoader interface. The FM2 TemplateLoader can't be 
adapted (wrapped) to it, but usually

  it's fairly trivial to "rearrange" an old custom TemplateLoader for the new 
interface. The new TemplateLoader comes
  with several advantages, such as:
  - It can work more efficiently with sophisticated storage mechanisms like a 
database, as it's now possible to pack
    together the existence check, the last modification change check, and 
reading operations into less storage level
    operations (like you can do all of them with a single SQL statement).
  - The new TemplateLoader allows returning the template content either as an 
InputStream or as a Reader. Almost all
    TemplateLoader-s should return InputStream, and FreeMarker takes care of 
charset issues transparently (as a result,
    TemplateLoader-s don't have to support re-reading a template anymore, as we 
solve charset detection misses in
    memory). TemplateLoader-s that are inherently backed by text (String-s), 
such as templates stored in a varchar or
    CLOB column, should return a Reader. Note that templates created from a 
Reader will have template.getEncoding()
    null (logically, as no charset was involved), which was impossible in 
FreeMarker 2.
  - The change detection of the template doesn't have to rely on a millisecond 
resolution timestamp anymore; you can
    use what's most appropriate for the storage mechanism, such as a 
cryptographic hash or a revision number.
  - Template lookups (where you try multiple names until you find the best 
template) can now be transactional and/or
    atomic if the backing storage mechanism supports that, by utilizing the 
TemplateLoaderSession interface.
  - TemplateLoader can now return template-level settings like the output 
format (MIME type basically) of the loaded
    template, in case the backing storage stores such extra information. This 
mechanism can be used together with
    the TemplateConfiguration mechanism (already familiar from FreeMarker 2), 
and overrides the individual settings
    coming from there.

- Template constructors won't close the Reader passed in as agrument anymore 
(because a Reader should be closed
  by who has created it). This avoids some surprises from the past, such as the 
unablility to reset a Reader to a mark
  after parsing. If you call those constructors, be sure that you close the 
Reader yourself. (Obviously, it doesn't
  mater for StringReader-s.)


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

Branch: refs/heads/3
Commit: 3d0fdc8c7250b36b374dc19489ff16050366cf10
Parents: f8d4057
Author: ddekany <[email protected]>
Authored: Sun Feb 5 19:38:32 2017 +0100
Committer: ddekany <[email protected]>
Committed: Sat Feb 11 00:46:46 2017 +0100

----------------------------------------------------------------------
 .../cache/ByteArrayTemplateLoader.java          | 203 +++--
 .../freemarker/cache/ClassTemplateLoader.java   |  58 +-
 ...ConditionalTemplateConfigurationFactory.java |   6 +-
 .../freemarker/cache/FileTemplateLoader.java    | 129 ++--
 .../FirstMatchTemplateConfigurationFactory.java |   4 +-
 .../MergingTemplateConfigurationFactory.java    |   4 +-
 .../freemarker/cache/MultiTemplateLoader.java   | 207 ++----
 .../cache/StatefulTemplateLoader.java           |  35 -
 .../freemarker/cache/StringTemplateLoader.java  | 233 +++---
 .../java/freemarker/cache/TemplateCache.java    | 735 +++++++++++--------
 .../cache/TemplateConfigurationFactory.java     |   8 +-
 .../java/freemarker/cache/TemplateLoader.java   | 138 ++--
 .../freemarker/cache/TemplateLoaderSession.java |  73 ++
 .../freemarker/cache/TemplateLoaderUtils.java   |   3 +-
 .../freemarker/cache/TemplateLoadingResult.java | 199 +++++
 .../cache/TemplateLoadingResultStatus.java      |  49 ++
 .../freemarker/cache/TemplateLoadingSource.java |  66 ++
 .../freemarker/cache/TemplateLookupContext.java |  29 +-
 .../freemarker/cache/TemplateLookupResult.java  |  34 +-
 .../freemarker/cache/URLTemplateLoader.java     | 135 +++-
 .../cache/URLTemplateLoadingSource.java         |  56 ++
 .../freemarker/cache/URLTemplateSource.java     | 144 ----
 .../freemarker/cache/WebAppTemplateLoader.java  | 296 ++++++++
 .../freemarker/cache/WebappTemplateLoader.java  | 228 ------
 .../freemarker/core/BuiltInsForStringsMisc.java |   2 +-
 src/main/java/freemarker/core/Configurable.java |   2 +-
 src/main/java/freemarker/core/Environment.java  |   4 +-
 ...eleaserTemplateSpecifiedEncodingHandler.java |  55 ++
 .../java/freemarker/core/StringLiteral.java     |   4 +-
 .../freemarker/core/TemplateConfiguration.java  |  32 +-
 .../core/TemplateSpecifiedEncodingHandler.java  |  61 ++
 src/main/java/freemarker/core/_CoreAPI.java     |   5 +-
 .../ext/servlet/FreemarkerServlet.java          |  30 +-
 .../freemarker/ext/servlet/InitParamParser.java |   4 +-
 .../java/freemarker/template/Configuration.java |  51 +-
 src/main/java/freemarker/template/Template.java |  70 +-
 .../java/freemarker/template/_TemplateAPI.java  |   5 -
 src/main/javacc/FTL.jj                          |  26 +-
 src/manual/en_US/FM3-CHANGE-LOG.txt             |  45 +-
 .../cache/MultiTemplateLoaderTest.java          |  47 +-
 .../freemarker/cache/TemplateCacheTest.java     | 330 +++++----
 .../cache/TemplateConfigurationFactoryTest.java |  19 +-
 .../cache/TemplateNameFormatTest.java           |  18 +-
 .../IncludeAndImportConfigurableLayersTest.java |   5 +-
 .../freemarker/core/IncludeAndImportTest.java   |  15 +-
 .../core/ObjectBuilderSettingsTest.java         |  20 +-
 .../freemarker/core/TemplatGetEncodingTest.java |  14 +-
 .../ext/jsp/RealServletContainertTest.java      |   6 +-
 .../ext/servlet/FreemarkerServletTest.java      |  64 +-
 .../ext/servlet/InitParamParserTest.java        |  29 +-
 .../java/freemarker/manual/ExamplesTest.java    |   4 +-
 .../freemarker/template/ConfigurationTest.java  |  44 +-
 .../template/TemplateLookupStrategyTest.java    | 174 ++---
 .../template/TemplateNotFoundMessageTest.java   |  35 +-
 .../CopyrightCommentRemoverTemplateLoader.java  |  64 +-
 .../test/MonitoredTemplateLoader.java           | 305 +++++++-
 src/test/java/freemarker/test/TemplateTest.java |  33 +-
 57 files changed, 2836 insertions(+), 1858 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/ByteArrayTemplateLoader.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/cache/ByteArrayTemplateLoader.java 
b/src/main/java/freemarker/cache/ByteArrayTemplateLoader.java
index 11b2182..974a0ef 100644
--- a/src/main/java/freemarker/cache/ByteArrayTemplateLoader.java
+++ b/src/main/java/freemarker/cache/ByteArrayTemplateLoader.java
@@ -20,12 +20,14 @@
 package freemarker.cache;
 
 import java.io.ByteArrayInputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
-import java.util.HashMap;
-import java.util.Iterator;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
 import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
 
 import freemarker.template.utility.StringUtil;
 
@@ -33,93 +35,79 @@ import freemarker.template.utility.StringUtil;
  * A {@link TemplateLoader} that uses a {@link Map} with {@code byte[]} as its 
source of templates. This is similar to
  * {@link StringTemplateLoader}, but uses {@code byte[]} instead of {@link 
String}; see more details there.
  * 
- * @since 2.3.24
+ * <p>Note that {@link ByteArrayTemplateLoader} can't be used with a 
distributed (cluster-wide) {@link CacheStorage},
+ * as it produces {@link TemplateLoadingSource}-s that deliberately throw 
exception on serialization (because the
+ * content is only accessible within a single JVM, and is also volatile).
  */
+// TODO JUnit tests
 public class ByteArrayTemplateLoader implements TemplateLoader {
     
-    private final Map<String, ByteArrayTemplateSource> templates = new 
HashMap<String, ByteArrayTemplateSource>();
+    private static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
     
-    /**
-     * Puts a template into the loader. A call to this method is identical to 
-     * the call to the three-arg {@link #putTemplate(String, byte[], long)} 
-     * passing <tt>System.currentTimeMillis()</tt> as the third argument.
-     * @param name the name of the template.
-     * @param templateSource the source code of the template.
-     */
-    public void putTemplate(String name, byte[] templateSource) {
-        putTemplate(name, templateSource, System.currentTimeMillis());
-    }
+    private final long instanceId = INSTANCE_COUNTER.get();
+    private final AtomicLong templatesRevision = new AtomicLong();
+    private final ConcurrentMap<String, ContentHolder> templates = new 
ConcurrentHashMap<>();
     
     /**
-     * Puts a template into the loader. The name can contain slashes to denote
-     * logical directory structure, but must not start with a slash. If the 
-     * method is called multiple times for the same name and with different
-     * last modified time, the configuration's template cache will reload the 
-     * template according to its own refresh settings (note that if the 
refresh 
-     * is disabled in the template cache, the template will not be reloaded).
-     * Also, since the cache uses lastModified to trigger reloads, calling the
-     * method with different source and identical timestamp won't trigger
-     * reloading.
-     * @param name the name of the template.
-     * @param templateSource the source code of the template.
-     * @param lastModified the time of last modification of the template in 
-     * terms of <tt>System.currentTimeMillis()</tt>
+     * Puts a template into the template loader. The name can contain slashes 
to denote logical directory structure, but
+     * must not start with a slash. Each template will get an unique revision 
number, thus replacing a template will
+     * cause the template cache to reload it (when the update delay expires).
+     * 
+     * <p>This method is thread-safe.
+     * 
+     * @param name
+     *            the name of the template.
+     * @param content
+     *            the source code of the template.
      */
-    public void putTemplate(String name, byte[] templateSource, long 
lastModified) {
-        templates.put(name, new ByteArrayTemplateSource(name, templateSource, 
lastModified));
-    }
-    
-    public void closeTemplateSource(Object templateSource) {
-    }
-    
-    public Object findTemplateSource(String name) {
-        return templates.get(name);
+    public void putTemplate(String name, byte[] content) {
+        templates.put(
+                name,
+                new ContentHolder(content, new Source(instanceId, name), 
templatesRevision.incrementAndGet()));
     }
     
-    public long getLastModified(Object templateSource) {
-        return ((ByteArrayTemplateSource) templateSource).lastModified;
+    /**
+     * Removes the template with the specified name if it was added earlier.
+     * 
+     * <p>
+     * This method is thread-safe.
+     * 
+     * @param name
+     *            Exactly the key with which the template was added.
+     * 
+     * @return Whether a template was found with the given key (and hence was 
removed now)
+     */ 
+    public boolean removeTemplate(String name) {
+        return templates.remove(name) != null;
     }
     
-    public Reader getReader(Object templateSource, String encoding) throws 
UnsupportedEncodingException {
-        return new InputStreamReader(
-                new ByteArrayInputStream(((ByteArrayTemplateSource) 
templateSource).source),
-                encoding);
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
     }
-    
-    private static class ByteArrayTemplateSource {
-        private final String name;
-        private final byte[] source;
-        private final long lastModified;
-        
-        ByteArrayTemplateSource(String name, byte[] source, long lastModified) 
{
-            if (name == null) {
-                throw new IllegalArgumentException("name == null");
-            }
-            if (source == null) {
-                throw new IllegalArgumentException("source == null");
-            }
-            if (lastModified < -1L) {
-                throw new IllegalArgumentException("lastModified < -1L");
-            }
-            this.name = name;
-            this.source = source;
-            this.lastModified = lastModified;
-        }
-        
-        @Override
-        public boolean equals(Object obj) {
-            if (obj instanceof ByteArrayTemplateSource) {
-                return name.equals(((ByteArrayTemplateSource) obj).name);
-            }
-            return false;
-        }
-        
-        @Override
-        public int hashCode() {
-            return name.hashCode();
+
+    @Override
+    public TemplateLoadingResult load(String name, TemplateLoadingSource 
ifSourceDiffersFrom,
+            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) 
throws IOException {
+        ContentHolder contentHolder = templates.get(name);
+        if (contentHolder == null) {
+            return TemplateLoadingResult.NOT_FOUND;
+        } else if (ifSourceDiffersFrom != null && 
ifSourceDiffersFrom.equals(contentHolder.source)
+                && Objects.equals(ifVersionDiffersFrom, 
contentHolder.version)) {
+            return TemplateLoadingResult.NOT_MODIFIED;
+        } else {
+            return new TemplateLoadingResult(
+                    contentHolder.source, contentHolder.version,
+                    new ByteArrayInputStream(contentHolder.content),
+                    null);
         }
     }
-    
+
+    @Override
+    public void resetState() {
+        // Do nothing
+    }
+
     /**
      * Show class name and some details that are useful in template-not-found 
errors.
      */
@@ -129,7 +117,7 @@ public class ByteArrayTemplateLoader implements 
TemplateLoader {
         sb.append(TemplateLoaderUtils.getClassNameForToString(this));
         sb.append("(Map { ");
         int cnt = 0;
-        for (Iterator it = templates.keySet().iterator(); it.hasNext(); ) {
+        for (String name : templates.keySet()) {
             cnt++;
             if (cnt != 1) {
                 sb.append(", ");
@@ -138,7 +126,7 @@ public class ByteArrayTemplateLoader implements 
TemplateLoader {
                 sb.append("...");
                 break;
             }
-            sb.append(StringUtil.jQuote(it.next()));
+            sb.append(StringUtil.jQuote(name));
             sb.append("=...");
         }
         if (cnt != 0) {
@@ -147,5 +135,60 @@ public class ByteArrayTemplateLoader implements 
TemplateLoader {
         sb.append("})");
         return sb.toString();
     }
+
+    private static class ContentHolder {
+        private final byte[] content;
+        private final Source source;
+        private final long version;
+        
+        public ContentHolder(byte[] content, Source source, long version) {
+            this.content = content;
+            this.source = source;
+            this.version = version;
+        }
+        
+    }
+    
+    @SuppressWarnings("serial")
+    private static class Source implements TemplateLoadingSource {
+        
+        private final long instanceId;
+        private final String name;
+        
+        public Source(long instanceId, String name) {
+            this.instanceId = instanceId;
+            this.name = name;
+        }
+    
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + (int) (instanceId ^ (instanceId >>> 32));
+            result = prime * result + ((name == null) ? 0 : name.hashCode());
+            return result;
+        }
+    
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            Source other = (Source) obj;
+            if (instanceId != other.instanceId) return false;
+            if (name == null) {
+                if (other.name != null) return false;
+            } else if (!name.equals(other.name)) {
+                return false;
+            }
+            return true;
+        }
+        
+        private void writeObject(ObjectOutputStream out) throws IOException {
+            throw new IOException(ByteArrayTemplateLoader.class.getName()
+                    + " sources can't be serialized, as they don't support 
clustering.");
+        }
+        
+    }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/ClassTemplateLoader.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/cache/ClassTemplateLoader.java 
b/src/main/java/freemarker/cache/ClassTemplateLoader.java
index 1bfbec8..e9825fe 100644
--- a/src/main/java/freemarker/cache/ClassTemplateLoader.java
+++ b/src/main/java/freemarker/cache/ClassTemplateLoader.java
@@ -29,48 +29,14 @@ import freemarker.template.utility.StringUtil;
  * anywhere where Java can load classes from. Internally, it uses {@link 
Class#getResource(String)} or
  * {@link ClassLoader#getResource(String)} to load templates.
  */
+// TODO
 public class ClassTemplateLoader extends URLTemplateLoader {
     
-    private final Class resourceLoaderClass;
+    private final Class<?> resourceLoaderClass;
     private final ClassLoader classLoader;
     private final String basePackagePath;
 
     /**
-     * Creates a template loader that will use the {@link 
Class#getResource(String)} method of its own class to load the
-     * resources, and {@code "/"} as base package path. This means that that 
template paths will be resolved relatively
-     * the root package of the class hierarchy, so you hardly ever should use 
this constructor, rather do something like
-     * this:<br>
-     * {@link #ClassTemplateLoader(Class, String) new 
ClassTemplateLoader(com.example.myapplication.SomeClass.class,
-     * "templates")}
-     *
-     * <p>
-     * If you extend this class, then the extending class will be used to load 
the resources.
-     *
-     * @deprecated It's a confusing constructor, and seldom useful; use {@link 
#ClassTemplateLoader(Class, String)}
-     *             instead.
-     */
-    @Deprecated
-    public ClassTemplateLoader() {
-        this(null, true, null, "/");
-    }
-
-    /**
-     * Creates a template loader that will use the {@link 
Class#getResource(String)} method of the specified class to
-     * load the resources, and {@code ""} as base package path. This means 
that template paths will be resolved
-     * relatively to the class location, that is, relatively to the directory 
(package) of the class.
-     *
-     * @param resourceLoaderClass
-     *            the class whose {@link Class#getResource(String)} will be 
used to load the templates.
-     *
-     * @deprecated It's confusing that the base path is {@code ""}; use {@link 
#ClassTemplateLoader(Class, String)}
-     *             instead.
-     */
-    @Deprecated
-    public ClassTemplateLoader(Class resourceLoaderClass) {
-        this(resourceLoaderClass, "");
-    }
-
-    /**
      * Creates a template loader that will use the {@link 
Class#getResource(String)} method of the specified class to
      * load the resources, and the specified base package path (absolute or 
relative).
      *
@@ -96,7 +62,7 @@ public class ClassTemplateLoader extends URLTemplateLoader {
      * 
      * @see #ClassTemplateLoader(ClassLoader, String)
      */
-    public ClassTemplateLoader(Class resourceLoaderClass, String 
basePackagePath) {
+    public ClassTemplateLoader(Class<?> resourceLoaderClass, String 
basePackagePath) {
         this(resourceLoaderClass, false, null, basePackagePath);
     }
 
@@ -105,16 +71,14 @@ public class ClassTemplateLoader extends URLTemplateLoader 
{
      * {@link ClassLoader#getResource(String)}. Because a {@link ClassLoader} 
isn't bound to any Java package, it
      * doesn't mater if the {@code basePackagePath} starts with {@code /} or 
not, it will be always relative to the root
      * of the package hierarchy
-     * 
-     * @since 2.3.22
      */
     public ClassTemplateLoader(ClassLoader classLoader, String 
basePackagePath) {
         this(null, true, classLoader, basePackagePath);
     }
 
-    private ClassTemplateLoader(Class resourceLoaderClass, boolean 
allowNullBaseClass, ClassLoader classLoader,
-            String basePackagePath) {
-        if (!allowNullBaseClass) {
+    private ClassTemplateLoader(Class<?> resourceLoaderClass, boolean 
allowNullResourceLoaderClass,
+            ClassLoader classLoader, String basePackagePath) {
+        if (!allowNullResourceLoaderClass) {
             NullArgumentException.check("resourceLoaderClass", 
resourceLoaderClass);
         }
         NullArgumentException.check("basePackagePath", basePackagePath);
@@ -167,8 +131,6 @@ public class ClassTemplateLoader extends URLTemplateLoader {
 
     /**
      * Show class name and some details that are useful in template-not-found 
errors.
-     * 
-     * @since 2.3.21
      */
     @Override
     public String toString() {
@@ -189,18 +151,14 @@ public class ClassTemplateLoader extends 
URLTemplateLoader {
     /**
      * See the similar parameter of {@link #ClassTemplateLoader(Class, 
String)}; {@code null} when other mechanism is
      * used to load the resources.
-     * 
-     * @since 2.3.22
      */
-    public Class getResourceLoaderClass() {
+    public Class<?> getResourceLoaderClass() {
         return resourceLoaderClass;
     }
 
     /**
      * See the similar parameter of {@link #ClassTemplateLoader(ClassLoader, 
String)}; {@code null} when other mechanism
      * is used to load the resources.
-     * 
-     * @since 2.3.22
      */
     public ClassLoader getClassLoader() {
         return classLoader;
@@ -209,8 +167,6 @@ public class ClassTemplateLoader extends URLTemplateLoader {
     /**
      * See the similar parameter of {@link #ClassTemplateLoader(ClassLoader, 
String)}; note that this is a normalized
      * version of what was actually passed to the constructor.
-     * 
-     * @since 2.3.22
      */
     public String getBasePackagePath() {
         return basePackagePath;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/ConditionalTemplateConfigurationFactory.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/freemarker/cache/ConditionalTemplateConfigurationFactory.java 
b/src/main/java/freemarker/cache/ConditionalTemplateConfigurationFactory.java
index 430632e..5110467 100644
--- 
a/src/main/java/freemarker/cache/ConditionalTemplateConfigurationFactory.java
+++ 
b/src/main/java/freemarker/cache/ConditionalTemplateConfigurationFactory.java
@@ -50,11 +50,11 @@ public class ConditionalTemplateConfigurationFactory 
extends TemplateConfigurati
     }
 
     @Override
-    public TemplateConfiguration get(String sourceName, Object templateSource)
+    public TemplateConfiguration get(String sourceName, TemplateLoadingSource 
templateLoadingSource)
             throws IOException, TemplateConfigurationFactoryException {
-        if (matcher.matches(sourceName, templateSource)) {
+        if (matcher.matches(sourceName, templateLoadingSource)) {
             if (templateConfigurationFactory != null) {
-                return templateConfigurationFactory.get(sourceName, 
templateSource);
+                return templateConfigurationFactory.get(sourceName, 
templateLoadingSource);
             } else {
                 return templateConfiguration;
             }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/FileTemplateLoader.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/cache/FileTemplateLoader.java 
b/src/main/java/freemarker/cache/FileTemplateLoader.java
index 166f128..fef103c 100644
--- a/src/main/java/freemarker/cache/FileTemplateLoader.java
+++ b/src/main/java/freemarker/cache/FileTemplateLoader.java
@@ -23,12 +23,13 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.Reader;
+import java.io.InputStream;
+import java.io.Serializable;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
+import java.util.Objects;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -41,8 +42,8 @@ import freemarker.template.utility.StringUtil;
  * A {@link TemplateLoader} that uses files inside a specified directory as 
the source of templates. By default it does
  * security checks on the <em>canonical</em> path that will prevent it serving 
templates outside that specified
  * directory. If you want symbolic links that point outside the template 
directory to work, you need to disable this
- * feature by using {@link #FileTemplateLoader(File, boolean)} with {@code 
true} second argument, but before that, check
- * the security implications there!
+ * feature by using {@link #FileTemplateLoader(File, boolean)} with {@code 
true} second argument, but before that,
+ * check the security implications there!
  */
 public class FileTemplateLoader implements TemplateLoader {
     
@@ -77,20 +78,6 @@ public class FileTemplateLoader implements TemplateLoader {
     private MruCacheStorage correctCasePaths;
 
     /**
-     * Creates a new file template cache that will use the current directory 
(the value of the system property
-     * <code>user.dir</code> as the base directory for loading templates. It 
will not allow access to template files
-     * that are accessible through symlinks that point outside the base 
directory.
-     * 
-     * @deprecated Relying on what the current directory is is a bad practice; 
use
-     *             {@link FileTemplateLoader#FileTemplateLoader(File)} instead.
-     */
-    @Deprecated
-    public FileTemplateLoader()
-    throws IOException {
-        this(new File(SecurityUtilities.getSystemProperty("user.dir")));
-    }
-
-    /**
      * Creates a new file template loader that will use the specified directory
      * as the base directory for loading templates. It will not allow access to
      * template files that are accessible through symlinks that point outside 
@@ -121,9 +108,9 @@ public class FileTemplateLoader implements TemplateLoader {
     public FileTemplateLoader(final File baseDir, final boolean 
disableCanonicalPathCheck)
     throws IOException {
         try {
-            Object[] retval = (Object[]) AccessController.doPrivileged(new 
PrivilegedExceptionAction() {
+            Object[] retval = AccessController.doPrivileged(new 
PrivilegedExceptionAction<Object[]>() {
                 @Override
-                public Object run() throws IOException {
+                public Object[] run() throws IOException {
                     if (!baseDir.exists()) {
                         throw new FileNotFoundException(baseDir + " does not 
exist.");
                     }
@@ -156,13 +143,11 @@ public class FileTemplateLoader implements TemplateLoader 
{
         }
     }
     
-    @Override
-    public Object findTemplateSource(final String name)
-    throws IOException {
+    private File getFile(final String name) throws IOException {
         try {
-            return AccessController.doPrivileged(new 
PrivilegedExceptionAction() {
+            return AccessController.doPrivileged(new 
PrivilegedExceptionAction<File>() {
                 @Override
-                public Object run() throws IOException {
+                public File run() throws IOException {
                     File source = new File(baseDir, SEP_IS_SLASH ? name : 
                         name.replace('/', File.separatorChar));
                     if (!source.isFile()) {
@@ -192,34 +177,22 @@ public class FileTemplateLoader implements TemplateLoader 
{
         }
     }
     
-    @Override
-    public long getLastModified(final Object templateSource) {
-        return ((Long) (AccessController.doPrivileged(new PrivilegedAction()
-        {
+    private long getLastModified(final File templateSource) {
+        return (AccessController.<Long>doPrivileged(new 
PrivilegedAction<Long>() {
             @Override
-            public Object run() {
-                return Long.valueOf(((File) templateSource).lastModified());
+            public Long run() {
+                return Long.valueOf((templateSource).lastModified());
             }
-        }))).longValue();
-        
-        
+        })).longValue();
     }
     
-    @Override
-    public Reader getReader(final Object templateSource, final String encoding)
+    private InputStream getInputStream(final File templateSource)
     throws IOException {
         try {
-            return (Reader) AccessController.doPrivileged(new 
PrivilegedExceptionAction()
-            {
+            return AccessController.doPrivileged(new 
PrivilegedExceptionAction<InputStream>() {
                 @Override
-                public Object run()
-                throws IOException {
-                    if (!(templateSource instanceof File)) {
-                        throw new IllegalArgumentException(
-                                "templateSource wasn't a File, but a: " + 
-                                templateSource.getClass().getName());
-                    }
-                    return new InputStreamReader(new FileInputStream((File) 
templateSource), encoding);
+                public InputStream run() throws IOException {
+                    return new FileInputStream(templateSource);
                 }
             });
         } catch (PrivilegedActionException e) {
@@ -228,9 +201,7 @@ public class FileTemplateLoader implements TemplateLoader {
     }
     
     /**
-     * Called by {@link #findTemplateSource(String)} when {@link 
#getEmulateCaseSensitiveFileSystem()} is {@code true}. Should throw
-     * {@link FileNotFoundException} if there's a mismatch; the error message 
should contain both the requested and the
-     * correct file name.
+     * Called by {@link #getFile(String)} when {@link 
#getEmulateCaseSensitiveFileSystem()} is {@code true}.
      */
     private boolean isNameCaseCorrect(File source) throws IOException {
         final String sourcePath = source.getPath();
@@ -278,11 +249,6 @@ public class FileTemplateLoader implements TemplateLoader {
         }
         return true;
     }
-
-    @Override
-    public void closeTemplateSource(Object templateSource) {
-        // Do nothing.
-    }
     
     /**
      * Returns the base directory in which the templates are searched. This 
comes from the constructor argument, but
@@ -356,5 +322,60 @@ public class FileTemplateLoader implements TemplateLoader {
                 + (emulateCaseSensitiveFileSystem ? ", 
emulateCaseSensitiveFileSystem=true" : "")
                 + ")";
     }
+
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
+    }
+
+    @Override
+    public TemplateLoadingResult load(String name, TemplateLoadingSource 
ifSourceDiffersFrom,
+            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) 
throws IOException {
+        File file = getFile(name);
+        if (file == null) {
+            return TemplateLoadingResult.NOT_FOUND;
+        }
+        
+        FileTemplateLoadingSource source = new FileTemplateLoadingSource(file);
+        
+        long lmd = getLastModified(file);
+        Long version = lmd != -1 ? lmd : null;
+        
+        if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(source) 
+                && Objects.equals(ifVersionDiffersFrom, version)) {
+            return TemplateLoadingResult.NOT_MODIFIED;
+        }
+        
+        return new TemplateLoadingResult(source, version, 
getInputStream(file), null);
+    }
+
+    @Override
+    public void resetState() {
+        // Does nothing
+    }
+    
+    @SuppressWarnings("serial")
+    private static class FileTemplateLoadingSource implements 
TemplateLoadingSource {
+        
+        private final File file;
+
+        FileTemplateLoadingSource(File file) {
+            this.file = file;
+        }
+
+        @Override
+        public int hashCode() {
+            return file.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            return file.equals(((FileTemplateLoadingSource) obj).file);
+        }
+        
+    }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/FirstMatchTemplateConfigurationFactory.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/freemarker/cache/FirstMatchTemplateConfigurationFactory.java 
b/src/main/java/freemarker/cache/FirstMatchTemplateConfigurationFactory.java
index 9382656..d521cc2 100644
--- a/src/main/java/freemarker/cache/FirstMatchTemplateConfigurationFactory.java
+++ b/src/main/java/freemarker/cache/FirstMatchTemplateConfigurationFactory.java
@@ -39,10 +39,10 @@ public class FirstMatchTemplateConfigurationFactory extends 
TemplateConfiguratio
     }
 
     @Override
-    public TemplateConfiguration get(String sourceName, Object templateSource)
+    public TemplateConfiguration get(String sourceName, TemplateLoadingSource 
templateLoadingSource)
             throws IOException, TemplateConfigurationFactoryException {
         for (TemplateConfigurationFactory tcf : 
templateConfigurationFactories) {
-            TemplateConfiguration tc = tcf.get(sourceName, templateSource); 
+            TemplateConfiguration tc = tcf.get(sourceName, 
templateLoadingSource); 
             if (tc != null) {
                 return tc;
             }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/MergingTemplateConfigurationFactory.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/freemarker/cache/MergingTemplateConfigurationFactory.java 
b/src/main/java/freemarker/cache/MergingTemplateConfigurationFactory.java
index 3675d19..b0df922 100644
--- a/src/main/java/freemarker/cache/MergingTemplateConfigurationFactory.java
+++ b/src/main/java/freemarker/cache/MergingTemplateConfigurationFactory.java
@@ -39,12 +39,12 @@ public class MergingTemplateConfigurationFactory extends 
TemplateConfigurationFa
     }
 
     @Override
-    public TemplateConfiguration get(String sourceName, Object templateSource)
+    public TemplateConfiguration get(String sourceName, TemplateLoadingSource 
templateLoadingSource)
             throws IOException, TemplateConfigurationFactoryException {
         TemplateConfiguration mergedTC = null;
         TemplateConfiguration resultTC = null;
         for (TemplateConfigurationFactory tcf : 
templateConfigurationFactories) {
-            TemplateConfiguration tc = tcf.get(sourceName, templateSource);
+            TemplateConfiguration tc = tcf.get(sourceName, 
templateLoadingSource);
             if (tc != null) {
                 if (resultTC == null) {
                     resultTC = tc;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/MultiTemplateLoader.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/cache/MultiTemplateLoader.java 
b/src/main/java/freemarker/cache/MultiTemplateLoader.java
index c86baa3..583f62e 100644
--- a/src/main/java/freemarker/cache/MultiTemplateLoader.java
+++ b/src/main/java/freemarker/cache/MultiTemplateLoader.java
@@ -20,153 +20,49 @@
 package freemarker.cache;
 
 import java.io.IOException;
-import java.io.Reader;
+import java.io.Serializable;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
+import freemarker.template.utility.NullArgumentException;
+
 /**
  * A {@link TemplateLoader} that uses a set of other loaders to load the 
templates. On every request, loaders are
- * queried in the order of their appearance in the array of loaders provided 
to the constructor. However, by default, if
+ * queried in the order of their appearance in the array of loaders provided 
to the constructor. Except, when the
+ * {@linkplain #setSticky(boolean)} sticky} setting is set to {@code true} 
(default is false {@code false}), if
  * a request for some template name was already satisfied in the past by one 
of the loaders, that loader is queried
- * first (stickiness). This behavior can be disabled with {@link 
#setSticky(boolean)}, then the loaders are always
- * queried in the order of their appearance in the array.
+ * first (stickiness).
  * 
  * <p>This class is thread-safe.
  */
-public class MultiTemplateLoader implements StatefulTemplateLoader {
+// TODO JUnit test
+public class MultiTemplateLoader implements TemplateLoader {
 
-    private final TemplateLoader[] loaders;
-    private final Map<String, TemplateLoader> lastLoaderForName = new 
ConcurrentHashMap<String, TemplateLoader>();
+    private final TemplateLoader[] templateLoaders;
+    private final Map<String, TemplateLoader> lastTemplateLoaderForName = new 
ConcurrentHashMap<String, TemplateLoader>();
     
-    private boolean sticky = true;
+    private boolean sticky = false;
 
     /**
-     * Creates a new multi template Loader that will use the specified loaders.
+     * Creates a new instance that will use the specified template loaders.
      * 
-     * @param loaders
-     *            the loaders that are used to load templates.
+     * @param templateLoaders
+     *            the template loaders that are used to load templates, in the 
order as they will be searched
+     *            (except where {@linkplain #setSticky(boolean) stickiness} 
says otherwise).
      */
-    public MultiTemplateLoader(TemplateLoader[] loaders) {
-        this.loaders = loaders.clone();
-    }
-
-    public Object findTemplateSource(String name)
-            throws IOException {
-        if (sticky) {
-            // Use soft affinity - give the loader that last found this
-            // resource a chance to find it again first.
-            TemplateLoader lastLoader = lastLoaderForName.get(name);
-            if (lastLoader != null) {
-                Object source = lastLoader.findTemplateSource(name);
-                if (source != null) {
-                    return new MultiSource(source, lastLoader);
-                }
-            }
-        }
-
-        // If there is no affine loader, or it could not find the resource
-        // again, try all loaders in order of appearance. If any manages
-        // to find the resource, then associate it as the new affine loader
-        // for this resource.
-        for (int i = 0; i < loaders.length; ++i) {
-            TemplateLoader loader = loaders[i];
-            Object source = loader.findTemplateSource(name);
-            if (source != null) {
-                if (sticky) {
-                    lastLoaderForName.put(name, loader);
-                }
-                return new MultiSource(source, loader);
-            }
-        }
-
-        if (sticky) {
-            lastLoaderForName.remove(name);
-        }
-        // Resource not found
-        return null;
-    }
-
-    private Object modifyForIcI(Object source) {
-        // TODO Auto-generated method stub
-        return null;
-    }
-
-    public long getLastModified(Object templateSource) {
-        return ((MultiSource) templateSource).getLastModified();
-    }
-
-    public Reader getReader(Object templateSource, String encoding)
-            throws IOException {
-        return ((MultiSource) templateSource).getReader(encoding);
-    }
-
-    public void closeTemplateSource(Object templateSource)
-            throws IOException {
-        ((MultiSource) templateSource).close();
+    public MultiTemplateLoader(TemplateLoader... templateLoaders) {
+        NullArgumentException.check("templateLoaders", templateLoaders);
+        this.templateLoaders = templateLoaders.clone();
     }
 
     /**
-     * Clears the soft affinity memory, also resets all enclosed {@link 
StatefulTemplateLoader}-s.
+     * Clears the sickiness memory, also resets the state of all enclosed 
{@link TemplateLoader}-s.
      */
+    @Override
     public void resetState() {
-        lastLoaderForName.clear();
-        for (int i = 0; i < loaders.length; i++) {
-            TemplateLoader loader = loaders[i];
-            if (loader instanceof StatefulTemplateLoader) {
-                ((StatefulTemplateLoader) loader).resetState();
-            }
-        }
-    }
-
-    /**
-     * Represents a template source bound to a specific template loader. It 
serves as the complete template source
-     * descriptor used by the MultiTemplateLoader class.
-     */
-    static final class MultiSource {
-
-        private final Object source;
-        private final TemplateLoader loader;
-
-        MultiSource(Object source, TemplateLoader loader) {
-            this.source = source;
-            this.loader = loader;
-        }
-
-        long getLastModified() {
-            return loader.getLastModified(source);
-        }
-
-        Reader getReader(String encoding)
-                throws IOException {
-            return loader.getReader(source, encoding);
-        }
-
-        void close()
-                throws IOException {
-            loader.closeTemplateSource(source);
-        }
-
-        Object getWrappedSource() {
-            return source;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (o instanceof MultiSource) {
-                MultiSource m = (MultiSource) o;
-                return m.loader.equals(loader) && m.source.equals(source);
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return loader.hashCode() + 31 * source.hashCode();
-        }
-
-        @Override
-        public String toString() {
-            return source.toString();
+        lastTemplateLoaderForName.clear();
+        for (TemplateLoader templateLoader : templateLoaders) {
+            templateLoader.resetState();
         }
     }
 
@@ -179,11 +75,11 @@ public class MultiTemplateLoader implements 
StatefulTemplateLoader {
     public String toString() {
         StringBuilder sb = new StringBuilder();
         sb.append("MultiTemplateLoader(");
-        for (int i = 0; i < loaders.length; i++) {
+        for (int i = 0; i < templateLoaders.length; i++) {
             if (i != 0) {
                 sb.append(", ");
             }
-            sb.append("loader").append(i + 1).append(" = ").append(loaders[i]);
+            sb.append("loader").append(i + 1).append(" = 
").append(templateLoaders[i]);
         }
         sb.append(")");
         return sb.toString();
@@ -195,7 +91,7 @@ public class MultiTemplateLoader implements 
StatefulTemplateLoader {
      * @since 2.3.23
      */
     public int getTemplateLoaderCount() {
-        return loaders.length;
+        return templateLoaders.length;
     }
 
     /**
@@ -205,22 +101,67 @@ public class MultiTemplateLoader implements 
StatefulTemplateLoader {
      *            Must be below {@link #getTemplateLoaderCount()}.
      */
     public TemplateLoader getTemplateLoader(int index) {
-        return loaders[index];
+        return templateLoaders[index];
     }
 
     /**
-     * @since 2.3.24
+     * Getter pair of {@link #setSticky(boolean)}.
      */
     public boolean isSticky() {
         return sticky;
     }
 
     /**
-     * @since 2.3.24
+     * Sets if for a name that was already loaded earlier the same {@link 
TemplateLoader} will be tried first, or
+     * we always try the {@link TemplateLoader}-s strictly in the order as it 
was specified in the constructor.
+     * The default is {@code false}.
      */
     public void setSticky(boolean sticky) {
         this.sticky = sticky;
     }
 
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
+    }
+
+    @Override
+    public TemplateLoadingResult load(String name, TemplateLoadingSource 
ifSourceDiffersFrom,
+            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) 
throws IOException {
+        TemplateLoader lastLoader = null;
+        if (sticky) {
+            // Use soft affinity - give the loader that last found this
+            // resource a chance to find it again first.
+            lastLoader = lastTemplateLoaderForName.get(name);
+            if (lastLoader != null) {
+                TemplateLoadingResult result = lastLoader.load(name, 
ifSourceDiffersFrom, ifVersionDiffersFrom, session);
+                if (result.getStatus() != 
TemplateLoadingResultStatus.NOT_FOUND) {
+                    return result;
+                }
+            }
+        }
+
+        // If there is no affine loader, or it could not find the resource
+        // again, try all loaders in order of appearance. If any manages
+        // to find the resource, then associate it as the new affine loader
+        // for this resource.
+        for (TemplateLoader templateLoader : templateLoaders) {
+            if (lastLoader != templateLoader) {
+                TemplateLoadingResult result = templateLoader.load(
+                        name, ifSourceDiffersFrom, ifVersionDiffersFrom, 
session);
+                if (result.getStatus() != 
TemplateLoadingResultStatus.NOT_FOUND) {
+                    if (sticky) {
+                        lastTemplateLoaderForName.put(name, templateLoader);
+                    }
+                    return result;
+                }
+            }
+        }
+
+        if (sticky) {
+            lastTemplateLoaderForName.remove(name);
+        }
+        return TemplateLoadingResult.NOT_FOUND;
+    }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/StatefulTemplateLoader.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/cache/StatefulTemplateLoader.java 
b/src/main/java/freemarker/cache/StatefulTemplateLoader.java
deleted file mode 100644
index 0e0a21c..0000000
--- a/src/main/java/freemarker/cache/StatefulTemplateLoader.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package freemarker.cache;
-
-import freemarker.template.Configuration;
-
-/**
- * Interface that can be implemented by {@link TemplateLoader}-s that maintain 
some 
- * sort of internal state (i.e. caches of earlier lookups for performance 
- * optimization purposes etc.) and support resetting of their state. 
- */
-public interface StatefulTemplateLoader extends TemplateLoader {
-    /**
-     * Invoked by {@link Configuration#clearTemplateCache()} to instruct this
-     * template loader to throw away its current state and start afresh. 
-     */
-    public void resetState();
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/StringTemplateLoader.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/cache/StringTemplateLoader.java 
b/src/main/java/freemarker/cache/StringTemplateLoader.java
index 8b8bd4a..34f568a 100644
--- a/src/main/java/freemarker/cache/StringTemplateLoader.java
+++ b/src/main/java/freemarker/cache/StringTemplateLoader.java
@@ -19,147 +19,97 @@
 
 package freemarker.cache;
 
-import java.io.Reader;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
 import java.io.StringReader;
-import java.util.HashMap;
-import java.util.Iterator;
 import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
 
 import freemarker.template.utility.StringUtil;
 
 /**
- * A {@link TemplateLoader} that uses a {@link Map} with {@link String}-s as 
its source of 
- * templates.
- *
- * In most case the regular way of loading templates from files will be fine.
- * However, there can be situations where you don't want to or can't load a
- * template from a file, e.g. if you have to deploy a single jar for 
- * JavaWebStart or if they are contained within a database.
- * A single template can be created manually
- * e.g.
- * <pre>
- *   String templateStr="Hello ${user}";
- *   Template t = new Template("name", new StringReader(templateStr),
- *               new Configuration());
- * </pre>
- * If, however, you want to create templates from strings which import other 
- * templates this method doesn't work.
- *
- * In that case you can create a StringTemplateLoader and add each template to 
- * it:
- * <pre>
- *   StringTemplateLoader stringLoader = new StringTemplateLoader();
- *   stringLoader.putTemplate("greetTemplate", "&lt;#macro 
greet&gt;Hello&lt;/#macro&gt;");
- *   stringLoader.putTemplate("myTemplate", "&lt;#include 
\"greetTemplate\"&gt;&lt;@greet/&gt; World!");
- * </pre>
- * Then you tell your Configuration object to use it:
- * <pre>
- *   cfg.setTemplateLoader(stringLoader);
- * </pre>
- * After that you should be able to use the templates as usual. Often you will
- * want to combine a <tt>StringTemplateLoader</tt> with another loader. You can
- * do so using a {@link freemarker.cache.MultiTemplateLoader}.
+ * A {@link TemplateLoader} that uses a {@link Map} with {@code String} as its 
source of templates. This is similar to
+ * {@link StringTemplateLoader}, but uses {@code String} instead of {@link 
String}; see more details there.
+ * 
+ * <p>Note that {@link StringTemplateLoader} can't be used with a distributed 
(cluster-wide) {@link CacheStorage},
+ * as it produces {@link TemplateLoadingSource}-s that deliberately throw 
exception on serialization (because the
+ * content is only accessible within a single JVM, and is also volatile).
  */
+// TODO JUnit tests
 public class StringTemplateLoader implements TemplateLoader {
     
-    private final Map<String, StringTemplateSource> templates = new 
HashMap<String, StringTemplateSource>();
+    private static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
     
-    /**
-     * Puts a template into the loader. A call to this method is identical to 
-     * the call to the three-arg {@link #putTemplate(String, String, long)} 
-     * passing <tt>System.currentTimeMillis()</tt> as the third argument.
-     * @param name the name of the template.
-     * @param templateSource the source code of the template.
-     */
-    public void putTemplate(String name, String templateSource) {
-        putTemplate(name, templateSource, System.currentTimeMillis());
-    }
+    private final long instanceId = INSTANCE_COUNTER.get();
+    private final AtomicLong templatesRevision = new AtomicLong();
+    private final ConcurrentMap<String, ContentHolder> templates = new 
ConcurrentHashMap<>();
     
     /**
-     * Puts a template into the loader. The name can contain slashes to denote
-     * logical directory structure, but must not start with a slash. If the 
-     * method is called multiple times for the same name and with different
-     * last modified time, the configuration's template cache will reload the 
-     * template according to its own refresh settings (note that if the 
refresh 
-     * is disabled in the template cache, the template will not be reloaded).
-     * Also, since the cache uses lastModified to trigger reloads, calling the
-     * method with different source and identical timestamp won't trigger
-     * reloading.
-     * @param name the name of the template.
-     * @param templateSource the source code of the template.
-     * @param lastModified the time of last modification of the template in 
-     * terms of <tt>System.currentTimeMillis()</tt>
+     * Puts a template into the template loader. The name can contain slashes 
to denote logical directory structure, but
+     * must not start with a slash. Each template will get an unique revision 
number, thus replacing a template will
+     * cause the template cache to reload it (when the update delay expires).
+     * 
+     * <p>This method is thread-safe.
+     * 
+     * @param name
+     *            the name of the template.
+     * @param content
+     *            the source code of the template.
      */
-    public void putTemplate(String name, String templateSource, long 
lastModified) {
-        templates.put(name, new StringTemplateSource(name, templateSource, 
lastModified));
+    public void putTemplate(String name, String content) {
+        templates.put(
+                name,
+                new ContentHolder(content, new Source(instanceId, name), 
templatesRevision.incrementAndGet()));
     }
     
     /**
      * Removes the template with the specified name if it was added earlier.
      * 
-     * @param name Exactly the key with which the template was added.
+     * <p>
+     * This method is thread-safe.
      * 
-     * @return Whether a template was found with the given key (and hence was 
removed now) 
+     * @param name
+     *            Exactly the key with which the template was added.
      * 
-     * @since 2.3.24
-     */
+     * @return Whether a template was found with the given key (and hence was 
removed now)
+     */ 
     public boolean removeTemplate(String name) {
         return templates.remove(name) != null;
     }
     
-    public void closeTemplateSource(Object templateSource) {
-    }
-    
-    public Object findTemplateSource(String name) {
-        return templates.get(name);
-    }
-    
-    public long getLastModified(Object templateSource) {
-        return ((StringTemplateSource) templateSource).lastModified;
-    }
-    
-    public Reader getReader(Object templateSource, String encoding) {
-        return new StringReader(((StringTemplateSource) 
templateSource).source);
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
     }
-    
-    private static class StringTemplateSource {
-        private final String name;
-        private final String source;
-        private final long lastModified;
-        
-        StringTemplateSource(String name, String source, long lastModified) {
-            if (name == null) {
-                throw new IllegalArgumentException("name == null");
-            }
-            if (source == null) {
-                throw new IllegalArgumentException("source == null");
-            }
-            if (lastModified < -1L) {
-                throw new IllegalArgumentException("lastModified < -1L");
-            }
-            this.name = name;
-            this.source = source;
-            this.lastModified = lastModified;
-        }
-        
-        @Override
-        public boolean equals(Object obj) {
-            if (obj instanceof StringTemplateSource) {
-                return name.equals(((StringTemplateSource) obj).name);
-            }
-            return false;
-        }
-        
-        @Override
-        public int hashCode() {
-            return name.hashCode();
+
+    @Override
+    public TemplateLoadingResult load(String name, TemplateLoadingSource 
ifSourceDiffersFrom,
+            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) 
throws IOException {
+        ContentHolder contentHolder = templates.get(name);
+        if (contentHolder == null) {
+            return TemplateLoadingResult.NOT_FOUND;
+        } else if (ifSourceDiffersFrom != null && 
ifSourceDiffersFrom.equals(contentHolder.source)
+                && Objects.equals(ifVersionDiffersFrom, 
contentHolder.version)) {
+            return TemplateLoadingResult.NOT_MODIFIED;
+        } else {
+            return new TemplateLoadingResult(
+                    contentHolder.source, contentHolder.version,
+                    new StringReader(contentHolder.content),
+                    null);
         }
     }
-    
+
+    @Override
+    public void resetState() {
+        // Do nothing
+    }
+
     /**
      * Show class name and some details that are useful in template-not-found 
errors.
-     * 
-     * @since 2.3.21
      */
     @Override
     public String toString() {
@@ -167,7 +117,7 @@ public class StringTemplateLoader implements TemplateLoader 
{
         sb.append(TemplateLoaderUtils.getClassNameForToString(this));
         sb.append("(Map { ");
         int cnt = 0;
-        for (Iterator it = templates.keySet().iterator(); it.hasNext(); ) {
+        for (String name : templates.keySet()) {
             cnt++;
             if (cnt != 1) {
                 sb.append(", ");
@@ -176,7 +126,7 @@ public class StringTemplateLoader implements TemplateLoader 
{
                 sb.append("...");
                 break;
             }
-            sb.append(StringUtil.jQuote(it.next()));
+            sb.append(StringUtil.jQuote(name));
             sb.append("=...");
         }
         if (cnt != 0) {
@@ -185,5 +135,60 @@ public class StringTemplateLoader implements 
TemplateLoader {
         sb.append("})");
         return sb.toString();
     }
+
+    private static class ContentHolder {
+        private final String content;
+        private final Source source;
+        private final long version;
+        
+        public ContentHolder(String content, Source source, long version) {
+            this.content = content;
+            this.source = source;
+            this.version = version;
+        }
+        
+    }
+    
+    @SuppressWarnings("serial")
+    private static class Source implements TemplateLoadingSource {
+        
+        private final long instanceId;
+        private final String name;
+        
+        public Source(long instanceId, String name) {
+            this.instanceId = instanceId;
+            this.name = name;
+        }
+    
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + (int) (instanceId ^ (instanceId >>> 32));
+            result = prime * result + ((name == null) ? 0 : name.hashCode());
+            return result;
+        }
+    
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            Source other = (Source) obj;
+            if (instanceId != other.instanceId) return false;
+            if (name == null) {
+                if (other.name != null) return false;
+            } else if (!name.equals(other.name)) {
+                return false;
+            }
+            return true;
+        }
+        
+        private void writeObject(ObjectOutputStream out) throws IOException {
+            throw new IOException(StringTemplateLoader.class.getName()
+                    + " sources can't be serialized, as they don't support 
clustering.");
+        }
+        
+    }
     
 }

Reply via email to