http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/SoftCacheStorage.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/SoftCacheStorage.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/SoftCacheStorage.java
deleted file mode 100644
index 0f75912..0000000
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/SoftCacheStorage.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.freemarker.core.templateresolver;
-
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.SoftReference;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
-import org.apache.freemarker.core.util.UndeclaredThrowableException;
-
-/**
- * Soft cache storage is a cache storage that uses {@link SoftReference} 
objects to hold the objects it was passed,
- * therefore allows the garbage collector to purge the cache when it 
determines that it wants to free up memory. This
- * class is thread-safe to the extent that its underlying map is. The 
parameterless constructor uses a thread-safe map
- * since 2.3.24 or Java 5.
- *
- * @see org.apache.freemarker.core.Configuration#setCacheStorage(CacheStorage)
- */
-public class SoftCacheStorage implements ConcurrentCacheStorage, 
CacheStorageWithGetSize {
-    private static final Method atomicRemove = getAtomicRemoveMethod();
-    
-    private final ReferenceQueue queue = new ReferenceQueue();
-    private final Map map;
-    private final boolean concurrent;
-    
-    /**
-     * Creates an instance that uses a {@link ConcurrentMap} internally.
-     */
-    public SoftCacheStorage() {
-        this(new ConcurrentHashMap());
-    }
-    
-    /**
-     * Returns true if the underlying Map is a {@code ConcurrentMap}.
-     */
-    public boolean isConcurrent() {
-        return concurrent;
-    }
-    
-    public SoftCacheStorage(Map backingMap) {
-        map = backingMap;
-        this.concurrent = map instanceof ConcurrentMap;
-    }
-    
-    public Object get(Object key) {
-        processQueue();
-        Reference ref = (Reference) map.get(key);
-        return ref == null ? null : ref.get();
-    }
-
-    public void put(Object key, Object value) {
-        processQueue();
-        map.put(key, new SoftValueReference(key, value, queue));
-    }
-
-    public void remove(Object key) {
-        processQueue();
-        map.remove(key);
-    }
-
-    public void clear() {
-        map.clear();
-        processQueue();
-    }
-    
-    /**
-     * Returns a close approximation of the number of cache entries.
-     * 
-     * @since 2.3.21
-     */
-    public int getSize() {
-        processQueue();
-        return map.size();
-    }
-
-    private void processQueue() {
-        for (; ; ) {
-            SoftValueReference ref = (SoftValueReference) queue.poll();
-            if (ref == null) {
-                return;
-            }
-            Object key = ref.getKey();
-            if (concurrent) {
-                try {
-                    atomicRemove.invoke(map, new Object[] { key, ref });
-                } catch (IllegalAccessException e) {
-                    throw new UndeclaredThrowableException(e);
-                } catch (InvocationTargetException e) {
-                    throw new UndeclaredThrowableException(e);
-                }
-            } else if (map.get(key) == ref) {
-                map.remove(key);
-            }
-        }
-    }
-
-    private static final class SoftValueReference extends SoftReference {
-        private final Object key;
-
-        SoftValueReference(Object key, Object value, ReferenceQueue queue) {
-            super(value, queue);
-            this.key = key;
-        }
-
-        Object getKey() {
-            return key;
-        }
-    }
-    
-    private static Method getAtomicRemoveMethod() {
-        try {
-            return 
Class.forName("java.util.concurrent.ConcurrentMap").getMethod("remove", new 
Class[] { Object.class, Object.class });
-        } catch (ClassNotFoundException e) {
-            return null;
-        } catch (NoSuchMethodException e) {
-            throw new UndeclaredThrowableException(e);
-        }
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/StringTemplateLoader.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/StringTemplateLoader.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/StringTemplateLoader.java
deleted file mode 100644
index 5d0d378..0000000
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/StringTemplateLoader.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.freemarker.core.templateresolver;
-
-import java.io.IOException;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
-import java.io.StringReader;
-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 org.apache.freemarker.core.util.StringUtil;
-
-/**
- * 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 static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
-    
-    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 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 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.
-     * 
-     * <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;
-    }
-    
-    @Override
-    public TemplateLoaderSession createSession() {
-        return null;
-    }
-
-    @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.
-     */
-    @Override
-    public String toString() {
-        StringBuilder sb = new StringBuilder();
-        sb.append(_TemplateLoaderUtils.getClassNameForToString(this));
-        sb.append("(Map { ");
-        int cnt = 0;
-        for (String name : templates.keySet()) {
-            cnt++;
-            if (cnt != 1) {
-                sb.append(", ");
-            }
-            if (cnt > 10) {
-                sb.append("...");
-                break;
-            }
-            sb.append(StringUtil.jQuote(name));
-            sb.append("=...");
-        }
-        if (cnt != 0) {
-            sb.append(' ');
-        }
-        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.");
-        }
-        
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/StrongCacheStorage.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/StrongCacheStorage.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/StrongCacheStorage.java
deleted file mode 100644
index ad5079f..0000000
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/StrongCacheStorage.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.freemarker.core.templateresolver;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Strong cache storage is a cache storage that simply wraps a {@link Map}. It 
holds a strong reference to all objects
- * it was passed, therefore prevents the cache from being purged during 
garbage collection. This class is always
- * thread-safe since 2.3.24, before that if we are running on Java 5 or later.
- *
- * @see org.apache.freemarker.core.Configuration#setCacheStorage(CacheStorage)
- */
-public class StrongCacheStorage implements ConcurrentCacheStorage, 
CacheStorageWithGetSize {
-    
-    private final Map map = new ConcurrentHashMap();
-
-    /**
-     * Always returns {@code true}.
-     */
-    public boolean isConcurrent() {
-        return true;
-    }
-    
-    public Object get(Object key) {
-        return map.get(key);
-    }
-
-    public void put(Object key, Object value) {
-        map.put(key, value);
-    }
-
-    public void remove(Object key) {
-        map.remove(key);
-    }
-    
-    /**
-     * Returns a close approximation of the number of cache entries.
-     * 
-     * @since 2.3.21
-     */
-    public int getSize() {
-        return map.size();
-    }
-    
-    public void clear() {
-        map.clear();
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoader.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoader.java 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoader.java
index f484224..2a58de7 100644
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoader.java
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoader.java
@@ -23,6 +23,7 @@ import java.io.Serializable;
 
 import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.TemplateNotFoundException;
+import 
org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
 
 /**
  * FreeMarker loads template "files" through objects that implement this 
interface, thus the templates need not be real
@@ -61,7 +62,7 @@ public interface TemplateLoader {
      * 
      * @param name
      *            The name (template root directory relative path) of the 
template, already localized and normalized by
-     *            the {@link 
org.apache.freemarker.core.templateresolver.DefaultTemplateResolver cache}. It 
is completely up to the loader implementation to
+     *            the {@link 
org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver 
cache}. It is completely up to the loader implementation to
      *            interpret the name, however it should expect to receive 
hierarchical paths where path components are
      *            separated by a slash (not backslash). Backslashes (or any 
other OS specific separator character) are
      *            not considered as separators by FreeMarker, and thus they 
will not be replaced with slash before

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoaderSession.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoaderSession.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoaderSession.java
index 9fd99ff..262dcce 100644
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoaderSession.java
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoaderSession.java
@@ -22,6 +22,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
 
+import 
org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
+
 /**
  * Stores shared state between {@link TemplateLoader} operations that are 
executed close to each other in the same
  * thread. For example, a {@link TemplateLoader} that reads from a database 
might wants to store the database

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResult.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResult.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResult.java
index 426e050..94844a4 100644
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResult.java
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingResult.java
@@ -27,6 +27,7 @@ import java.util.Date;
 
 import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.ast.TemplateConfiguration;
+import 
org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
 import org.apache.freemarker.core.util.NullArgumentException;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingSource.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingSource.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingSource.java
index 42b5398..2af5721 100644
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingSource.java
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLoadingSource.java
@@ -21,6 +21,9 @@ package org.apache.freemarker.core.templateresolver;
 import java.io.Serializable;
 import java.util.HashMap;
 
+import 
org.apache.freemarker.core.templateresolver.impl.ByteArrayTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.FileTemplateLoader;
+
 /**
  * The point of {@link TemplateLoadingSource} is that with their {@link 
Object#equals(Object)} method we can tell if two
  * cache entries were generated from the same physical resource or not. 
Comparing the template names isn't enough,

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java
index 304d9b6..a37e043 100644
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupContext.java
@@ -24,6 +24,7 @@ import java.io.Serializable;
 import java.util.Locale;
 
 import org.apache.freemarker.core.Configuration;
+import 
org.apache.freemarker.core.templateresolver.impl.DefaultTemplateLookupStrategy;
 
 /**
  * Used as the parameter of {@link 
TemplateLookupStrategy#lookup(TemplateLookupContext)}.
@@ -31,13 +32,11 @@ import org.apache.freemarker.core.Configuration;
  * 
  * @since 2.3.22
  */
-public abstract class TemplateLookupContext {
+public abstract class TemplateLookupContext<R extends TemplateLookupResult> {
     
     private final String templateName;
     private final Locale templateLocale;
     private final Object customLookupCondition;
-    private final TemplateLoadingSource cachedResultSource;
-    private final Serializable cachedResultVersion;
 
     /**
      * Finds the template source based on its <em>normalized</em> name; 
handles {@code *} steps (so called acquisition),
@@ -56,7 +55,7 @@ public abstract class TemplateLookupContext {
      *         not for {@link TemplateLoader}-s). Hence discarding a positive 
result and looking for another can
      *         generate substantial extra I/O.
      */
-    public abstract TemplateLookupResult lookupWithAcquisitionStrategy(String 
templateName) throws IOException;
+    public abstract R lookupWithAcquisitionStrategy(String templateName) 
throws IOException;
 
     /**
      * Finds the template source based on its <em>normalized</em> name; tries 
localized variations going from most
@@ -64,19 +63,16 @@ public abstract class TemplateLookupContext {
      * If {@code templateLocale} is {@code null} (typically, because {@link 
Configuration#getLocalizedLookup()} is
      * {@code false})), then it's the same as calling {@link 
#lookupWithAcquisitionStrategy(String)} directly. This is
      * the default strategy of FreeMarker (at least in 2.3.x), so for more 
information, see
-     * {@link TemplateLookupStrategy#DEFAULT_2_3_0}.
+     * {@link DefaultTemplateLookupStrategy#INSTANCE}.
      */
-    public abstract TemplateLookupResult 
lookupWithLocalizedThenAcquisitionStrategy(String templateName,
+    public abstract R lookupWithLocalizedThenAcquisitionStrategy(String 
templateName,
             Locale templateLocale) throws IOException;
     
     /** Default visibility to prevent extending the class from outside this 
package. */
-    TemplateLookupContext(String templateName, Locale templateLocale, Object 
customLookupCondition,
-            TemplateLoadingSource cachedResultSource, Serializable 
cachedResultVersion) {
+    protected TemplateLookupContext(String templateName, Locale 
templateLocale, Object customLookupCondition) {
         this.templateName = templateName;
         this.templateLocale = templateLocale;
         this.customLookupCondition = customLookupCondition;
-        this.cachedResultSource = cachedResultSource;
-        this.cachedResultVersion = cachedResultVersion;
     }
 
     /**
@@ -111,16 +107,6 @@ public abstract class TemplateLookupContext {
      * {@link TemplateLookupStrategy#lookup(TemplateLookupContext)}. (In the 
current implementation it just always
      * returns the same static singleton, but that might need to change in the 
future.)
      */
-    public TemplateLookupResult createNegativeLookupResult() {
-        return TemplateLookupResult.createNegativeResult();
-    }
-
-    TemplateLoadingSource getCachedResultSource() {
-        return cachedResultSource;
-    }
-
-    Serializable getCachedResultVersion() {
-        return cachedResultVersion;
-    }
+    public abstract R createNegativeLookupResult();
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java
index c6a4aa5..4917878 100644
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupResult.java
@@ -20,30 +20,23 @@
 package org.apache.freemarker.core.templateresolver;
 
 import org.apache.freemarker.core.Template;
-import org.apache.freemarker.core.util.NullArgumentException;
+import 
org.apache.freemarker.core.templateresolver.impl.TemplateLoaderBasedTemplateLookupResult;
 
 /**
  * The return value of {@link 
TemplateLookupStrategy#lookup(TemplateLookupContext)} and similar lookup 
methods. You
  * usually get one from {@link 
TemplateLookupContext#lookupWithAcquisitionStrategy(String)} or
- * {@link TemplateLookupContext#createNegativeLookupResult()}; you can't 
create instances of this directly.
+ * {@link TemplateLookupContext#createNegativeLookupResult()}.
+ * 
+ * <p>
+ * Subclass this only if you are implementing a {@link TemplateLookupContext}; 
if the {@link TemplateLookupContext} that
+ * you are implementing uses {@link TemplateLoader}-s, consider using {@link 
TemplateLoaderBasedTemplateLookupResult}
+ * instead of writing your own subclass.
  * 
  * @since 2.3.22
  */
 public abstract class TemplateLookupResult {
 
-    /** Used internally to get a not-found result (currently just a static 
singleton). */
-    static TemplateLookupResult createNegativeResult() {
-        return NegativeTemplateLookupResult.INSTANCE;
-    }
-    
-    /** Used internally to create the appropriate kind of result from the 
parameters. */
-    static TemplateLookupResult from(String templateSourceName, 
TemplateLoadingResult templateLoaderResult) {
-        return templateLoaderResult.getStatus() != 
TemplateLoadingResultStatus.NOT_FOUND
-                ? new PositiveTemplateLookupResult(templateSourceName, 
templateLoaderResult)
-                : createNegativeResult();
-    }
-    
-    private TemplateLookupResult() {
+    protected TemplateLookupResult() {
         // nop
     }
     
@@ -57,77 +50,5 @@ public abstract class TemplateLookupResult {
      * Tells if the lookup has found a matching template.
      */
     public abstract boolean isPositive();
-
-    /**
-     * Used internally to extract the {@link TemplateLoadingResult}; {@code 
null} if {@link #isPositive()} is
-     * {@code false}.
-     */
-    abstract TemplateLoadingResult getTemplateLoaderResult();
-
-    private static final class PositiveTemplateLookupResult extends 
TemplateLookupResult {
-
-        private final String templateSourceName;
-        private final TemplateLoadingResult templateLoaderResult;
-
-        /**
-         * @param templateSourceName
-         *            The name of the matching template found. This is not 
necessarily the same as the template name
-         *            with which the template was originally requested. For 
example, one may gets a template for the
-         *            {@code "foo.ftl"} name, but due to localized lookup the 
template is actually loaded from
-         *            {@code "foo_de.ftl"}. Then this parameter must be {@code 
"foo_de.ftl"}, not {@code "foo.ftl"}. Not
-         *            {@code null}.
-         * 
-         * @param templateLoaderResult
-         *            See {@link TemplateLoader#load} to understand what that 
means. Not
-         *            {@code null}.
-         */
-        private PositiveTemplateLookupResult(String templateSourceName, 
TemplateLoadingResult templateLoaderResult) {
-            NullArgumentException.check("templateName", templateSourceName);
-            NullArgumentException.check("templateLoaderResult", 
templateLoaderResult);
-
-            this.templateSourceName = templateSourceName;
-            this.templateLoaderResult = templateLoaderResult;
-        }
-
-        @Override
-        public String getTemplateSourceName() {
-            return templateSourceName;
-        }
-
-        @Override
-        TemplateLoadingResult getTemplateLoaderResult() {
-            return templateLoaderResult;
-        }
-
-        @Override
-        public boolean isPositive() {
-            return true;
-        }
-    }
-
-    private static final class NegativeTemplateLookupResult extends 
TemplateLookupResult {
-        
-        private static final NegativeTemplateLookupResult INSTANCE = new 
NegativeTemplateLookupResult();
-                
-        private NegativeTemplateLookupResult() {
-            // nop
-        }
-
-        @Override
-        public String getTemplateSourceName() {
-            return null;
-        }
-
-        @Override
-        TemplateLoadingResult getTemplateLoaderResult() {
-            return null;
-        }
-
-        @Override
-        public boolean isPositive() {
-            return false;
-        }
-        
-    }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java
index e993140..a1c9577 100644
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateLookupStrategy.java
@@ -20,7 +20,6 @@
 package org.apache.freemarker.core.templateresolver;
 
 import java.io.IOException;
-import java.util.Locale;
 
 import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.Template;
@@ -58,28 +57,6 @@ import org.apache.freemarker.core.Template;
 public abstract class TemplateLookupStrategy {
 
     /**
-     * <p>
-     * The default lookup strategy of FreeMarker.
-     * 
-     * <p>
-     * Through an example: Assuming localized lookup is enabled and that a 
template is requested for the name
-     * {@code example.ftl} and {@code Locale("es", "ES", "Traditional_WIN")}, 
it will try the following template names,
-     * in this order: {@code "foo_en_AU_Traditional_WIN.ftl"}, {@code 
"foo_en_AU_Traditional.ftl"},
-     * {@code "foo_en_AU.ftl"}, {@code "foo_en.ftl"}, {@code "foo.ftl"}. It 
stops at the first variation where it finds
-     * a template. (If the template name contains "*" steps, finding the 
template for the attempted localized variation
-     * happens with the template acquisition mechanism.) If localized lookup 
is disabled, it won't try to add any locale
-     * strings, so it just looks for {@code "foo.ftl"}.
-     * 
-     * <p>
-     * The generation of the localized name variation with the default lookup 
strategy, happens like this: It removes
-     * the file extension (the part starting with the <em>last</em> dot), then 
appends {@link Locale#toString()} after
-     * it, and puts back the extension. Then it starts to remove the parts 
from the end of the locale, considering
-     * {@code "_"} as the separator between the parts. It won't remove parts 
that are not part of the locale string
-     * (like if the requested template name is {@code foo_bar.ftl}, it won't 
remove the {@code "_bar"}).
-     */
-    public static final TemplateLookupStrategy DEFAULT_2_3_0 = new 
Default020300();
-    
-    /**
      * Finds the template source that matches the template name, locale (if 
not {@code null}) and other parameters
      * specified in the {@link TemplateLookupContext}. See also the 
class-level {@link TemplateLookupStrategy}
      * documentation to understand lookup strategies more.
@@ -96,20 +73,6 @@ public abstract class TemplateLookupStrategy {
      *         {@code TemplateLookupContext#createNegativeLookupResult()} if 
no matching template exists. Can't be
      *         {@code null}.
      */
-    public abstract TemplateLookupResult lookup(TemplateLookupContext ctx) 
throws IOException;
-    
-    private static class Default020300 extends TemplateLookupStrategy {
-        
-        @Override
-        public TemplateLookupResult lookup(TemplateLookupContext ctx) throws 
IOException {
-            return 
ctx.lookupWithLocalizedThenAcquisitionStrategy(ctx.getTemplateName(), 
ctx.getTemplateLocale());
-        }
-        
-        @Override
-        public String toString() {
-            return "TemplateLookupStrategy.DEFAULT_2_3_0";
-        }
-        
-    }
+    public abstract <R extends TemplateLookupResult> R 
lookup(TemplateLookupContext<R> ctx) throws IOException;
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java
index 5b68241..ebcbe9a 100644
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateNameFormat.java
@@ -19,11 +19,6 @@
 
 package org.apache.freemarker.core.templateresolver;
 
-import org.apache.freemarker.core.Configuration;
-import org.apache.freemarker.core.TemplateNotFoundException;
-import org.apache.freemarker.core.Version;
-import org.apache.freemarker.core.util.StringUtil;
-
 /**
  * Symbolized template name format. The API of this class isn't exposed as 
it's too immature, so custom
  * template name formats aren't possible yet.
@@ -32,389 +27,28 @@ import org.apache.freemarker.core.util.StringUtil;
  */
 public abstract class TemplateNameFormat {
 
-    private TemplateNameFormat() {
-       // Currently can't be instantiated from outside 
+    protected TemplateNameFormat() {
+       //  
     }
     
     /**
-     * The default template name format when {@link 
Configuration#Configuration(Version) incompatible_improvements} is
-     * below 2.4.0. As of FreeMarker 2.4.0, the default {@code 
incompatible_improvements} is still {@code 2.3.0}, and it
-     * will certainly remain so for a very long time. In new projects it's 
highly recommended to use
-     * {@link #DEFAULT_2_4_0} instead.
-     */
-    public static final TemplateNameFormat DEFAULT_2_3_0 = new Default020300();
-    
-    /**
-     * The default template name format only when {@link 
Configuration#Configuration(Version) incompatible_improvements}
-     * is set to 2.4.0 (or higher). This is not the out-of-the-box default 
format of FreeMarker 2.4.x, because the
-     * default {@code incompatible_improvements} is still 2.3.0 there.
-     * 
-     * <p>
-     * Differences to the {@link #DEFAULT_2_3_0} format:
-     * 
-     * <ul>
-     * 
-     * <li>The scheme and the path need not be separated with {@code "://"} 
anymore, only with {@code ":"}. This makes
-     * template names like {@code "classpath:foo.ftl"} interpreted as an 
absolute name with scheme {@code "classpath"}
-     * and absolute path "foo.ftl". The scheme name before the {@code ":"} 
can't contain {@code "/"}, or else it's
-     * treated as a malformed name. The scheme part can be separated either 
with {@code "://"} or just {@code ":"} from
-     * the path. Hence, {@code myschme:/x} is normalized to {@code myschme:x}, 
while {@code myschme:///x} is normalized
-     * to {@code myschme://x}, but {@code myschme://x} or {@code myschme:/x} 
aren't changed by normalization. It's up
-     * the {@link TemplateLoader} to which the normalized names are passed to 
decide which of these scheme separation
-     * conventions are valid (maybe both).</li>
-     * 
-     * <li>{@code ":"} is not allowed in template names, except as the scheme 
separator (see previous point).
-     * 
-     * <li>Malformed paths throw {@link MalformedTemplateNameException} 
instead of acting like if the template wasn't
-     * found.
-     * 
-     * <li>{@code "\"} (backslash) is not allowed in template names, and 
causes {@link MalformedTemplateNameException}.
-     * With {@link #DEFAULT_2_3_0} you would certainly end up with a {@link 
TemplateNotFoundException} (or worse,
-     * it would work, but steps like {@code ".."} wouldn't be normalized by 
FreeMarker).
-     * 
-     * <li>Template names might end with {@code /}, like {@code "foo/"}, and 
the presence or lack of the terminating
-     * {@code /} is seen as significant. While their actual interpretation is 
up to the {@link TemplateLoader},
-     * operations that manipulate template names assume that the last step 
refers to a "directory" as opposed to a
-     * "file" exactly if the terminating {@code /} is present. Except, the 
empty name is assumed to refer to the root
-     * "directory" (despite that it doesn't end with {@code /}).
-     *
-     * <li>{@code //} is normalized to {@code /}, except of course if it's in 
the scheme name terminator. Like
-     * {@code foo//bar///baaz.ftl} is normalized to {@code foo/bar/baaz.ftl}. 
(In general, 0 long step names aren't
-     * possible anymore.)</li>
-     * 
-     * <li>The {@code ".."} bugs of the legacy normalizer are fixed: {@code 
".."} steps has removed the preceding
-     * {@code "."} or {@code "*"} or scheme steps, not treating them specially 
as they should be. Now these work as
-     * expected. Examples: {@code "a/./../c"} has become to {@code "a/c"}, now 
it will be {@code "c"}; {@code "a/b/*}
-     * {@code /../c"} has become to {@code "a/b/c"}, now it will be {@code 
"a/*}{@code /c"}; {@code "scheme://.."} has
-     * become to {@code "scheme:/"}, now it will be {@code null} ({@link 
TemplateNotFoundException}) for backing out of
-     * the root directory.</li>
-     * 
-     * <li>As now directory paths has to be handled as well, it recognizes 
terminating, leading, and lonely {@code ".."}
-     * and {@code "."} steps. For example, {@code "foo/bar/.."} now becomes to 
{@code "foo/"}</li>
-     * 
-     * <li>Multiple consecutive {@code *} steps are normalized to one</li>
-     * 
-     * </ul>
-     */
-    public static final TemplateNameFormat DEFAULT_2_4_0 = new Default020400();
-    
-    /**
      * Implements {@link TemplateResolver#toRootBasedName(String, String)}; 
see more there.
      */
-    abstract String toRootBasedName(String baseName, String targetName) throws 
MalformedTemplateNameException;
+    public abstract String toRootBasedName(String baseName, String targetName) 
throws MalformedTemplateNameException;
     
     /**
      * Implements {@link TemplateResolver#normalizeRootBasedName(String)}; see 
more there.
      */
-    abstract String normalizeRootBasedName(String name) throws 
MalformedTemplateNameException;
-
-    private static final class Default020300 extends TemplateNameFormat {
-        @Override
-        String toRootBasedName(String baseName, String targetName) {
-            if (targetName.indexOf("://") > 0) {
-                return targetName;
-            } else if (targetName.startsWith("/")) {
-                int schemeSepIdx = baseName.indexOf("://");
-                if (schemeSepIdx > 0) {
-                    return baseName.substring(0, schemeSepIdx + 2) + 
targetName;
-                } else {
-                    return targetName.substring(1);
-                }
-            } else {
-                if (!baseName.endsWith("/")) {
-                    baseName = baseName.substring(0, baseName.lastIndexOf("/") 
+ 1);
-                }
-                return baseName + targetName;
-            }
-        }
-    
-        @Override
-        String normalizeRootBasedName(final String name) throws 
MalformedTemplateNameException {
-            // Disallow 0 for security reasons.
-            checkNameHasNoNullCharacter(name);
-            
-            // The legacy algorithm haven't considered schemes, so the name is 
in effect a path.
-            // Also, note that `path` will be repeatedly replaced below, while 
`name` is final.
-            String path = name;
-            
-            for (; ; ) {
-                int parentDirPathLoc = path.indexOf("/../");
-                if (parentDirPathLoc == 0) {
-                    // If it starts with /../, then it reaches outside the 
template
-                    // root.
-                    throw newRootLeavingException(name);
-                }
-                if (parentDirPathLoc == -1) {
-                    if (path.startsWith("../")) {
-                        throw newRootLeavingException(name);
-                    }
-                    break;
-                }
-                int previousSlashLoc = path.lastIndexOf('/', parentDirPathLoc 
- 1);
-                path = path.substring(0, previousSlashLoc + 1) +
-                       path.substring(parentDirPathLoc + "/../".length());
-            }
-            for (; ; ) {
-                int currentDirPathLoc = path.indexOf("/./");
-                if (currentDirPathLoc == -1) {
-                    if (path.startsWith("./")) {
-                        path = path.substring("./".length());
-                    }
-                    break;
-                }
-                path = path.substring(0, currentDirPathLoc) +
-                       path.substring(currentDirPathLoc + "/./".length() - 1);
-            }
-            // Editing can leave us with a leading slash; strip it.
-            if (path.length() > 1 && path.charAt(0) == '/') {
-                path = path.substring(1);
-            }
-            return path;
-        }
-        
-        @Override
-        public String toString() {
-            return "TemplateNameFormat.DEFAULT_2_3_0";
-        }
-        
-    }
-
-    private static final class Default020400 extends TemplateNameFormat {
-        @Override
-        String toRootBasedName(String baseName, String targetName) {
-            if (findSchemeSectionEnd(targetName) != 0) {
-                return targetName;
-            } else if (targetName.startsWith("/")) {  // targetName is an 
absolute path
-                final String targetNameAsRelative = targetName.substring(1);
-                final int schemeSectionEnd = findSchemeSectionEnd(baseName);
-                if (schemeSectionEnd == 0) {
-                    return targetNameAsRelative;
-                } else {
-                    // Prepend the scheme of baseName:
-                    return baseName.substring(0, schemeSectionEnd) + 
targetNameAsRelative;
-                }
-            } else {  // targetName is a relative path
-                if (!baseName.endsWith("/")) {
-                    // Not a directory name => get containing directory name
-                    int baseEnd = baseName.lastIndexOf("/") + 1;
-                    if (baseEnd == 0) {
-                        // For something like "classpath:t.ftl", must not 
remove the scheme part:
-                        baseEnd = findSchemeSectionEnd(baseName);
-                    }
-                    baseName = baseName.substring(0, baseEnd);
-                }
-                return baseName + targetName;
-            }
-        }
-    
-        @Override
-        String normalizeRootBasedName(final String name) throws 
MalformedTemplateNameException {
-            // Disallow 0 for security reasons.
-            checkNameHasNoNullCharacter(name);
-    
-            if (name.indexOf('\\') != -1) {
-                throw new MalformedTemplateNameException(
-                        name,
-                        "Backslash (\"\\\") is not allowed in template names. 
Use slash (\"/\") instead.");
-            }
-            
-            // Split name to a scheme and a path:
-            final String scheme;
-            String path;
-            {
-                int schemeSectionEnd = findSchemeSectionEnd(name);
-                if (schemeSectionEnd == 0) {
-                    scheme = null;
-                    path = name;
-                } else {
-                    scheme = name.substring(0, schemeSectionEnd);
-                    path = name.substring(schemeSectionEnd);
-                }
-            }
-            
-            if (path.indexOf(':') != -1) {
-                throw new MalformedTemplateNameException(name,
-                        "The ':' character can only be used after the scheme 
name (if there's any), "
-                        + "not in the path part");
-            }
-            
-            path = removeRedundantSlashes(path);
-            // path now doesn't start with "/"
-            
-            path = removeDotSteps(path);
-            
-            path = resolveDotDotSteps(path, name);
-    
-            path = removeRedundantStarSteps(path);
-            
-            return scheme == null ? path : scheme + path;
-        }
-
-        private int findSchemeSectionEnd(String name) {
-            int schemeColonIdx = name.indexOf(":");
-            if (schemeColonIdx == -1 || name.lastIndexOf('/', schemeColonIdx - 
1) != -1) {
-                return 0;
-            } else {
-                // If there's a following "//", it's treated as the part of 
the scheme section:
-                if (schemeColonIdx + 2 < name.length()
-                        && name.charAt(schemeColonIdx + 1) == '/' && 
name.charAt(schemeColonIdx + 2) == '/') {
-                    return schemeColonIdx + 3;
-                } else {
-                    return schemeColonIdx + 1;
-                }
-            }
-        }
-    
-        private String removeRedundantSlashes(String path) {
-            String prevName;
-            do {
-                prevName = path;
-                path = StringUtil.replace(path, "//", "/");
-            } while (prevName != path);
-            return path.startsWith("/") ? path.substring(1) : path;
-        }
-    
-        private String removeDotSteps(String path) {
-            int nextFromIdx = path.length() - 1;
-            findDotSteps: while (true) {
-                final int dotIdx = path.lastIndexOf('.', nextFromIdx);
-                if (dotIdx < 0) {
-                    return path;
-                }
-                nextFromIdx = dotIdx - 1;
-                
-                if (dotIdx != 0 && path.charAt(dotIdx - 1) != '/') {
-                    // False alarm
-                    continue findDotSteps;
-                }
-                
-                final boolean slashRight;
-                if (dotIdx + 1 == path.length()) {
-                    slashRight = false;
-                } else if (path.charAt(dotIdx + 1) == '/') {
-                    slashRight = true;
-                } else {
-                    // False alarm
-                    continue findDotSteps;
-                }
-                
-                if (slashRight) { // "foo/./bar" or "./bar" 
-                    path = path.substring(0, dotIdx) + path.substring(dotIdx + 
2);
-                } else { // "foo/." or "."
-                    path = path.substring(0, path.length() - 1);
-                }
-            }
-        }
-    
-        /**
-         * @param name The original name, needed for exception error messages.
-         */
-        private String resolveDotDotSteps(String path, final String name) 
throws MalformedTemplateNameException {
-            int nextFromIdx = 0;
-            findDotDotSteps: while (true) {
-                final int dotDotIdx = path.indexOf("..", nextFromIdx);
-                if (dotDotIdx < 0) {
-                    return path;
-                }
-    
-                if (dotDotIdx == 0) {
-                    throw newRootLeavingException(name);
-                } else if (path.charAt(dotDotIdx - 1) != '/') {
-                    // False alarm
-                    nextFromIdx = dotDotIdx + 3;
-                    continue findDotDotSteps;
-                }
-                // Here we know that it has a preceding "/".
-                
-                final boolean slashRight;
-                if (dotDotIdx + 2 == path.length()) {
-                    slashRight = false;
-                } else if (path.charAt(dotDotIdx + 2) == '/') {
-                    slashRight = true;
-                } else {
-                    // False alarm
-                    nextFromIdx = dotDotIdx + 3;
-                    continue findDotDotSteps;
-                }
-                
-                int previousSlashIdx;
-                boolean skippedStarStep = false;
-                {
-                    int searchSlashBacwardsFrom = dotDotIdx - 2; // before the 
"/.."
-                    scanBackwardsForSlash: while (true) {
-                        if (searchSlashBacwardsFrom == -1) {
-                            throw newRootLeavingException(name);
-                        }
-                        previousSlashIdx = path.lastIndexOf('/', 
searchSlashBacwardsFrom);
-                        if (previousSlashIdx == -1) {
-                            if (searchSlashBacwardsFrom == 0 && path.charAt(0) 
== '*') {
-                                // "*/.."
-                                throw newRootLeavingException(name);
-                            }
-                            break scanBackwardsForSlash;
-                        }
-                        if (path.charAt(previousSlashIdx + 1) == '*' && 
path.charAt(previousSlashIdx + 2) == '/') {
-                            skippedStarStep = true;
-                            searchSlashBacwardsFrom = previousSlashIdx - 1; 
-                        } else {
-                            break scanBackwardsForSlash;
-                        }
-                    }
-                }
-                
-                // Note: previousSlashIdx is possibly -1
-                // Removed part in {}: "a/{b/*/../}c" or "a/{b/*/..}"
-                path = path.substring(0, previousSlashIdx + 1)
-                        + (skippedStarStep ? "*/" : "")
-                        + path.substring(dotDotIdx + (slashRight ? 3 : 2));
-                nextFromIdx = previousSlashIdx + 1;
-            }
-        }
-    
-        private String removeRedundantStarSteps(String path) {
-            String prevName;
-            removeDoubleStarSteps: do {
-                int supiciousIdx = path.indexOf("*/*");
-                if (supiciousIdx == -1) {
-                    break removeDoubleStarSteps;
-                }
-        
-                prevName = path;
-                
-                // Is it delimited on both sided by "/" or by the string 
boundaires? 
-                if ((supiciousIdx == 0 || path.charAt(supiciousIdx - 1) == '/')
-                        && (supiciousIdx + 3 == path.length() || 
path.charAt(supiciousIdx + 3) == '/')) {
-                    path = path.substring(0, supiciousIdx) + 
path.substring(supiciousIdx + 2); 
-                }
-            } while (prevName != path);
-            
-            // An initial "*" step is redundant:
-            if (path.startsWith("*")) {
-                if (path.length() == 1) {
-                    path = "";
-                } else if (path.charAt(1) == '/') {
-                    path = path.substring(2); 
-                }
-                // else: it's wasn't a "*" step.
-            }
-            
-            return path;
-        }
-        
-        @Override
-        public String toString() {
-            return "TemplateNameFormat.DEFAULT_2_4_0";
-        }
-    }
+    public abstract String normalizeRootBasedName(String name) throws 
MalformedTemplateNameException;
 
-    private static void checkNameHasNoNullCharacter(final String name) throws 
MalformedTemplateNameException {
+    protected void checkNameHasNoNullCharacter(final String name) throws 
MalformedTemplateNameException {
         if (name.indexOf(0) != -1) {
             throw new MalformedTemplateNameException(name,
                     "Null character (\\u0000) in the name; possible attack 
attempt");
         }
     }
     
-    private static MalformedTemplateNameException 
newRootLeavingException(final String name) {
+    protected MalformedTemplateNameException newRootLeavingException(final 
String name) {
         return new MalformedTemplateNameException(name, "Backing out from the 
root directory is not allowed");
     }
     

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java
index 6253354..800b1b8 100644
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/TemplateResolver.java
@@ -25,6 +25,7 @@ import org.apache.freemarker.core.Configuration;
 import org.apache.freemarker.core.Template;
 import org.apache.freemarker.core.TemplateNotFoundException;
 import org.apache.freemarker.core.ast.ParseException;
+import 
org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
 
 /**
  * This class was introduced to allow users to fully implement the template 
lookup, loading and caching logic,
@@ -96,10 +97,10 @@ public abstract class TemplateResolver {
      * <p>
      * This method is thread-safe and can be called while the engine processes 
templates.
      * 
-     * @throw {@link UnsupportedOperationException} If the {@link 
TemplateResolver} implementation doesn't support this
+     * @throws UnsupportedOperationException If the {@link TemplateResolver} 
implementation doesn't support this
      *        operation.
      */
-    public abstract void clearTemplateCache();
+    public abstract void clearTemplateCache() throws 
UnsupportedOperationException;
 
     /**
      * Removes a template from the template cache, hence forcing the 
re-loading of it when it's next time requested;
@@ -112,11 +113,11 @@ public abstract class TemplateResolver {
      * <p>
      * This method is thread-safe and can be called while the engine processes 
templates.
      * 
-     * @throw {@link UnsupportedOperationException} If the {@link 
TemplateResolver} implementation doesn't support this
+     * @throws UnsupportedOperationException If the {@link TemplateResolver} 
implementation doesn't support this
      *        operation.
      */
     public abstract void removeTemplateFromCache(String name, Locale locale, 
String encoding, boolean parse)
-            throws IOException;
+            throws IOException, UnsupportedOperationException;;
 
     /**
      * Converts a name to a template root directory based name, so that it can 
be used to find a template without

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/URLTemplateLoader.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/URLTemplateLoader.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/URLTemplateLoader.java
deleted file mode 100644
index 70cb733..0000000
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/URLTemplateLoader.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.freemarker.core.templateresolver;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Serializable;
-import java.net.JarURLConnection;
-import java.net.URL;
-import java.net.URLConnection;
-import java.util.Objects;
-
-import org.apache.freemarker.core.Configuration;
-import org.apache.freemarker.core._CoreLogs;
-import org.slf4j.Logger;
-
-/**
- * This is an abstract template loader that can load templates whose location 
can be described by an URL. Subclasses
- * only need to override the {@link #getURL(String)}, {@link 
#extractNegativeResult(URLConnection)}, and perhaps the
- * {@link #prepareConnection(URLConnection)} method.
- */
-// TODO JUnit test (including implementing a HTTP-based template loader to 
test the new protected methods)
-public abstract class URLTemplateLoader implements TemplateLoader {
-    
-    private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER;
-    
-    private Boolean urlConnectionUsesCaches = false;
-    
-    /**
-     * Getter pair of {@link #setURLConnectionUsesCaches(Boolean)}.
-     * 
-     * @since 2.3.21
-     */
-    public Boolean getURLConnectionUsesCaches() {
-        return urlConnectionUsesCaches;
-    }
-
-    /**
-     * Sets if {@link URLConnection#setUseCaches(boolean)} will be called, and 
with what value. By default this is
-     * {@code false}, because FreeMarker has its own template cache with its 
own update delay setting
-     * ({@link Configuration#setTemplateUpdateDelay(int)}). If this is set to 
{@code null},
-     * {@link URLConnection#setUseCaches(boolean)} won't be called.
-     */
-    public void setURLConnectionUsesCaches(Boolean urlConnectionUsesCaches) {
-        this.urlConnectionUsesCaches = urlConnectionUsesCaches;
-    }
-
-    @Override
-    public TemplateLoaderSession createSession() {
-        return null;
-    }
-
-    @Override
-    public TemplateLoadingResult load(String name, TemplateLoadingSource 
ifSourceDiffersFrom,
-            Serializable ifVersionDiffersFrom, TemplateLoaderSession session) 
throws IOException {
-        URL url = getURL(name);
-        if (url == null) {
-            return TemplateLoadingResult.NOT_FOUND;             
-        }
-        
-        URLConnection conn = url.openConnection();
-        Boolean urlConnectionUsesCaches = getURLConnectionUsesCaches();
-        if (urlConnectionUsesCaches != null) {
-            conn.setUseCaches(urlConnectionUsesCaches);
-        }
-        
-        prepareConnection(conn);
-        conn.connect();
-        
-        InputStream inputStream = null;
-        Long version;
-        URLTemplateLoadingSource source;
-        try {
-            TemplateLoadingResult negativeResult = extractNegativeResult(conn);
-            if (negativeResult != null) {
-                return negativeResult;
-            }
-            
-            // To prevent clustering issues, 
getLastModified(fallbackToJarLMD=false)
-            long lmd = getLastModified(conn, false);
-            version = lmd != -1 ? lmd : null;
-            
-            source = new URLTemplateLoadingSource(url);
-            
-            if (ifSourceDiffersFrom != null && 
ifSourceDiffersFrom.equals(source)
-                    && Objects.equals(ifVersionDiffersFrom, version)) {
-                return TemplateLoadingResult.NOT_MODIFIED;
-            }
-            
-            inputStream = conn.getInputStream();
-        } catch (Throwable e) {
-            try {
-                if (inputStream == null) {
-                    // There's no URLConnection.close(), so we do this hack. 
In case of HttpURLConnection we could call
-                    // disconnect(), but that's perhaps too aggressive.
-                    conn.getInputStream().close();
-                }
-            } catch (IOException e2) {
-                LOG.debug("Failed to close connection inputStream", e2);
-            }
-            throw e;
-        }
-        return new TemplateLoadingResult(source, version, inputStream, null);
-    }
-
-    @Override
-    public void resetState() {
-        // Do nothing
-    }
-
-    /**
-     * {@link URLConnection#getLastModified()} with JDK bug workarounds. 
Because of JDK-6956385, for files inside a jar,
-     * it returns the last modification time of the jar itself, rather than 
the last modification time of the file
-     * inside the jar.
-     * 
-     * @param fallbackToJarLMD
-     *            Tells if the file is in side jar, then we should return the 
last modification time of the jar itself,
-     *            or -1 (to work around JDK-6956385).
-     */
-    public static long getLastModified(URLConnection conn, boolean 
fallbackToJarLMD) throws IOException {
-        if (conn instanceof JarURLConnection) {
-            // There is a bug in sun's jar url connection that causes file 
handle leaks when calling getLastModified()
-            // (see https://bugs.openjdk.java.net/browse/JDK-6956385).
-            // Since the time stamps of jar file contents can't vary 
independent from the jar file timestamp, just use
-            // the jar file timestamp
-            if (fallbackToJarLMD) {
-                URL jarURL = ((JarURLConnection) conn).getJarFileURL();
-                if (jarURL.getProtocol().equals("file")) {
-                    // Return the last modified time of the underlying file - 
saves some opening and closing
-                    return new File(jarURL.getFile()).lastModified();
-                } else {
-                    // Use the URL mechanism
-                    URLConnection jarConn = null;
-                    try {
-                        jarConn = jarURL.openConnection();
-                        return jarConn.getLastModified();
-                    } finally {
-                        try {
-                            if (jarConn != null) {
-                                jarConn.getInputStream().close();
-                            }
-                        } catch (IOException e) {
-                            LOG.warn("Failed to close URL connection for: {}", 
conn, e);
-                        }
-                    }
-                }
-            } else {
-                return -1;
-            }
-        } else {
-          return conn.getLastModified();
-        }
-    }
-
-    /**
-     * Given a template name (plus potential locale decorations) retrieves an 
URL that points the template source.
-     * 
-     * @param name
-     *            the name of the sought template (including the locale 
decorations, or other decorations the
-     *            {@link TemplateLookupStrategy} uses).
-     *            
-     * @return An URL that points to the template source, or null if it can be 
determined that the template source does
-     *         not exist. For many implementations the existence of the 
template can't be decided at this point, and you
-     *         rely on {@link #extractNegativeResult(URLConnection)} instead.
-     */
-    protected abstract URL getURL(String name);
-
-    /**
-     * Called before the resource if read, checks if we can immediately return 
a {@link TemplateLoadingResult#NOT_FOUND}
-     * or {@link TemplateLoadingResult#NOT_MODIFIED}, or throw an {@link 
IOException}. For example, for a HTTP-based
-     * storage, the HTTP response status 404 could result in {@link 
TemplateLoadingResult#NOT_FOUND}, 304 in
-     * {@link TemplateLoadingResult#NOT_MODIFIED}, and some others, like 500 
in throwing an {@link IOException}.
-     * 
-     * <p>Some
-     * implementations rely on {@link #getURL(String)} returning {@code null} 
when a template is missing, in which case
-     * this method is certainly not applicable.
-     */
-    protected abstract TemplateLoadingResult 
extractNegativeResult(URLConnection conn) throws IOException;
-
-    /**
-     * Called before anything that causes the connection to actually build up. 
This is where
-     * {@link URLConnection#setIfModifiedSince(long)} and such can be called 
if someone overrides this.
-     * The default implementation in {@link URLTemplateLoader} does nothing. 
-     */
-    protected void prepareConnection(URLConnection conn) {
-        // Does nothing
-    }
-
-    /**
-     * Can be used by subclasses to canonicalize URL path prefixes.
-     * @param prefix the path prefix to canonicalize
-     * @return the canonicalized prefix. All backslashes are replaced with
-     * forward slashes, and a trailing slash is appended if the original
-     * prefix wasn't empty and didn't already end with a slash.
-     */
-    protected static String canonicalizePrefix(String prefix) {
-        // make it foolproof
-        prefix = prefix.replace('\\', '/');
-        // ensure there's a trailing slash
-        if (prefix.length() > 0 && !prefix.endsWith("/")) {
-            prefix += "/";
-        }
-        return prefix;
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/URLTemplateLoadingSource.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/URLTemplateLoadingSource.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/URLTemplateLoadingSource.java
deleted file mode 100644
index 99fbc25..0000000
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/URLTemplateLoadingSource.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.freemarker.core.templateresolver;
-
-import java.net.URL;
-
-import org.apache.freemarker.core.util.NullArgumentException;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-@SuppressWarnings("serial")
-public class URLTemplateLoadingSource implements TemplateLoadingSource {
-
-    private final URL url;
-
-    public URLTemplateLoadingSource(URL url) {
-        NullArgumentException.check("url", url);
-        this.url = url;
-    }
-
-    public URL getUrl() {
-        return url;
-    }
-
-    @Override
-    public int hashCode() {
-        return url.hashCode();
-    }
-
-    @Override
-    @SuppressFBWarnings("EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS")
-    public boolean equals(Object obj) {
-        return url.equals(obj);
-    }
-
-    @Override
-    public String toString() {
-        return url.toString();
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/_TemplateLoaderUtils.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/_TemplateLoaderUtils.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/_TemplateLoaderUtils.java
deleted file mode 100644
index 9c7e8a8..0000000
--- 
a/src/main/java/org/apache/freemarker/core/templateresolver/_TemplateLoaderUtils.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.freemarker.core.templateresolver;
-
-import org.apache.freemarker.core.Configuration;
-
-/**
- * For internal use only; don't depend on this, there's no backward 
compatibility guarantee at all!
- * This class is to work around the lack of module system in Java, i.e., so 
that other FreeMarker packages can
- * access things inside this package that users shouldn't. 
- */ 
-public final class _TemplateLoaderUtils {
-
-    private _TemplateLoaderUtils() {
-        // Not meant to be instantiated
-    }
-
-    public static String getClassNameForToString(TemplateLoader 
templateLoader) {
-        final Class<? extends TemplateLoader> tlClass = 
templateLoader.getClass();
-        final Package tlPackage = tlClass.getPackage();
-        return tlPackage == Configuration.class.getPackage() || tlPackage == 
TemplateLoader.class.getPackage()
-                ? tlClass.getSimpleName() : tlClass.getName();
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java
new file mode 100644
index 0000000..f77de7c
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/ByteArrayTemplateLoader.java
@@ -0,0 +1,199 @@
+/*
+ * 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.impl;
+
+import java.io.ByteArrayInputStream;
+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 org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoaderSession;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.util.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.
+ * 
+ * <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 static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
+    
+    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 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[] 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.
+     * 
+     * <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;
+    }
+    
+    @Override
+    public TemplateLoaderSession createSession() {
+        return null;
+    }
+
+    @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.
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(_TemplateLoaderUtils.getClassNameForToString(this));
+        sb.append("(Map { ");
+        int cnt = 0;
+        for (String name : templates.keySet()) {
+            cnt++;
+            if (cnt != 1) {
+                sb.append(", ");
+            }
+            if (cnt > 10) {
+                sb.append("...");
+                break;
+            }
+            sb.append(StringUtil.jQuote(name));
+            sb.append("=...");
+        }
+        if (cnt != 0) {
+            sb.append(' ');
+        }
+        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/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java
new file mode 100644
index 0000000..9cd8ead
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/ClassTemplateLoader.java
@@ -0,0 +1,184 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingResult;
+import org.apache.freemarker.core.util.NullArgumentException;
+import org.apache.freemarker.core.util.StringUtil;
+
+/**
+ * A {@link TemplateLoader} that can load templates from the "classpath". 
Naturally, it can load from jar files, or from
+ * 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 ClassLoader classLoader;
+    private final String basePackagePath;
+
+    /**
+     * 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).
+     *
+     * <p>
+     * Examples:
+     * <ul>
+     * <li>Relative base path (will load from the {@code 
com.example.myapplication.templates} package):<br>
+     * {@code new 
ClassTemplateLoader(com.example.myapplication.SomeClass.class, "templates")}
+     * <li>Absolute base path:<br>
+     * {@code new ClassTemplateLoader(somepackage.SomeClass.class, 
"/com/example/myapplication/templates")}
+     * </ul>
+     *
+     * @param resourceLoaderClass
+     *            The class whose {@link Class#getResource(String)} method 
will be used to load the templates. Be sure
+     *            that you chose a class whose defining class-loader sees the 
templates. This parameter can't be
+     *            {@code null}.
+     * @param basePackagePath
+     *            The package that contains the templates, in path ({@code 
/}-separated) format. If it doesn't start
+     *            with a {@code /} then it's relative to the path (package) of 
the {@code resourceLoaderClass} class. If
+     *            it starts with {@code /} then it's relative to the root of 
the package hierarchy. Note that path
+     *            components should be separated by forward slashes 
independently of the separator character used by the
+     *            underlying operating system. This parameter can't be {@code 
null}.
+     * 
+     * @see #ClassTemplateLoader(ClassLoader, String)
+     */
+    public ClassTemplateLoader(Class<?> resourceLoaderClass, String 
basePackagePath) {
+        this(resourceLoaderClass, false, null, basePackagePath);
+    }
+
+    /**
+     * Similar to {@link #ClassTemplateLoader(Class, String)}, but instead of 
{@link Class#getResource(String)} it uses
+     * {@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
+     */
+    public ClassTemplateLoader(ClassLoader classLoader, String 
basePackagePath) {
+        this(null, true, classLoader, basePackagePath);
+    }
+
+    private ClassTemplateLoader(Class<?> resourceLoaderClass, boolean 
allowNullResourceLoaderClass,
+            ClassLoader classLoader, String basePackagePath) {
+        if (!allowNullResourceLoaderClass) {
+            NullArgumentException.check("resourceLoaderClass", 
resourceLoaderClass);
+        }
+        NullArgumentException.check("basePackagePath", basePackagePath);
+
+        // Either set a non-null resourceLoaderClass or a non-null 
classLoader, not both:
+        this.resourceLoaderClass = classLoader == null ? (resourceLoaderClass 
== null ? this.getClass()
+                : resourceLoaderClass) : null;
+        if (this.resourceLoaderClass == null && classLoader == null) {
+            throw new NullArgumentException("classLoader");
+        }
+        this.classLoader = classLoader;
+
+        String canonBasePackagePath = canonicalizePrefix(basePackagePath);
+        if (this.classLoader != null && canonBasePackagePath.startsWith("/")) {
+            canonBasePackagePath = canonBasePackagePath.substring(1);
+        }
+        this.basePackagePath = canonBasePackagePath;
+    }
+
+    private static boolean isSchemeless(String fullPath) {
+        int i = 0;
+        int ln = fullPath.length();
+
+        // Skip a single initial /, as things like "/file:/..." might work:
+        if (i < ln && fullPath.charAt(i) == '/') i++;
+
+        // Check if there's no ":" earlier than a '/', as the URLClassLoader
+        // could interpret that as an URL scheme:
+        while (i < ln) {
+            char c = fullPath.charAt(i);
+            if (c == '/') return true;
+            if (c == ':') return false;
+            i++;
+        }
+        return true;
+    }
+
+    /**
+     * Show class name and some details that are useful in template-not-found 
errors.
+     */
+    @Override
+    public String toString() {
+        return _TemplateLoaderUtils.getClassNameForToString(this) + "("
+                + (resourceLoaderClass != null
+                        ? "resourceLoaderClass=" + 
resourceLoaderClass.getName()
+                        : "classLoader=" + StringUtil.jQuote(classLoader))
+                + ", basePackagePath"
+                + "="
+                + StringUtil.jQuote(basePackagePath)
+                + (resourceLoaderClass != null
+                        ? (basePackagePath.startsWith("/") ? "" : " /* 
relatively to resourceLoaderClass pkg */")
+                        : ""
+                )
+                + ")";
+    }
+
+    /**
+     * See the similar parameter of {@link #ClassTemplateLoader(Class, 
String)}; {@code null} when other mechanism is
+     * used to load the resources.
+     */
+    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.
+     */
+    public ClassLoader getClassLoader() {
+        return classLoader;
+    }
+
+    /**
+     * See the similar parameter of {@link #ClassTemplateLoader(ClassLoader, 
String)}; note that this is a normalized
+     * version of what was actually passed to the constructor.
+     */
+    public String getBasePackagePath() {
+        return basePackagePath;
+    }
+
+    @Override
+    protected URL getURL(String name) {
+        String fullPath = basePackagePath + name;
+    
+        // Block java.net.URLClassLoader exploits:
+        if (basePackagePath.equals("/") && !isSchemeless(fullPath)) {
+            return null;
+        }
+    
+        return resourceLoaderClass != null ? 
resourceLoaderClass.getResource(fullPath) : classLoader
+                .getResource(fullPath);
+    }
+
+    @Override
+    protected TemplateLoadingResult extractNegativeResult(URLConnection conn) 
throws IOException {
+        return null;
+    }
+
+}
\ No newline at end of file

Reply via email to