freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [15/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th
Date Sun, 14 May 2017 10:52:58 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java
new file mode 100644
index 0000000..ea1cc63
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java
@@ -0,0 +1,904 @@
+/*
+ * 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.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateConfiguration;
+import org.apache.freemarker.core.TemplateLanguage;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core.WrongTemplateCharsetException;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.GetTemplateResult;
+import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactoryException;
+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.TemplateLoadingResultStatus;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.templateresolver.TemplateResolver;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.util._StringUtil;
+import org.slf4j.Logger;
+
+/**
+ * Performs caching and on-demand loading of the templates.
+ * The actual template "file" loading is delegated to a {@link TemplateLoader} that you can specify in the constructor.
+ * Some aspects of caching is delegated to a {@link CacheStorage} that you can also specify in the constructor.
+ * 
+ * <p>Typically you don't instantiate or otherwise use this class directly. By default the {@link Configuration} embeds
+ * an instance of this class, that you access indirectly through {@link Configuration#getTemplate(String)} and other
+ * {@link Configuration} API-s. When you set the {@link Configuration#getTemplateLoader() templateLoader} or
+ * {@link Configuration#getCacheStorage() cacheStorage} of the {@link Configuration}, you indirectly configure the
+ * {@link TemplateResolver}.
+ */
+public class DefaultTemplateResolver extends TemplateResolver {
+    
+    /**
+     * The default template update delay; see {@link Configuration#getTemplateUpdateDelayMilliseconds()}.
+     * 
+     * @since 2.3.23
+     */
+    public static final long DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS = 5000L;
+    
+    private static final String ASTERISKSTR = "*";
+    private static final char ASTERISK = '*';
+    private static final char SLASH = '/';
+    private static final String LOCALE_PART_SEPARATOR = "_";
+    private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER;
+
+    /** Maybe {@code null}. */
+    private final TemplateLoader templateLoader;
+    
+    /** Here we keep our cached templates */
+    private final CacheStorage cacheStorage;
+    private final TemplateLookupStrategy templateLookupStrategy;
+    private final TemplateNameFormat templateNameFormat;
+    private final TemplateConfigurationFactory templateConfigurations;
+    private final long templateUpdateDelayMilliseconds;
+    private final boolean localizedLookup;
+
+    private Configuration config;
+    
+    /**
+     * @param templateLoader
+     *            The {@link TemplateLoader} to use. Can be {@code null}, though then every request will result in
+     *            {@link TemplateNotFoundException}.
+     * @param cacheStorage
+     *            The {@link CacheStorage} to use. Can't be {@code null}.
+     * @param templateLookupStrategy
+     *            The {@link TemplateLookupStrategy} to use. Can't be {@code null}.
+     * @param templateUpdateDelayMilliseconds
+     *            See {@link Configuration#getTemplateUpdateDelayMilliseconds()}
+     * @param templateNameFormat
+     *            The {@link TemplateNameFormat} to use. Can't be {@code null}.
+     * @param templateConfigurations
+     *            The {@link TemplateConfigurationFactory} to use. Can be {@code null} (then all templates will use the
+     *            settings coming from the {@link Configuration} as is, except in the very rare case where a
+     *            {@link TemplateLoader} itself specifies a {@link TemplateConfiguration}).
+     * @param config
+     *            The {@link Configuration} this cache will be used for. Can't be {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    public DefaultTemplateResolver(
+            TemplateLoader templateLoader,
+            CacheStorage cacheStorage, long templateUpdateDelayMilliseconds,
+            TemplateLookupStrategy templateLookupStrategy, boolean localizedLookup,
+            TemplateNameFormat templateNameFormat,
+            TemplateConfigurationFactory templateConfigurations,
+            Configuration config) {
+        super(config);
+        
+        this.templateLoader = templateLoader;
+        
+        _NullArgumentException.check("cacheStorage", cacheStorage);
+        this.cacheStorage = cacheStorage;
+        
+        this.templateUpdateDelayMilliseconds = templateUpdateDelayMilliseconds;
+        
+        this.localizedLookup = localizedLookup;
+        
+        _NullArgumentException.check("templateLookupStrategy", templateLookupStrategy);
+        this.templateLookupStrategy = templateLookupStrategy;
+
+        _NullArgumentException.check("templateNameFormat", templateNameFormat);
+        this.templateNameFormat = templateNameFormat;
+
+        // Can be null
+        this.templateConfigurations = templateConfigurations;
+        
+        _NullArgumentException.check("config", config);
+        this.config = config;
+    }
+    
+    /**
+     * Returns the configuration for internal usage.
+     */
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    public TemplateLoader getTemplateLoader() {
+        return templateLoader;
+    }
+
+    public CacheStorage getCacheStorage() {
+        return cacheStorage;
+    }
+    
+    /**
+     * @since 2.3.22
+     */
+    public TemplateLookupStrategy getTemplateLookupStrategy() {
+        return templateLookupStrategy;
+    }
+    
+    /**
+     * @since 2.3.22
+     */
+    public TemplateNameFormat getTemplateNameFormat() {
+        return templateNameFormat;
+    }
+    
+    /**
+     * @since 2.3.24
+     */
+    public TemplateConfigurationFactory getTemplateConfigurations() {
+        return templateConfigurations;
+    }
+
+    /**
+     * Retrieves the template with the given name (and according the specified further parameters) from the template
+     * cache, loading it into the cache first if it's missing/staled.
+     * 
+     * <p>
+     * All parameters must be non-{@code null}, except {@code customLookupCondition}. For the meaning of the parameters
+     * see {@link Configuration#getTemplate(String, Locale, Serializable, boolean)}.
+     *
+     * @return A {@link GetTemplateResult} object that contains the {@link Template}, or a
+     *         {@link GetTemplateResult} object that contains {@code null} as the {@link Template} and information
+     *         about the missing template. The return value itself is never {@code null}. Note that exceptions occurring
+     *         during template loading will not be classified as a missing template, so they will cause an exception to
+     *         be thrown by this method instead of returning a {@link GetTemplateResult}. The idea is that having a
+     *         missing template is normal (not exceptional), providing that the backing storage mechanism could indeed
+     *         check that it's missing.
+     * 
+     * @throws MalformedTemplateNameException
+     *             If the {@code name} was malformed according the current {@link TemplateNameFormat}. However, if the
+     *             {@link TemplateNameFormat} is {@link DefaultTemplateNameFormatFM2#INSTANCE} and
+     *             {@link Configuration#getIncompatibleImprovements()} is less than 2.4.0, then instead of throwing this
+     *             exception, a {@link GetTemplateResult} will be returned, similarly as if the template were missing
+     *             (the {@link GetTemplateResult#getMissingTemplateReason()} will describe the real error).
+     * 
+     * @throws IOException
+     *             If reading the template has failed from a reason other than the template is missing. This method
+     *             should never be a {@link TemplateNotFoundException}, as that condition is indicated in the return
+     *             value.
+     * 
+     * @since 2.3.22
+     */
+    @Override
+    public GetTemplateResult getTemplate(String name, Locale locale, Serializable customLookupCondition)
+    throws IOException {
+        _NullArgumentException.check("name", name);
+        _NullArgumentException.check("locale", locale);
+
+        name = templateNameFormat.normalizeRootBasedName(name);
+        
+        if (templateLoader == null) {
+            return new GetTemplateResult(name, "The TemplateLoader (and TemplateLoader2) was null.");
+        }
+        
+        Template template = getTemplateInternal(name, locale, customLookupCondition);
+        return template != null ? new GetTemplateResult(template) : new GetTemplateResult(name, (String) null);
+    }
+
+    @Override
+    public String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException {
+        return templateNameFormat.toRootBasedName(baseName, targetName);
+    }
+
+    @Override
+    public String normalizeRootBasedName(String name) throws MalformedTemplateNameException {
+        return templateNameFormat.normalizeRootBasedName(name);
+    }
+
+    private Template getTemplateInternal(
+            final String name, final Locale locale, final Serializable customLookupCondition)
+    throws IOException {
+        final boolean debug = LOG.isDebugEnabled();
+        final String debugPrefix = debug
+                ? getDebugPrefix("getTemplate", name, locale, customLookupCondition)
+                : null;
+        final CachedResultKey cacheKey = new CachedResultKey(name, locale, customLookupCondition);
+        
+        CachedResult oldCachedResult = (CachedResult) cacheStorage.get(cacheKey);
+        
+        final long now = System.currentTimeMillis();
+        
+        boolean rethrownCachedException = false;
+        boolean suppressFinallyException = false;
+        TemplateLoaderBasedTemplateLookupResult newLookupResult = null;
+        CachedResult newCachedResult = null;
+        TemplateLoaderSession session = null;
+        try {
+            if (oldCachedResult != null) {
+                // If we're within the refresh delay, return the cached result
+                if (now - oldCachedResult.lastChecked < templateUpdateDelayMilliseconds) {
+                    if (debug) {
+                        LOG.debug(debugPrefix + "Cached copy not yet stale; using cached.");
+                    }
+                    Object t = oldCachedResult.templateOrException;
+                    // t can be null, indicating a cached negative lookup
+                    if (t instanceof Template || t == null) {
+                        return (Template) t;
+                    } else if (t instanceof RuntimeException) {
+                        rethrowCachedException((RuntimeException) t);
+                    } else if (t instanceof IOException) {
+                        rethrownCachedException = true;
+                        rethrowCachedException((IOException) t);
+                    }
+                    throw new BugException("Unhandled class for t: " + t.getClass().getName());
+                }
+                // The freshness of the cache result must be checked.
+                
+                // Clone, as the instance in the cache store must not be modified to ensure proper concurrent behavior.
+                newCachedResult = oldCachedResult.clone();
+                newCachedResult.lastChecked = now;
+
+                session = templateLoader.createSession();
+                if (debug && session != null) {
+                    LOG.debug(debugPrefix + "Session created.");
+                }
+                
+                // Find the template source, load it if it doesn't correspond to the cached result.
+                newLookupResult = lookupAndLoadTemplateIfChanged(
+                        name, locale, customLookupCondition, oldCachedResult.source, oldCachedResult.version, session);
+
+                // Template source was removed (TemplateLoader2ResultStatus.NOT_FOUND, or no TemplateLoader2Result)
+                if (!newLookupResult.isPositive()) { 
+                    if (debug) {
+                        LOG.debug(debugPrefix + "No source found.");
+                    } 
+                    setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null);
+                    return null;
+                }
+
+                final TemplateLoadingResult newTemplateLoaderResult = newLookupResult.getTemplateLoaderResult();
+                if (newTemplateLoaderResult.getStatus() == TemplateLoadingResultStatus.NOT_MODIFIED) {
+                    // Return the cached version.
+                    if (debug) {
+                        LOG.debug(debugPrefix + ": Using cached template "
+                                + "(source: " + newTemplateLoaderResult.getSource() + ")"
+                                + " as it hasn't been changed on the backing store.");
+                    }
+                    cacheStorage.put(cacheKey, newCachedResult);
+                    return (Template) newCachedResult.templateOrException;
+                } else {
+                    if (newTemplateLoaderResult.getStatus() != TemplateLoadingResultStatus.OPENED) {
+                        // TemplateLoader2ResultStatus.NOT_FOUND was already handler earlier
+                        throw new BugException("Unxpected status: " + newTemplateLoaderResult.getStatus());
+                    }
+                    if (debug) {
+                        StringBuilder debugMsg = new StringBuilder();
+                        debugMsg.append(debugPrefix)
+                                .append("Reloading template instead of using the cached result because ");
+                        if (newCachedResult.templateOrException instanceof Throwable) {
+                            debugMsg.append("it's a cached error (retrying).");
+                        } else {
+                            Object newSource = newTemplateLoaderResult.getSource();
+                            if (!nullSafeEquals(newSource, oldCachedResult.source)) {
+                                debugMsg.append("the source has been changed: ")
+                                        .append("cached.source=").append(_StringUtil.jQuoteNoXSS(oldCachedResult.source))
+                                        .append(", current.source=").append(_StringUtil.jQuoteNoXSS(newSource));
+                            } else {
+                                Serializable newVersion = newTemplateLoaderResult.getVersion();
+                                if (!nullSafeEquals(oldCachedResult.version, newVersion)) {
+                                    debugMsg.append("the version has been changed: ")
+                                            .append("cached.version=").append(oldCachedResult.version) 
+                                            .append(", current.version=").append(newVersion);
+                                } else {
+                                    debugMsg.append("??? (unknown reason)");
+                                }
+                            }
+                        }
+                        LOG.debug(debugMsg.toString());
+                    }
+                }
+            } else { // if there was no cached result
+                if (debug) {
+                    LOG.debug(debugPrefix + "No cached result was found; will try to load template.");
+                }
+                
+                newCachedResult = new CachedResult();
+                newCachedResult.lastChecked = now;
+            
+                session = templateLoader.createSession();
+                if (debug && session != null) {
+                    LOG.debug(debugPrefix + "Session created.");
+                } 
+                
+                newLookupResult = lookupAndLoadTemplateIfChanged(
+                        name, locale, customLookupCondition, null, null, session);
+                
+                if (!newLookupResult.isPositive()) {
+                    setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null);
+                    return null;
+                }
+            }
+            // We have newCachedResult and newLookupResult initialized at this point.
+
+            TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult();
+            newCachedResult.source = templateLoaderResult.getSource();
+            
+            // If we get here, then we need to (re)load the template
+            if (debug) {
+                LOG.debug(debugPrefix + "Reading template content (source: "
+                        + _StringUtil.jQuoteNoXSS(newCachedResult.source) + ")");
+            }
+            
+            Template template = loadTemplate(
+                    templateLoaderResult,
+                    name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition);
+            if (session != null) {
+                session.close();
+                if (debug) {
+                    LOG.debug(debugPrefix + "Session closed.");
+                } 
+            }
+            newCachedResult.templateOrException = template;
+            newCachedResult.version = templateLoaderResult.getVersion();
+            cacheStorage.put(cacheKey, newCachedResult);
+            return template;
+        } catch (RuntimeException e) {
+            if (newCachedResult != null) {
+                setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e);
+            }
+            suppressFinallyException = true;
+            throw e;
+        } catch (IOException e) {
+            // Rethrown cached exceptions are wrapped into IOException-s, so we only need this condition here.
+            if (!rethrownCachedException) {
+                setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e);
+            }
+            suppressFinallyException = true;
+            throw e;
+        } finally {
+            try {
+                // Close streams first:
+                
+                if (newLookupResult != null && newLookupResult.isPositive()) {
+                    TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult();
+                    Reader reader = templateLoaderResult.getReader();
+                    if (reader != null) {
+                        try {
+                            reader.close();
+                        } catch (IOException e) { // [FM3] Exception e
+                            if (suppressFinallyException) {
+                                if (LOG.isWarnEnabled()) { 
+                                    LOG.warn("Failed to close template content Reader for: " + name, e);
+                                }
+                            } else {
+                                suppressFinallyException = true;
+                                throw e;
+                            }
+                        }
+                    } else if (templateLoaderResult.getInputStream() != null) {
+                        try {
+                            templateLoaderResult.getInputStream().close();
+                        } catch (IOException e) { // [FM3] Exception e
+                            if (suppressFinallyException) {
+                                if (LOG.isWarnEnabled()) { 
+                                    LOG.warn("Failed to close template content InputStream for: " + name, e);
+                                }
+                            } else {
+                                suppressFinallyException = true;
+                                throw e;
+                            }
+                        }
+                    }
+                }
+            } finally {
+                // Then close streams:
+                
+                if (session != null && !session.isClosed()) {
+                    try {
+                        session.close();
+                        if (debug) {
+                            LOG.debug(debugPrefix + "Session closed.");
+                        } 
+                    } catch (IOException e) { // [FM3] Exception e
+                        if (suppressFinallyException) {
+                            if (LOG.isWarnEnabled()) { 
+                                LOG.warn("Failed to close template loader session for" + name, e);
+                            }
+                        } else {
+                            suppressFinallyException = true;
+                            throw e;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    
+    
+    private static final Method INIT_CAUSE = getInitCauseMethod();
+    
+    private static Method getInitCauseMethod() {
+        try {
+            return Throwable.class.getMethod("initCause", Throwable.class);
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+    
+    /**
+     * Creates an {@link IOException} that has a cause exception.
+     */
+    // [Java 6] Remove
+    private IOException newIOException(String message, Throwable cause) {
+        if (cause == null) {
+            return new IOException(message);
+        }
+        
+        IOException ioe;
+        if (INIT_CAUSE != null) {
+            ioe = new IOException(message);
+            try {
+                INIT_CAUSE.invoke(ioe, cause);
+            } catch (RuntimeException ex) {
+                throw ex;
+            } catch (Exception ex) {
+                throw new UndeclaredThrowableException(ex);
+            }
+        } else {
+            ioe = new IOException(message + "\nCaused by: " + cause.getClass().getName() + 
+            ": " + cause.getMessage());
+        }
+        return ioe;
+    }
+    
+    private void rethrowCachedException(Throwable e) throws IOException {
+        throw newIOException("There was an error loading the " +
+                "template on an earlier attempt; see cause exception.", e);
+    }
+
+    private void setToNegativeAndPutIntoCache(CachedResultKey cacheKey, CachedResult cachedResult, Exception e) {
+        cachedResult.templateOrException = e;
+        cachedResult.source = null;
+        cachedResult.version = null;
+        cacheStorage.put(cacheKey, cachedResult);
+    }
+
+    private Template loadTemplate(
+            TemplateLoadingResult templateLoaderResult,
+            final String name, final String sourceName, Locale locale, final Serializable customLookupCondition)
+            throws IOException {
+        TemplateConfiguration tc;
+        {
+            TemplateConfiguration cfgTC;
+            try {
+                cfgTC = templateConfigurations != null
+                        ? templateConfigurations.get(sourceName, templateLoaderResult.getSource()) : null;
+            } catch (TemplateConfigurationFactoryException e) {
+                throw newIOException("Error while getting TemplateConfiguration; see cause exception.", e);
+            }
+            TemplateConfiguration templateLoaderResultTC = templateLoaderResult.getTemplateConfiguration();
+            if (templateLoaderResultTC != null) {
+                TemplateConfiguration.Builder mergedTCBuilder = new TemplateConfiguration.Builder();
+                if (cfgTC != null) {
+                    mergedTCBuilder.merge(cfgTC);
+                }
+                mergedTCBuilder.merge(templateLoaderResultTC);
+
+                tc = mergedTCBuilder.build();
+            } else {
+                tc = cfgTC;
+            }
+        }
+
+        if (tc != null && tc.isLocaleSet()) {
+            locale = tc.getLocale();
+        }
+        Charset initialEncoding = tc != null && tc.isSourceEncodingSet() ? tc.getSourceEncoding()
+                : config.getSourceEncoding();
+        TemplateLanguage templateLanguage = tc != null && tc.isTemplateLanguageSet() ? tc.getTemplateLanguage()
+                : config.getTemplateLanguage();
+
+        Template template;
+        {
+            Reader reader = templateLoaderResult.getReader();
+            InputStream inputStream = templateLoaderResult.getInputStream();
+            InputStream markedInputStream;
+            if (reader != null) {
+                if (inputStream != null) {
+                    throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName()
+                            + ", both getReader() and getInputStream() has returned non-null.");
+                }
+                initialEncoding = null;  // No charset decoding has happened
+                markedInputStream = null;
+            } else if (inputStream != null) {
+                if (templateLanguage.getCanSpecifyCharsetInContent()) {
+                    // We need mark support, to restart if the charset suggested by <#ftl encoding=...> differs
+                    // from that we use initially.
+                    if (!inputStream.markSupported()) {
+                        inputStream = new BufferedInputStream(inputStream);
+                    }
+                    inputStream.mark(Integer.MAX_VALUE); // Mark is released after the 1st FTL tag
+                    markedInputStream = inputStream;
+                } else {
+                    markedInputStream = null;
+                }
+                // Regarding buffering worries: On the Reader side we should only read in chunks (like through a
+                // BufferedReader), so there shouldn't be a problem if the InputStream is not buffered. (Also, at least
+                // on Oracle JDK and OpenJDK 7 the InputStreamReader itself has an internal ~8K buffer.)
+                reader = new InputStreamReader(inputStream, initialEncoding);
+            } else {
+                throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName()
+                        + ", both getReader() and getInputStream() has returned null.");
+            }
+            
+            try {
+                try {
+                    template = templateLanguage.parse(name, sourceName, reader, config, tc,
+                            initialEncoding, markedInputStream);
+                } catch (WrongTemplateCharsetException charsetException) {
+                    final Charset templateSpecifiedEncoding = charsetException.getTemplateSpecifiedEncoding();
+
+                    if (inputStream != null) {
+                        // We restart InputStream to re-decode it with the new charset.
+                        inputStream.reset();
+
+                        // Don't close `reader`; it's an InputStreamReader that would close the wrapped InputStream.
+                        reader = new InputStreamReader(inputStream, templateSpecifiedEncoding);
+                    } else {
+                        throw new IllegalStateException(
+                                "TemplateLanguage " + _StringUtil.jQuote(templateLanguage.getName()) + " has thrown "
+                                + WrongTemplateCharsetException.class.getName()
+                                + ", but its canSpecifyCharsetInContent property is false.");
+                    }
+
+                    template = templateLanguage.parse(name, sourceName, reader, config, tc,
+                            templateSpecifiedEncoding, markedInputStream);
+                }
+            } finally {
+                reader.close();
+            }
+        }
+
+        template.setLookupLocale(locale);
+        template.setCustomLookupCondition(customLookupCondition);
+        return template;
+    }
+
+    /**
+     * Gets the delay in milliseconds between checking for newer versions of a
+     * template source.
+     * @return the current value of the delay
+     */
+    public long getTemplateUpdateDelayMilliseconds() {
+        // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
+        synchronized (this) {
+            return templateUpdateDelayMilliseconds;
+        }
+    }
+
+    /**
+     * Returns if localized template lookup is enabled or not.
+     */
+    public boolean getLocalizedLookup() {
+        // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
+        synchronized (this) {
+            return localizedLookup;
+        }
+    }
+
+    /**
+     * Removes all entries from the cache, forcing reloading of templates on subsequent
+     * {@link #getTemplate(String, Locale, Serializable)} calls.
+     * 
+     * @param resetTemplateLoader
+     *            Whether to call {@link TemplateLoader#resetState()}. on the template loader.
+     */
+    public void clearTemplateCache(boolean resetTemplateLoader) {
+        synchronized (cacheStorage) {
+            cacheStorage.clear();
+            if (templateLoader != null && resetTemplateLoader) {
+                templateLoader.resetState();
+            }
+        }
+    }
+    
+    /**
+     * Same as {@link #clearTemplateCache(boolean)} with {@code true} {@code resetTemplateLoader} argument.
+     */
+    @Override
+    public void clearTemplateCache() {
+        synchronized (cacheStorage) {
+            cacheStorage.clear();
+            if (templateLoader != null) {
+                templateLoader.resetState();
+            }
+        }
+    }
+
+    /**
+     * Removes an entry from the cache, hence forcing the re-loading of it when it's next time requested. (It doesn't
+     * delete the template file itself.) This is to give the application finer control over cache updating than the
+     * update delay ({@link #getTemplateUpdateDelayMilliseconds()}) alone does.
+     * 
+     * For the meaning of the parameters, see
+     * {@link Configuration#getTemplate(String, Locale, Serializable, boolean)}
+     */
+    @Override
+    public void removeTemplateFromCache(
+            String name, Locale locale, Serializable customLookupCondition)
+    throws IOException {
+        if (name == null) {
+            throw new IllegalArgumentException("Argument \"name\" can't be null");
+        }
+        if (locale == null) {
+            throw new IllegalArgumentException("Argument \"locale\" can't be null");
+        }
+        name = templateNameFormat.normalizeRootBasedName(name);
+        if (name != null && templateLoader != null) {
+            boolean debug = LOG.isDebugEnabled();
+            String debugPrefix = debug
+                    ? getDebugPrefix("removeTemplate", name, locale, customLookupCondition)
+                    : null;
+            CachedResultKey tk = new CachedResultKey(name, locale, customLookupCondition);
+            
+            cacheStorage.remove(tk);
+            if (debug) {
+                LOG.debug(debugPrefix + "Template was removed from the cache, if it was there");
+            }
+        }
+    }
+
+    private String getDebugPrefix(String operation, String name, Locale locale, Object customLookupCondition) {
+        return operation + " " + _StringUtil.jQuoteNoXSS(name) + "("
+                + _StringUtil.jQuoteNoXSS(locale)
+                + (customLookupCondition != null ? ", cond=" + _StringUtil.jQuoteNoXSS(customLookupCondition) : "")
+                + "): ";
+    }    
+
+    /**
+     * Looks up according the {@link TemplateLookupStrategy} and then starts reading the template, if it was changed
+     * compared to the cached result, or if there was no cached result yet.
+     */
+    private TemplateLoaderBasedTemplateLookupResult lookupAndLoadTemplateIfChanged(
+            String name, Locale locale, Object customLookupCondition,
+            TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion,
+            TemplateLoaderSession session) throws IOException {
+        final TemplateLoaderBasedTemplateLookupResult lookupResult = templateLookupStrategy.lookup(
+                new DefaultTemplateResolverTemplateLookupContext(
+                        name, locale, customLookupCondition,
+                        cachedResultSource, cachedResultVersion,
+                        session));
+        if (lookupResult == null) {
+            throw new NullPointerException("Lookup result shouldn't be null");
+        }
+        return lookupResult;
+    }
+
+    private String concatPath(List<String> pathSteps, int from, int to) {
+        StringBuilder buf = new StringBuilder((to - from) * 16);
+        for (int i = from; i < to; ++i) {
+            buf.append(pathSteps.get(i));
+            if (i < pathSteps.size() - 1) {
+                buf.append('/');
+            }
+        }
+        return buf.toString();
+    }
+    
+    // Replace with Objects.equals in Java 7
+    private static boolean nullSafeEquals(Object o1, Object o2) {
+        if (o1 == o2) return true;
+        if (o1 == null || o2 == null) return false;
+        return o1.equals(o2);
+    }
+    
+    /**
+     * Used as cache key to look up a {@link CachedResult}. 
+     */
+    @SuppressWarnings("serial")
+    private static final class CachedResultKey implements Serializable {
+        private final String name;
+        private final Locale locale;
+        private final Serializable customLookupCondition;
+
+        CachedResultKey(String name, Locale locale, Serializable customLookupCondition) {
+            this.name = name;
+            this.locale = locale;
+            this.customLookupCondition = customLookupCondition;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof CachedResultKey)) {
+                return false;
+            }
+            CachedResultKey tk = (CachedResultKey) o;
+            return
+                    name.equals(tk.name) &&
+                    locale.equals(tk.locale) &&
+                    nullSafeEquals(customLookupCondition, tk.customLookupCondition);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = name.hashCode();
+            result = 31 * result + locale.hashCode();
+            result = 31 * result + (customLookupCondition != null ? customLookupCondition.hashCode() : 0);
+            return result;
+        }
+
+    }
+
+    /**
+     * Hold the a cached {@link #getTemplate(String, Locale, Serializable)} result and the associated
+     * information needed to check if the cached value is up to date.
+     * 
+     * <p>
+     * Note: this class is Serializable to allow custom 3rd party CacheStorage implementations to serialize/replicate
+     * them; FreeMarker code itself doesn't rely on its serializability.
+     * 
+     * @see CachedResultKey
+     */
+    private static final class CachedResult implements Cloneable, Serializable {
+        private static final long serialVersionUID = 1L;
+
+        Object templateOrException;
+        TemplateLoadingSource source;
+        Serializable version;
+        long lastChecked;
+        
+        @Override
+        public CachedResult clone() {
+            try {
+                return (CachedResult) super.clone();
+            } catch (CloneNotSupportedException e) {
+                throw new UndeclaredThrowableException(e);
+            }
+        }
+    }
+    
+    private class DefaultTemplateResolverTemplateLookupContext extends TemplateLoaderBasedTemplateLookupContext {
+
+        private final TemplateLoaderSession session; 
+        
+        DefaultTemplateResolverTemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition,
+                TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion,
+                TemplateLoaderSession session) {
+            super(templateName, localizedLookup ? templateLocale : null, customLookupCondition,
+                    cachedResultSource, cachedResultVersion);
+            this.session = session;
+        }
+
+        @Override
+        public TemplateLoaderBasedTemplateLookupResult lookupWithAcquisitionStrategy(String path) throws IOException {
+            // Only one of the possible ways of making a name non-normalized, but is the easiest mistake to do:
+            if (path.startsWith("/")) {
+                throw new IllegalArgumentException("Non-normalized name, starts with \"/\": " + path);
+            }
+            
+            int asterisk = path.indexOf(ASTERISK);
+            // Shortcut in case there is no acquisition
+            if (asterisk == -1) {
+                return createLookupResult(
+                        path,
+                        templateLoader.load(path, getCachedResultSource(), getCachedResultVersion(), session));
+            }
+            StringTokenizer pathTokenizer = new StringTokenizer(path, "/");
+            int lastAsterisk = -1;
+            List<String> pathSteps = new ArrayList<>();
+            while (pathTokenizer.hasMoreTokens()) {
+                String pathStep = pathTokenizer.nextToken();
+                if (pathStep.equals(ASTERISKSTR)) {
+                    if (lastAsterisk != -1) {
+                        pathSteps.remove(lastAsterisk);
+                    }
+                    lastAsterisk = pathSteps.size();
+                }
+                pathSteps.add(pathStep);
+            }
+            if (lastAsterisk == -1) {  // if there was no real "*" step after all
+                return createLookupResult(
+                        path,
+                        templateLoader.load(path, getCachedResultSource(), getCachedResultVersion(), session));
+            }
+            String basePath = concatPath(pathSteps, 0, lastAsterisk);
+            String postAsteriskPath = concatPath(pathSteps, lastAsterisk + 1, pathSteps.size());
+            StringBuilder buf = new StringBuilder(path.length()).append(basePath);
+            int basePathLen = basePath.length();
+            while (true) {
+                String fullPath = buf.append(postAsteriskPath).toString();
+                TemplateLoadingResult templateLoaderResult = templateLoader.load(
+                        fullPath, getCachedResultSource(), getCachedResultVersion(), session);
+                if (templateLoaderResult.getStatus() == TemplateLoadingResultStatus.OPENED) {
+                    return createLookupResult(fullPath, templateLoaderResult);
+                }
+                if (basePathLen == 0) {
+                    return createNegativeLookupResult();
+                }
+                basePathLen = basePath.lastIndexOf(SLASH, basePathLen - 2) + 1;
+                buf.setLength(basePathLen);
+            }
+        }
+
+        @Override
+        public TemplateLoaderBasedTemplateLookupResult lookupWithLocalizedThenAcquisitionStrategy(final String templateName,
+                final Locale templateLocale) throws IOException {
+            
+                if (templateLocale == null) {
+                    return lookupWithAcquisitionStrategy(templateName);
+                }
+                
+                int lastDot = templateName.lastIndexOf('.');
+                String prefix = lastDot == -1 ? templateName : templateName.substring(0, lastDot);
+                String suffix = lastDot == -1 ? "" : templateName.substring(lastDot);
+                String localeName = LOCALE_PART_SEPARATOR + templateLocale.toString();
+                StringBuilder buf = new StringBuilder(templateName.length() + localeName.length());
+                buf.append(prefix);
+                tryLocaleNameVariations: while (true) {
+                    buf.setLength(prefix.length());
+                    String path = buf.append(localeName).append(suffix).toString();
+                    TemplateLoaderBasedTemplateLookupResult lookupResult = lookupWithAcquisitionStrategy(path);
+                    if (lookupResult.isPositive()) {
+                        return lookupResult;
+                    }
+                    
+                    int lastUnderscore = localeName.lastIndexOf('_');
+                    if (lastUnderscore == -1) {
+                        break tryLocaleNameVariations;
+                    }
+                    localeName = localeName.substring(0, lastUnderscore);
+                }
+                return createNegativeLookupResult();
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
new file mode 100644
index 0000000..e2437c1
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
@@ -0,0 +1,383 @@
+/*
+ * 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.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+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.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core._CoreLogs;
+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._SecurityUtil;
+import org.apache.freemarker.core.util._StringUtil;
+import org.slf4j.Logger;
+
+/**
+ * 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!
+ */
+public class FileTemplateLoader implements TemplateLoader {
+    
+    /**
+     * By setting this Java system property to {@code true}, you can change the default of
+     * {@code #getEmulateCaseSensitiveFileSystem()}.
+     */
+    public static String SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM
+            = "org.freemarker.emulateCaseSensitiveFileSystem";
+    private static final boolean EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT;
+    static {
+        final String s = _SecurityUtil.getSystemProperty(SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM,
+                "false");
+        boolean emuCaseSensFS;
+        try {
+            emuCaseSensFS = _StringUtil.getYesNo(s);
+        } catch (Exception e) {
+            emuCaseSensFS = false;
+        }
+        EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT = emuCaseSensFS;
+    }
+
+    private static final int CASE_CHECH_CACHE_HARD_SIZE = 50;
+    private static final int CASE_CHECK_CACHE__SOFT_SIZE = 1000;
+    private static final boolean SEP_IS_SLASH = File.separatorChar == '/';
+    
+    private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER;
+    
+    public final File baseDir;
+    private final String canonicalBasePath;
+    private boolean emulateCaseSensitiveFileSystem;
+    private MruCacheStorage correctCasePaths;
+
+    /**
+     * 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 
+     * the base directory.
+     * @param baseDir the base directory for loading templates
+     */
+    public FileTemplateLoader(final File baseDir)
+    throws IOException {
+        this(baseDir, false);
+    }
+
+    /**
+     * Creates a new file template loader that will use the specified directory as the base directory for loading
+     * templates. See the parameters for allowing symlinks that point outside the base directory.
+     * 
+     * @param baseDir
+     *            the base directory for loading templates
+     * 
+     * @param disableCanonicalPathCheck
+     *            If {@code true}, it will not check if the file to be loaded is inside the {@code baseDir} or not,
+     *            according the <em>canonical</em> paths of the {@code baseDir} and the file to load. Note that
+     *            {@link Configuration#getTemplate(String)} and (its overloads) already prevents backing out from the
+     *            template directory with paths like {@code /../../../etc/password}, however, that can be circumvented
+     *            with symbolic links or other file system features. If you really want to use symbolic links that point
+     *            outside the {@code baseDir}, set this parameter to {@code true}, but then be very careful with
+     *            template paths that are supplied by the visitor or an external system.
+     */
+    public FileTemplateLoader(final File baseDir, final boolean disableCanonicalPathCheck)
+    throws IOException {
+        try {
+            Object[] retval = AccessController.doPrivileged(new PrivilegedExceptionAction<Object[]>() {
+                @Override
+                public Object[] run() throws IOException {
+                    if (!baseDir.exists()) {
+                        throw new FileNotFoundException(baseDir + " does not exist.");
+                    }
+                    if (!baseDir.isDirectory()) {
+                        throw new IOException(baseDir + " is not a directory.");
+                    }
+                    Object[] retval = new Object[2];
+                    if (disableCanonicalPathCheck) {
+                        retval[0] = baseDir;
+                        retval[1] = null;
+                    } else {
+                        retval[0] = baseDir.getCanonicalFile();
+                        String basePath = ((File) retval[0]).getPath();
+                        // Most canonical paths don't end with File.separator,
+                        // but some does. Like, "C:\" VS "C:\templates".
+                        if (!basePath.endsWith(File.separator)) {
+                            basePath += File.separatorChar;
+                        }
+                        retval[1] = basePath;
+                    }
+                    return retval;
+                }
+            });
+            this.baseDir = (File) retval[0];
+            canonicalBasePath = (String) retval[1];
+            
+            setEmulateCaseSensitiveFileSystem(getEmulateCaseSensitiveFileSystemDefault());
+        } catch (PrivilegedActionException e) {
+            throw (IOException) e.getException();
+        }
+    }
+    
+    private File getFile(final String name) throws IOException {
+        try {
+            return AccessController.doPrivileged(new PrivilegedExceptionAction<File>() {
+                @Override
+                public File run() throws IOException {
+                    File source = new File(baseDir, SEP_IS_SLASH ? name : 
+                        name.replace('/', File.separatorChar));
+                    if (!source.isFile()) {
+                        return null;
+                    }
+                    // Security check for inadvertently returning something 
+                    // outside the template directory when linking is not 
+                    // allowed.
+                    if (canonicalBasePath != null) {
+                        String normalized = source.getCanonicalPath();
+                        if (!normalized.startsWith(canonicalBasePath)) {
+                            throw new SecurityException(source.getAbsolutePath() 
+                                    + " resolves to " + normalized + " which " + 
+                                    " doesn't start with " + canonicalBasePath);
+                        }
+                    }
+                    
+                    if (emulateCaseSensitiveFileSystem && !isNameCaseCorrect(source)) {
+                        return null;
+                    }
+                    
+                    return source;
+                }
+            });
+        } catch (PrivilegedActionException e) {
+            throw (IOException) e.getException();
+        }
+    }
+    
+    private long getLastModified(final File templateSource) {
+        return (AccessController.<Long>doPrivileged(new PrivilegedAction<Long>() {
+            @Override
+            public Long run() {
+                return Long.valueOf((templateSource).lastModified());
+            }
+        })).longValue();
+    }
+    
+    private InputStream getInputStream(final File templateSource)
+    throws IOException {
+        try {
+            return AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>() {
+                @Override
+                public InputStream run() throws IOException {
+                    return new FileInputStream(templateSource);
+                }
+            });
+        } catch (PrivilegedActionException e) {
+            throw (IOException) e.getException();
+        }
+    }
+    
+    /**
+     * Called by {@link #getFile(String)} when {@link #getEmulateCaseSensitiveFileSystem()} is {@code true}.
+     */
+    private boolean isNameCaseCorrect(File source) throws IOException {
+        final String sourcePath = source.getPath();
+        synchronized (correctCasePaths) {
+            if (correctCasePaths.get(sourcePath) != null) {
+                return true;
+            }
+        }
+        
+        final File parentDir = source.getParentFile();
+        if (parentDir != null) {
+            if (!baseDir.equals(parentDir) && !isNameCaseCorrect(parentDir)) {
+                return false;
+            }
+            
+            final String[] listing = parentDir.list();
+            if (listing != null) {
+                final String fileName = source.getName();
+                
+                boolean identicalNameFound = false;
+                for (int i = 0; !identicalNameFound && i < listing.length; i++) {
+                    if (fileName.equals(listing[i])) {
+                        identicalNameFound = true;
+                    }
+                }
+        
+                if (!identicalNameFound) {
+                    // If we find a similarly named file that only differs in case, then this is a file-not-found.
+                    for (final String listingEntry : listing) {
+                        if (fileName.equalsIgnoreCase(listingEntry)) {
+                            if (LOG.isDebugEnabled()) {
+                                LOG.debug("Emulating file-not-found because of letter case differences to the "
+                                        + "real file, for: {}", sourcePath);
+                            }
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+
+        synchronized (correctCasePaths) {
+            correctCasePaths.put(sourcePath, Boolean.TRUE);        
+        }
+        return true;
+    }
+    
+    /**
+     * Returns the base directory in which the templates are searched. This comes from the constructor argument, but
+     * it's possibly a canonicalized version of that. 
+     *  
+     * @since 2.3.21
+     */
+    public File getBaseDirectory() {
+        return baseDir;
+    }
+    
+    /**
+     * Intended for development only, checks if the template name matches the case (upper VS lower case letters) of the
+     * actual file name, and if it doesn't, it emulates a file-not-found even if the file system is case insensitive.
+     * This is useful when developing application on Windows, which will be later installed on Linux, OS X, etc. This
+     * check can be resource intensive, as to check the file name the directories involved, up to the
+     * {@link #getBaseDirectory()} directory, must be listed. Positive results (matching case) will be cached without
+     * expiration time.
+     * 
+     * <p>The default in {@link FileTemplateLoader} is {@code false}, but subclasses may change they by overriding
+     * {@link #getEmulateCaseSensitiveFileSystemDefault()}.
+     * 
+     * @since 2.3.23
+     */
+    public void setEmulateCaseSensitiveFileSystem(boolean emulateCaseSensitiveFileSystem) {
+        // Ensure that the cache exists exactly when needed:
+        if (emulateCaseSensitiveFileSystem) {
+            if (correctCasePaths == null) {
+                correctCasePaths = new MruCacheStorage(CASE_CHECH_CACHE_HARD_SIZE, CASE_CHECK_CACHE__SOFT_SIZE);
+            }
+        } else {
+            correctCasePaths = null;
+        }
+        
+        this.emulateCaseSensitiveFileSystem = emulateCaseSensitiveFileSystem;
+    }
+
+    /**
+     * Getter pair of {@link #setEmulateCaseSensitiveFileSystem(boolean)}.
+     * 
+     * @since 2.3.23
+     */
+    public boolean getEmulateCaseSensitiveFileSystem() {
+        return emulateCaseSensitiveFileSystem;
+    }
+
+    /**
+     * Returns the default of {@link #getEmulateCaseSensitiveFileSystem()}. In {@link FileTemplateLoader} it's
+     * {@code false}, unless the {@link #SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM} system property was
+     * set to {@code true}, but this can be overridden here in custom subclasses. For example, if your environment
+     * defines something like developer mode, you may want to override this to return {@code true} on Windows.
+     * 
+     * @since 2.3.23
+     */
+    protected boolean getEmulateCaseSensitiveFileSystemDefault() {
+        return EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT;
+    }
+
+    /**
+     * Show class name and some details that are useful in template-not-found errors.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public String toString() {
+        // We don't _StringUtil.jQuote paths here, because on Windows there will be \\-s then that some may find
+        // confusing.
+        return _TemplateLoaderUtils.getClassNameForToString(this) + "("
+                + "baseDir=\"" + baseDir + "\""
+                + (canonicalBasePath != null ? ", canonicalBasePath=\"" + canonicalBasePath + "\"" : "")
+                + (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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java
new file mode 100644
index 0000000..9f004fe
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java
@@ -0,0 +1,330 @@
+/*
+ * 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.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize;
+
+/**
+ * A cache storage that implements a two-level Most Recently Used cache. In the
+ * first level, items are strongly referenced up to the specified maximum. When
+ * the maximum is exceeded, the least recently used item is moved into the  
+ * second level cache, where they are softly referenced, up to another 
+ * specified maximum. When the second level maximum is also exceeded, the least 
+ * recently used item is discarded altogether. This cache storage is a 
+ * generalization of both {@link StrongCacheStorage} and 
+ * {@link SoftCacheStorage} - the effect of both of them can be achieved by 
+ * setting one maximum to zero and the other to the largest positive integer. 
+ * On the other hand, if you wish to use this storage in a strong-only mode, or
+ * in a soft-only mode, you might consider using {@link StrongCacheStorage} or
+ * {@link SoftCacheStorage} instead, as they can be used by 
+ * {@link DefaultTemplateResolver} concurrently without any synchronization on a 5.0 or 
+ * later JRE.
+ *  
+ * <p>This class is <em>NOT</em> thread-safe. If it's accessed from multiple
+ * threads concurrently, proper synchronization must be provided by the callers.
+ * Note that {@link DefaultTemplateResolver}, the natural user of this class provides the
+ * necessary synchronizations when it uses the class.
+ * Also you might consider whether you need this sort of a mixed storage at all
+ * in your solution, as in most cases SoftCacheStorage can also be sufficient. 
+ * SoftCacheStorage will use Java soft references, and they already use access 
+ * timestamps internally to bias the garbage collector against clearing 
+ * recently used references, so you can get reasonably good (and 
+ * memory-sensitive) most-recently-used caching through 
+ * {@link SoftCacheStorage} as well.
+ *
+ * @see Configuration#getCacheStorage()
+ */
+public class MruCacheStorage implements CacheStorageWithGetSize {
+    private final MruEntry strongHead = new MruEntry();
+    private final MruEntry softHead = new MruEntry();
+    {
+        softHead.linkAfter(strongHead);
+    }
+    private final Map map = new HashMap();
+    private final ReferenceQueue refQueue = new ReferenceQueue();
+    private final int strongSizeLimit;
+    private final int softSizeLimit;
+    private int strongSize = 0;
+    private int softSize = 0;
+    
+    /**
+     * Creates a new MRU cache storage with specified maximum cache sizes. Each
+     * cache size can vary between 0 and {@link Integer#MAX_VALUE}.
+     * @param strongSizeLimit the maximum number of strongly referenced templates; when exceeded, the entry used
+     *          the least recently will be moved into the soft cache.
+     * @param softSizeLimit the maximum number of softly referenced templates; when exceeded, the entry used
+     *          the least recently will be discarded.
+     */
+    public MruCacheStorage(int strongSizeLimit, int softSizeLimit) {
+        if (strongSizeLimit < 0) throw new IllegalArgumentException("strongSizeLimit < 0");
+        if (softSizeLimit < 0) throw new IllegalArgumentException("softSizeLimit < 0");
+        this.strongSizeLimit = strongSizeLimit;
+        this.softSizeLimit = softSizeLimit;
+    }
+    
+    @Override
+    public Object get(Object key) {
+        removeClearedReferences();
+        MruEntry entry = (MruEntry) map.get(key);
+        if (entry == null) {
+            return null;
+        }
+        relinkEntryAfterStrongHead(entry, null);
+        Object value = entry.getValue();
+        if (value instanceof MruReference) {
+            // This can only happen with strongSizeLimit == 0
+            return ((MruReference) value).get();
+        }
+        return value;
+    }
+
+    @Override
+    public void put(Object key, Object value) {
+        removeClearedReferences();
+        MruEntry entry = (MruEntry) map.get(key);
+        if (entry == null) {
+            entry = new MruEntry(key, value);
+            map.put(key, entry);
+            linkAfterStrongHead(entry);
+        } else {
+            relinkEntryAfterStrongHead(entry, value);
+        }
+        
+    }
+
+    @Override
+    public void remove(Object key) {
+        removeClearedReferences();
+        removeInternal(key);
+    }
+
+    private void removeInternal(Object key) {
+        MruEntry entry = (MruEntry) map.remove(key);
+        if (entry != null) {
+            unlinkEntryAndInspectIfSoft(entry);
+        }
+    }
+
+    @Override
+    public void clear() {
+        strongHead.makeHead();
+        softHead.linkAfter(strongHead);
+        map.clear();
+        strongSize = softSize = 0;
+        // Quick refQueue processing
+        while (refQueue.poll() != null);
+    }
+
+    private void relinkEntryAfterStrongHead(MruEntry entry, Object newValue) {
+        if (unlinkEntryAndInspectIfSoft(entry) && newValue == null) {
+            // Turn soft reference into strong reference, unless is was cleared
+            MruReference mref = (MruReference) entry.getValue();
+            Object strongValue = mref.get();
+            if (strongValue != null) {
+                entry.setValue(strongValue);
+                linkAfterStrongHead(entry);
+            } else {
+                map.remove(mref.getKey());
+            }
+        } else {
+            if (newValue != null) {
+                entry.setValue(newValue);
+            }
+            linkAfterStrongHead(entry);
+        }
+    }
+
+    private void linkAfterStrongHead(MruEntry entry) {
+        entry.linkAfter(strongHead);
+        if (strongSize == strongSizeLimit) {
+            // softHead.previous is LRU strong entry
+            MruEntry lruStrong = softHead.getPrevious();
+            // Attila: This is equaivalent to strongSizeLimit != 0
+            // DD: But entry.linkAfter(strongHead) was just executed above, so
+            //     lruStrong != strongHead is true even if strongSizeLimit == 0.
+            if (lruStrong != strongHead) {
+                lruStrong.unlink();
+                if (softSizeLimit > 0) {
+                    lruStrong.linkAfter(softHead);
+                    lruStrong.setValue(new MruReference(lruStrong, refQueue));
+                    if (softSize == softSizeLimit) {
+                        // List is circular, so strongHead.previous is LRU soft entry
+                        MruEntry lruSoft = strongHead.getPrevious();
+                        lruSoft.unlink();
+                        map.remove(lruSoft.getKey());
+                    } else {
+                        ++softSize;
+                    }
+                } else {
+                    map.remove(lruStrong.getKey());
+                }
+            }
+        } else {
+            ++strongSize;
+        }
+    }
+
+    private boolean unlinkEntryAndInspectIfSoft(MruEntry entry) {
+        entry.unlink();
+        if (entry.getValue() instanceof MruReference) {
+            --softSize;
+            return true;
+        } else {
+            --strongSize;
+            return false;
+        }
+    }
+    
+    private void removeClearedReferences() {
+        for (; ; ) {
+            MruReference ref = (MruReference) refQueue.poll();
+            if (ref == null) {
+                break;
+            }
+            removeInternal(ref.getKey());
+        }
+    }
+    
+    /**
+     * Returns the configured upper limit of the number of strong cache entries.
+     *  
+     * @since 2.3.21
+     */
+    public int getStrongSizeLimit() {
+        return strongSizeLimit;
+    }
+
+    /**
+     * Returns the configured upper limit of the number of soft cache entries.
+     * 
+     * @since 2.3.21
+     */
+    public int getSoftSizeLimit() {
+        return softSizeLimit;
+    }
+
+    /**
+     * Returns the <em>current</em> number of strong cache entries.
+     *  
+     * @see #getStrongSizeLimit()
+     * @since 2.3.21
+     */
+    public int getStrongSize() {
+        return strongSize;
+    }
+
+    /**
+     * Returns a close approximation of the <em>current</em> number of soft cache entries.
+     * 
+     * @see #getSoftSizeLimit()
+     * @since 2.3.21
+     */
+    public int getSoftSize() {
+        removeClearedReferences();
+        return softSize;
+    }
+    
+    /**
+     * Returns a close approximation of the current number of cache entries.
+     * 
+     * @see #getStrongSize()
+     * @see #getSoftSize()
+     * @since 2.3.21
+     */
+    @Override
+    public int getSize() {
+        return getSoftSize() + getStrongSize();
+    }
+
+    private static final class MruEntry {
+        private MruEntry prev;
+        private MruEntry next;
+        private final Object key;
+        private Object value;
+        
+        /**
+         * Used solely to construct the head element
+         */
+        MruEntry() {
+            makeHead();
+            key = value = null;
+        }
+        
+        MruEntry(Object key, Object value) {
+            this.key = key;
+            this.value = value;
+        }
+        
+        Object getKey() {
+            return key;
+        }
+        
+        Object getValue() {
+            return value;
+        }
+        
+        void setValue(Object value) {
+            this.value = value;
+        }
+
+        MruEntry getPrevious() {
+            return prev;
+        }
+        
+        void linkAfter(MruEntry entry) {
+            next = entry.next;
+            entry.next = this;
+            prev = entry;
+            next.prev = this;
+        }
+        
+        void unlink() {
+            next.prev = prev;
+            prev.next = next;
+            prev = null;
+            next = null;
+        }
+        
+        void makeHead() {
+            prev = next = this;
+        }
+    }
+    
+    private static class MruReference extends SoftReference {
+        private final Object key;
+        
+        MruReference(MruEntry entry, ReferenceQueue queue) {
+            super(entry.getValue(), queue);
+            key = entry.getKey();
+        }
+        
+        Object getKey() {
+            return key;
+        }
+    }
+    
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java
new file mode 100644
index 0000000..883ec62
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java
@@ -0,0 +1,172 @@
+/*
+ * 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.io.Serializable;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+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.TemplateLoadingResultStatus;
+import org.apache.freemarker.core.templateresolver.TemplateLoadingSource;
+import org.apache.freemarker.core.util._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. 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).
+ * 
+ * <p>This class is thread-safe.
+ */
+// TODO JUnit test
+public class MultiTemplateLoader implements TemplateLoader {
+
+    private final TemplateLoader[] templateLoaders;
+    private final Map<String, TemplateLoader> lastTemplateLoaderForName = new ConcurrentHashMap<>();
+    
+    private boolean sticky = false;
+
+    /**
+     * Creates a new instance that will use the specified template loaders.
+     * 
+     * @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... templateLoaders) {
+        _NullArgumentException.check("templateLoaders", templateLoaders);
+        this.templateLoaders = templateLoaders.clone();
+    }
+
+    /**
+     * Clears the sickiness memory, also resets the state of all enclosed {@link TemplateLoader}-s.
+     */
+    @Override
+    public void resetState() {
+        lastTemplateLoaderForName.clear();
+        for (TemplateLoader templateLoader : templateLoaders) {
+            templateLoader.resetState();
+        }
+    }
+
+    /**
+     * Show class name and some details that are useful in template-not-found errors.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("MultiTemplateLoader(");
+        for (int i = 0; i < templateLoaders.length; i++) {
+            if (i != 0) {
+                sb.append(", ");
+            }
+            sb.append("loader").append(i + 1).append(" = ").append(templateLoaders[i]);
+        }
+        sb.append(")");
+        return sb.toString();
+    }
+
+    /**
+     * Returns the number of {@link TemplateLoader}-s directly inside this {@link TemplateLoader}.
+     * 
+     * @since 2.3.23
+     */
+    public int getTemplateLoaderCount() {
+        return templateLoaders.length;
+    }
+
+    /**
+     * Returns the {@link TemplateLoader} at the given index.
+     * 
+     * @param index
+     *            Must be below {@link #getTemplateLoaderCount()}.
+     */
+    public TemplateLoader getTemplateLoader(int index) {
+        return templateLoaders[index];
+    }
+
+    /**
+     * Getter pair of {@link #setSticky(boolean)}.
+     */
+    public boolean isSticky() {
+        return sticky;
+    }
+
+    /**
+     * 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/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java
new file mode 100644
index 0000000..c8ff55c
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java
@@ -0,0 +1,71 @@
+/*
+ * 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 org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize;
+
+/**
+ * A cache storage that doesn't store anything. Use this if you
+ * don't want caching.
+ *
+ * @see Configuration#getCacheStorage()
+ * 
+ * @since 2.3.17
+ */
+public class NullCacheStorage implements CacheStorage, CacheStorageWithGetSize {
+    
+    /**
+     * @since 2.3.22
+     */
+    public static final NullCacheStorage INSTANCE = new NullCacheStorage();
+    
+    @Override
+    public Object get(Object key) {
+        return null;
+    }
+
+    @Override
+    public void put(Object key, Object value) {
+        // do nothing
+    }
+
+    @Override
+    public void remove(Object key) {
+        // do nothing
+    }
+    
+    @Override
+    public void clear() {
+        // do nothing
+    }
+
+    /**
+     * Always returns 0.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public int getSize() {
+        return 0;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java
new file mode 100644
index 0000000..3e22c33
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java
@@ -0,0 +1,112 @@
+/*
+ * 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.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.freemarker.core.Configuration.ExtendableBuilder;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize;
+
+/**
+ * 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 ExtendableBuilder#setCacheStorage(CacheStorage)
+ */
+public class SoftCacheStorage implements CacheStorage, CacheStorageWithGetSize {
+    
+    private final ReferenceQueue queue = new ReferenceQueue();
+    private final ConcurrentMap map;
+    
+    /**
+     * Creates an instance that uses a {@link ConcurrentMap} internally.
+     */
+    public SoftCacheStorage() {
+        map = new ConcurrentHashMap();
+    }
+    
+    @Override
+    public Object get(Object key) {
+        processQueue();
+        Reference ref = (Reference) map.get(key);
+        return ref == null ? null : ref.get();
+    }
+
+    @Override
+    public void put(Object key, Object value) {
+        processQueue();
+        map.put(key, new SoftValueReference(key, value, queue));
+    }
+
+    @Override
+    public void remove(Object key) {
+        processQueue();
+        map.remove(key);
+    }
+
+    @Override
+    public void clear() {
+        map.clear();
+        processQueue();
+    }
+    
+    /**
+     * Returns a close approximation of the number of cache entries.
+     * 
+     * @since 2.3.21
+     */
+    @Override
+    public int getSize() {
+        processQueue();
+        return map.size();
+    }
+
+    private void processQueue() {
+        for (; ; ) {
+            SoftValueReference ref = (SoftValueReference) queue.poll();
+            if (ref == null) {
+                return;
+            }
+            Object key = ref.getKey();
+            map.remove(key, ref);
+        }
+    }
+
+    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;
+        }
+    }
+    
+}
\ No newline at end of file


Mime
View raw message