http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java
new file mode 100644
index 0000000..af197ef
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateLookupStrategy.java
@@ -0,0 +1,61 @@
+/*
+ * 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.util.Locale;
+
+import org.apache.freemarker.core.templateresolver.TemplateLookupContext;
+import org.apache.freemarker.core.templateresolver.TemplateLookupResult;
+import org.apache.freemarker.core.templateresolver.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 class DefaultTemplateLookupStrategy extends TemplateLookupStrategy {
+    
+    public static final DefaultTemplateLookupStrategy INSTANCE = new 
DefaultTemplateLookupStrategy();
+    
+    private DefaultTemplateLookupStrategy() {
+        //
+    }
+    
+    @Override
+    public  <R extends TemplateLookupResult> R lookup(TemplateLookupContext<R> 
ctx) throws IOException {
+        return 
ctx.lookupWithLocalizedThenAcquisitionStrategy(ctx.getTemplateName(), 
ctx.getTemplateLocale());
+    }
+    
+}
\ 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/impl/DefaultTemplateNameFormat.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java
new file mode 100644
index 0000000..448f2af
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormat.java
@@ -0,0 +1,310 @@
+/*
+ * 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.TemplateNotFoundException;
+import org.apache.freemarker.core.Version;
+import 
org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateNameFormat;
+import org.apache.freemarker.core.util.StringUtil;
+
+/**
+ * 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 DefaultTemplateNameFormatFM2} 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 DefaultTemplateNameFormatFM2} 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 final class DefaultTemplateNameFormat extends TemplateNameFormat {
+    
+    public static DefaultTemplateNameFormat INSTANCE = new 
DefaultTemplateNameFormat();
+    
+    private DefaultTemplateNameFormat() {
+        //
+    }
+    
+    @Override
+    public 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
+    public 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;
+    }
+    
+}
\ 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/impl/DefaultTemplateNameFormatFM2.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java
new file mode 100644
index 0000000..c90b067
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateNameFormatFM2.java
@@ -0,0 +1,106 @@
+/*
+ * 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.Version;
+import 
org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.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 DefaultTemplateNameFormat#INSTANCE} instead.
+ * 
+ * @deprecated [FM3] Remove
+ */
+@Deprecated
+public final class DefaultTemplateNameFormatFM2 extends TemplateNameFormat {
+    
+    public static final DefaultTemplateNameFormatFM2 INSTANCE = new 
DefaultTemplateNameFormatFM2();
+    
+    private DefaultTemplateNameFormatFM2() {
+        //
+    }
+    
+    @Override
+    public 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
+    public 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;
+    }
+    
+}
\ 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/impl/DefaultTemplateResolver.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java
new file mode 100644
index 0000000..456f944
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java
@@ -0,0 +1,1042 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateNotFoundException;
+import org.apache.freemarker.core._CoreLogs;
+import org.apache.freemarker.core._TemplateAPI;
+import org.apache.freemarker.core.ast.BugException;
+import 
org.apache.freemarker.core.ast.MarkReleaserTemplateSpecifiedEncodingHandler;
+import org.apache.freemarker.core.ast.TemplateConfiguration;
+import org.apache.freemarker.core.ast.TemplateSpecifiedEncodingHandler;
+import org.apache.freemarker.core.templateresolver.CacheStorage;
+import org.apache.freemarker.core.templateresolver.ConcurrentCacheStorage;
+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.NullArgumentException;
+import org.apache.freemarker.core.util.StringUtil;
+import org.apache.freemarker.core.util.UndeclaredThrowableException;
+import org.slf4j.Logger;
+
+/**
+ * Performs caching and on-demand loading of the templates.
+ * The actual template "file" loading is delegated to a {@link TemplateLoader} 
that you can specify in the constructor.
+ * Some aspects of caching is delegated to a {@link CacheStorage} that you can 
also specify in the constructor.
+ * 
+ * <p>Typically you don't instantiate or otherwise use this class directly. 
The {@link Configuration} embeds an
+ * instance of this class, that you access indirectly through {@link 
Configuration#getTemplate(String)} and other
+ * {@link Configuration} API-s. Then {@link TemplateLoader} and {@link 
CacheStorage} can be set with
+ * {@link Configuration#setTemplateLoader(TemplateLoader)} and
+ * {@link Configuration#setCacheStorage(CacheStorage)}.
+ */
+public class DefaultTemplateResolver extends TemplateResolver {
+    
+    /**
+     * The default template update delay; see {@link 
Configuration#setTemplateUpdateDelayMilliseconds(long)}.
+     * 
+     * @since 2.3.23
+     */
+    public static final long DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS = 5000L;
+    
+    private static final String ASTERISKSTR = "*";
+    private static final char ASTERISK = '*';
+    private static final char SLASH = '/';
+    private static final String LOCALE_PART_SEPARATOR = "_";
+    private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER;
+
+    /** Maybe {@code null}. */
+    private final TemplateLoader templateLoader;
+    
+    /** Here we keep our cached templates */
+    private final CacheStorage cacheStorage;
+    private final TemplateLookupStrategy templateLookupStrategy;
+    private final TemplateNameFormat templateNameFormat;
+    private final TemplateConfigurationFactory templateConfigurations;
+    
+    private final boolean isCacheStorageConcurrent;
+    /** {@link Configuration#setTemplateUpdateDelayMilliseconds(long)} */
+    private long templateUpdateDelayMilliseconds = 
DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS;
+    /** {@link Configuration#setLocalizedLookup(boolean)} */
+    private boolean localizedLookup = true;
+
+    private Configuration config;
+
+    /**
+     * Same as {@link #DefaultTemplateResolver(TemplateLoader, CacheStorage, 
Configuration)} with a new {@link SoftCacheStorage}
+     * as the 2nd parameter.
+     * 
+     * @since 2.3.21
+     */
+    public DefaultTemplateResolver(TemplateLoader templateLoader, 
Configuration config) {
+        this(templateLoader, 
_TemplateAPI.createDefaultCacheStorage(Configuration.VERSION_2_3_0), config);
+    }
+    
+    /**
+     * Same as
+     * {@link #DefaultTemplateResolver(TemplateLoader, CacheStorage, 
TemplateLookupStrategy, TemplateNameFormat, Configuration)}
+     * with {@link DefaultTemplateLookupStrategy#INSTANCE} and {@link 
DefaultTemplateNameFormatFM2#INSTANCE}.
+     * 
+     * @since 2.3.21
+     */
+    public DefaultTemplateResolver(TemplateLoader templateLoader, CacheStorage 
cacheStorage, Configuration config) {
+        this(templateLoader, cacheStorage,
+                
_TemplateAPI.getDefaultTemplateLookupStrategy(Configuration.VERSION_2_3_0),
+                
_TemplateAPI.getDefaultTemplateNameFormat(Configuration.VERSION_2_3_0),
+                config);
+    }
+    
+    /**
+     * Same as
+     * {@link #DefaultTemplateResolver(TemplateLoader, CacheStorage, 
TemplateLookupStrategy, TemplateNameFormat,
+     * TemplateConfigurationFactory, Configuration)} with {@code null} for 
{@code templateConfigurations}-s.
+     * 
+     * @since 2.3.22
+     */
+    public DefaultTemplateResolver(TemplateLoader templateLoader, CacheStorage 
cacheStorage,
+            TemplateLookupStrategy templateLookupStrategy, TemplateNameFormat 
templateNameFormat,
+            Configuration config) {
+        this(templateLoader, cacheStorage, templateLookupStrategy, 
templateNameFormat, null, config);
+    }
+
+    /**
+     * @param templateLoader
+     *            The {@link TemplateLoader} to use. Can be {@code null}, 
though then every request will result in
+     *            {@link TemplateNotFoundException}.
+     * @param cacheStorage
+     *            The {@link CacheStorage} to use. Can't be {@code null}.
+     * @param templateLookupStrategy
+     *            The {@link TemplateLookupStrategy} to use. Can't be {@code 
null}.
+     * @param templateNameFormat
+     *            The {@link TemplateNameFormat} to use. Can't be {@code null}.
+     * @param templateConfigurations
+     *            The {@link TemplateConfigurationFactory} to use. Can be 
{@code null} (then all templates will use the
+     *            settings coming from the {@link Configuration} as is, except 
in the very rare case where a
+     *            {@link TemplateLoader} itself specifies a {@link 
TemplateConfiguration}).
+     * @param config
+     *            The {@link Configuration} this cache will be used for. Can't 
be {@code null}.
+     * 
+     * @since 2.3.24
+     */
+    public DefaultTemplateResolver(TemplateLoader templateLoader, CacheStorage 
cacheStorage,
+            TemplateLookupStrategy templateLookupStrategy, TemplateNameFormat 
templateNameFormat,
+            TemplateConfigurationFactory templateConfigurations,
+            Configuration config) {
+        super(config);
+        
+        this.templateLoader = templateLoader;
+        
+        NullArgumentException.check("cacheStorage", cacheStorage);
+        this.cacheStorage = cacheStorage;
+        isCacheStorageConcurrent = cacheStorage instanceof 
ConcurrentCacheStorage &&
+                ((ConcurrentCacheStorage) cacheStorage).isConcurrent();
+        
+        NullArgumentException.check("templateLookupStrategy", 
templateLookupStrategy);
+        this.templateLookupStrategy = templateLookupStrategy;
+
+        NullArgumentException.check("templateNameFormat", templateNameFormat);
+        this.templateNameFormat = templateNameFormat;
+
+        // Can be null
+        this.templateConfigurations = templateConfigurations;
+        
+        NullArgumentException.check("config", config);
+        this.config = config;
+    }
+    
+    /**
+     * Returns the configuration for internal usage.
+     */
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    public TemplateLoader getTemplateLoader() {
+        return templateLoader;
+    }
+
+    public CacheStorage getCacheStorage() {
+        return cacheStorage;
+    }
+    
+    /**
+     * @since 2.3.22
+     */
+    public TemplateLookupStrategy getTemplateLookupStrategy() {
+        return templateLookupStrategy;
+    }
+    
+    /**
+     * @since 2.3.22
+     */
+    public TemplateNameFormat getTemplateNameFormat() {
+        return templateNameFormat;
+    }
+    
+    /**
+     * @since 2.3.24
+     */
+    public TemplateConfigurationFactory getTemplateConfigurations() {
+        return templateConfigurations;
+    }
+
+    /**
+     * Retrieves the template with the given name (and according the specified 
further parameters) from the template
+     * cache, loading it into the cache first if it's missing/staled.
+     * 
+     * <p>
+     * All parameters must be non-{@code null}, except {@code 
customLookupCondition}. For the meaning of the parameters
+     * see {@link Configuration#getTemplate(String, Locale, String, boolean)}.
+     *
+     * @return A {@link GetTemplateResult} object that contains the {@link 
Template}, or a
+     *         {@link GetTemplateResult} object that contains {@code null} as 
the {@link Template} and information
+     *         about the missing template. The return value itself is never 
{@code null}. Note that exceptions occurring
+     *         during template loading will not be classified as a missing 
template, so they will cause an exception to
+     *         be thrown by this method instead of returning a {@link 
GetTemplateResult}. The idea is that having a
+     *         missing template is normal (not exceptional), providing that 
the backing storage mechanism could indeed
+     *         check that it's missing.
+     * 
+     * @throws MalformedTemplateNameException
+     *             If the {@code name} was malformed according the current 
{@link TemplateNameFormat}. However, if the
+     *             {@link TemplateNameFormat} is {@link 
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, Object 
customLookupCondition,
+            String encoding, boolean parseAsFTL)
+    throws IOException {
+        NullArgumentException.check("name", name);
+        NullArgumentException.check("locale", locale);
+        NullArgumentException.check("encoding", encoding);
+        
+        try {
+            name = templateNameFormat.normalizeRootBasedName(name);
+        } catch (MalformedTemplateNameException e) {
+            // If we don't have to emulate backward compatible behavior, then 
just rethrow it: 
+            if (templateNameFormat != DefaultTemplateNameFormatFM2.INSTANCE
+                    || config.getIncompatibleImprovements().intValue() >= 
_TemplateAPI.VERSION_INT_2_4_0) {
+                throw e;
+            }
+            return new GetTemplateResult(null, e);
+        }
+        
+        if (templateLoader == null) {
+            return new GetTemplateResult(name, "The TemplateLoader (and 
TemplateLoader2) was null.");
+        }
+        
+        Template template = getTemplateInternal(name, locale, 
customLookupCondition, encoding, parseAsFTL);
+        return template != null ? new GetTemplateResult(template) : new 
GetTemplateResult(name, (String) null);
+    }
+
+    @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 Object 
customLookupCondition,
+            final String encoding, final boolean parseAsFTL)
+    throws IOException {
+        final boolean debug = LOG.isDebugEnabled();
+        final String debugPrefix = debug
+                ? getDebugPrefix("getTemplate", name, locale, 
customLookupCondition, encoding, parseAsFTL)
+                : null;
+        final CachedResultKey cacheKey = new CachedResultKey(name, locale, 
customLookupCondition, encoding, parseAsFTL);
+        
+        CachedResult oldCachedResult;
+        if (isCacheStorageConcurrent) {
+            oldCachedResult = (CachedResult) cacheStorage.get(cacheKey);
+        } else {
+            synchronized (cacheStorage) {
+                oldCachedResult = (CachedResult) cacheStorage.get(cacheKey);
+            }
+        }
+        
+        final long now = System.currentTimeMillis();
+        
+        boolean rethrownCachedException = false;
+        boolean suppressFinallyException = false;
+        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.");
+                    }
+                    putIntoCache(cacheKey, newCachedResult);
+                    return (Template) newCachedResult.templateOrException;
+                } else {
+                    if (newTemplateLoaderResult.getStatus() != 
TemplateLoadingResultStatus.OPENED) {
+                        // TemplateLoader2ResultStatus.NOT_FOUND was already 
handler earlier
+                        throw new BugException("Unxpected status: " + 
newTemplateLoaderResult.getStatus());
+                    }
+                    if (debug) {
+                        StringBuilder debugMsg = new StringBuilder();
+                        debugMsg.append(debugPrefix)
+                                .append("Reloading template instead of using 
the cached result because ");
+                        if (newCachedResult.templateOrException instanceof 
Throwable) {
+                            debugMsg.append("it's a cached error (retrying).");
+                        } else {
+                            Object newSource = 
newTemplateLoaderResult.getSource();
+                            if (!nullSafeEquals(newSource, 
oldCachedResult.source)) {
+                                debugMsg.append("the source has been changed: 
")
+                                        
.append("cached.source=").append(StringUtil.jQuoteNoXSS(oldCachedResult.source))
+                                        .append(", 
current.source=").append(StringUtil.jQuoteNoXSS(newSource));
+                            } else {
+                                Serializable newVersion = 
newTemplateLoaderResult.getVersion();
+                                if (!nullSafeEquals(oldCachedResult.version, 
newVersion)) {
+                                    debugMsg.append("the version has been 
changed: ")
+                                            
.append("cached.version=").append(oldCachedResult.version) 
+                                            .append(", 
current.version=").append(newVersion);
+                                } else {
+                                    debugMsg.append("??? (unknown reason)");
+                                }
+                            }
+                        }
+                        LOG.debug(debugMsg.toString());
+                    }
+                }
+            } else { // if there was no cached result
+                if (debug) {
+                    LOG.debug(debugPrefix + "No cached result was found; will 
try to load template.");
+                }
+                
+                newCachedResult = new CachedResult();
+                newCachedResult.lastChecked = now;
+            
+                session = templateLoader.createSession();
+                if (debug && session != null) {
+                    LOG.debug(debugPrefix + "Session created.");
+                } 
+                
+                newLookupResult = lookupAndLoadTemplateIfChanged(
+                        name, locale, customLookupCondition, null, null, 
session);
+                
+                if (!newLookupResult.isPositive()) {
+                    setToNegativeAndPutIntoCache(cacheKey, newCachedResult, 
null);
+                    return null;
+                }
+            }
+            // We have newCachedResult and newLookupResult initialized at this 
point.
+
+            TemplateLoadingResult templateLoaderResult = 
newLookupResult.getTemplateLoaderResult();
+            newCachedResult.source = templateLoaderResult.getSource();
+            
+            // If we get here, then we need to (re)load the template
+            if (debug) {
+                LOG.debug(debugPrefix + "Reading template content (source: "
+                        + StringUtil.jQuoteNoXSS(newCachedResult.source) + 
")");
+            }
+            
+            Template template = loadTemplate(
+                    templateLoaderResult,
+                    name, newLookupResult.getTemplateSourceName(), locale, 
customLookupCondition,
+                    encoding, parseAsFTL);
+            if (session != null) {
+                session.close();
+                if (debug) {
+                    LOG.debug(debugPrefix + "Session closed.");
+                } 
+            }
+            newCachedResult.templateOrException = template;
+            newCachedResult.version = templateLoaderResult.getVersion();
+            putIntoCache(cacheKey, newCachedResult);
+            return template;
+        } catch (RuntimeException e) {
+            if (newCachedResult != null) {
+                setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e);
+            }
+            suppressFinallyException = true;
+            throw e;
+        } catch (IOException e) {
+            // Rethrown cached exceptions are wrapped into IOException-s, so 
we only need this condition here.
+            if (!rethrownCachedException) {
+                setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e);
+            }
+            suppressFinallyException = true;
+            throw e;
+        } finally {
+            try {
+                // Close streams first:
+                
+                if (newLookupResult != null && newLookupResult.isPositive()) {
+                    TemplateLoadingResult templateLoaderResult = 
newLookupResult.getTemplateLoaderResult();
+                    Reader reader = templateLoaderResult.getReader();
+                    if (reader != null) {
+                        try {
+                            reader.close();
+                        } catch (IOException e) { // [FM3] Exception e
+                            if (suppressFinallyException) {
+                                if (LOG.isWarnEnabled()) { 
+                                    LOG.warn("Failed to close template content 
Reader for: " + name, e);
+                                }
+                            } else {
+                                suppressFinallyException = true;
+                                throw e;
+                            }
+                        }
+                    } else if (templateLoaderResult.getInputStream() != null) {
+                        try {
+                            templateLoaderResult.getInputStream().close();
+                        } catch (IOException e) { // [FM3] Exception e
+                            if (suppressFinallyException) {
+                                if (LOG.isWarnEnabled()) { 
+                                    LOG.warn("Failed to close template content 
InputStream for: " + name, e);
+                                }
+                            } else {
+                                suppressFinallyException = true;
+                                throw e;
+                            }
+                        }
+                    }
+                }
+            } finally {
+                // Then close streams:
+                
+                if (session != null && !session.isClosed()) {
+                    try {
+                        session.close();
+                        if (debug) {
+                            LOG.debug(debugPrefix + "Session closed.");
+                        } 
+                    } catch (IOException e) { // [FM3] Exception e
+                        if (suppressFinallyException) {
+                            if (LOG.isWarnEnabled()) { 
+                                LOG.warn("Failed to close template loader 
session for" + name, e);
+                            }
+                        } else {
+                            suppressFinallyException = true;
+                            throw e;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    
+    
+    private static final Method INIT_CAUSE = getInitCauseMethod();
+    
+    private static final Method getInitCauseMethod() {
+        try {
+            return Throwable.class.getMethod("initCause", new Class[] { 
Throwable.class });
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+    
+    /**
+     * Creates an {@link IOException} that has a cause exception.
+     */
+    // [Java 6] Remove
+    private IOException newIOException(String message, Throwable cause) {
+        if (cause == null) {
+            return new IOException(message);
+        }
+        
+        IOException ioe;
+        if (INIT_CAUSE != null) {
+            ioe = new IOException(message);
+            try {
+                INIT_CAUSE.invoke(ioe, cause);
+            } catch (RuntimeException ex) {
+                throw ex;
+            } catch (Exception ex) {
+                throw new UndeclaredThrowableException(ex);
+            }
+        } else {
+            ioe = new IOException(message + "\nCaused by: " + 
cause.getClass().getName() + 
+            ": " + cause.getMessage());
+        }
+        return ioe;
+    }
+    
+    private void rethrowCachedException(Throwable e) throws IOException {
+        throw newIOException("There was an error loading the " +
+                "template on an earlier attempt; see cause exception.", e);
+    }
+
+    private void setToNegativeAndPutIntoCache(CachedResultKey cacheKey, 
CachedResult cachedResult, Exception e) {
+        cachedResult.templateOrException = e;
+        cachedResult.source = null;
+        cachedResult.version = null;
+        putIntoCache(cacheKey, cachedResult);
+    }
+
+    private void putIntoCache(CachedResultKey tk, CachedResult cachedTemplate) 
{
+        if (isCacheStorageConcurrent) {
+            cacheStorage.put(tk, cachedTemplate);
+        } else {
+            synchronized (cacheStorage) {
+                cacheStorage.put(tk, cachedTemplate);
+            }
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private Template loadTemplate(
+            TemplateLoadingResult templateLoaderResult,
+            final String name, final String sourceName, Locale locale, final 
Object customLookupCondition,
+            String initialEncoding, final boolean parseAsFTL) throws 
IOException {
+        TemplateConfiguration tc;
+        {
+            TemplateConfiguration cfgTC;
+            try {
+                cfgTC = templateConfigurations != null
+                        ? templateConfigurations.get(sourceName, 
templateLoaderResult.getSource()) : null;
+            } catch (TemplateConfigurationFactoryException e) {
+                throw newIOException("Error while getting 
TemplateConfiguration; see cause exception.", e);
+            }
+            TemplateConfiguration resultTC = 
templateLoaderResult.getTemplateConfiguration();
+            if (resultTC != null) {
+                TemplateConfiguration mergedTC = new TemplateConfiguration();
+                if (cfgTC != null) {
+                    mergedTC.merge(cfgTC);
+                }
+                if (resultTC != null) {
+                    mergedTC.merge(resultTC);
+                }
+                mergedTC.setParentConfiguration(config);
+                
+                tc = mergedTC;
+            } else {
+                tc = cfgTC;
+            }
+        }
+        
+        if (tc != null) {
+            // TC.{encoding,locale} is stronger than the cfg.getTemplate 
arguments by design.
+            if (tc.isEncodingSet()) {
+                initialEncoding = tc.getEncoding();
+            }
+            if (tc.isLocaleSet()) {
+                locale = tc.getLocale();
+            }
+        }
+        
+        Template template;
+        {
+            Reader reader = templateLoaderResult.getReader();
+            InputStream inputStream = templateLoaderResult.getInputStream();
+            TemplateSpecifiedEncodingHandler templateSpecifiedEncodingHandler;
+            if (reader != null) {
+                if (inputStream != null) {
+                    throw new IllegalStateException("For a(n) " + 
templateLoaderResult.getClass().getName()
+                            + ", both getReader() and getInputStream() has 
returned non-null.");
+                }
+                initialEncoding = null;  // No charset decoding has happened
+                templateSpecifiedEncodingHandler = 
TemplateSpecifiedEncodingHandler.DEFAULT; 
+            } else if (inputStream != null) {
+                if (parseAsFTL) {
+                    // We need mark support, to restart if the charset 
suggested by <#ftl encoding=...> differs
+                    // from that we use initially.
+                    if (!inputStream.markSupported()) {
+                        inputStream = new BufferedInputStream(inputStream);
+                    }
+                    inputStream.mark(Integer.MAX_VALUE); // Mark is released 
after the 1st FTL tag
+                    templateSpecifiedEncodingHandler = new 
MarkReleaserTemplateSpecifiedEncodingHandler(inputStream);
+                } else {
+                    templateSpecifiedEncodingHandler = null; 
+                }
+                // Regarding buffering worries: On the Reader side we should 
only read in chunks (like through a
+                // BufferedReader), so there shouldn't be a problem if the 
InputStream is not buffered. (Also, at least
+                // on Oracle JDK and OpenJDK 7 the InputStreamReader itself 
has an internal ~8K buffer.)
+                reader = new InputStreamReader(inputStream, initialEncoding);
+            } else {
+                throw new IllegalStateException("For a(n) " + 
templateLoaderResult.getClass().getName()
+                        + ", both getReader() and getInputStream() has 
returned null.");
+            }
+            
+            try {
+                if (parseAsFTL) {
+                    try {
+                        template = new Template(name, sourceName, reader, 
config, tc,
+                                initialEncoding, 
templateSpecifiedEncodingHandler);
+                    } catch (Template.WrongEncodingException wee) {
+                        final String templateSpecifiedEncoding = 
wee.getTemplateSpecifiedEncoding();
+                        
+                        if (inputStream != null) {
+                            // We restart InputStream to re-decode it with the 
new charset.
+                            inputStream.reset();
+                            
+                            // Don't close `reader`; it's an InputStreamReader 
that would close the wrapped InputStream.
+                            reader = new InputStreamReader(inputStream, 
templateSpecifiedEncoding);
+                        } else {
+                            // Should be impossible to get here
+                            throw new BugException();
+                        }
+                        
+                        template = new Template(name, sourceName, reader, 
config, tc,
+                                templateSpecifiedEncoding, 
templateSpecifiedEncodingHandler);
+                    }
+                } else {
+                    // Read the contents into a StringWriter, then construct a 
single-text-block template from it.
+                    final StringBuilder sb = new StringBuilder();
+                    final char[] buf = new char[4096];
+                    int charsRead;
+                    while ((charsRead = reader.read(buf)) > 0) {
+                        sb.append(buf, 0, charsRead);
+                    }
+                    template = Template.getPlainTextTemplate(name, sourceName, 
sb.toString(), config);
+                    template.setEncoding(initialEncoding);
+                }
+            } finally {
+                reader.close();
+            }
+        }
+
+        if (tc != null) {
+            tc.apply(template);
+        }
+        
+        template.setLocale(locale);
+        template.setCustomLookupCondition(customLookupCondition);
+        return template;
+    }
+
+    /**
+     * Gets the delay in milliseconds between checking for newer versions of a
+     * template source.
+     * @return the current value of the delay
+     */
+    public long getTemplateUpdateDelayMilliseconds() {
+        // synchronized was moved here so that we don't advertise that it's 
thread-safe, as it's not.
+        synchronized (this) {
+            return templateUpdateDelayMilliseconds;
+        }
+    }
+
+    /**
+     * Sets the delay in milliseconds between checking for newer versions of a
+     * template sources.
+     * @param templateUpdateDelayMilliseconds the new value of the delay
+     */
+    public void setTemplateUpdateDelayMilliseconds(long 
templateUpdateDelayMilliseconds) {
+        // synchronized was moved here so that we don't advertise that it's 
thread-safe, as it's not.
+        synchronized (this) {
+            this.templateUpdateDelayMilliseconds = 
templateUpdateDelayMilliseconds;
+        }
+    }
+
+    /**
+     * Returns if localized template lookup is enabled or not.
+     */
+    public boolean getLocalizedLookup() {
+        // synchronized was moved here so that we don't advertise that it's 
thread-safe, as it's not.
+        synchronized (this) {
+            return localizedLookup;
+        }
+    }
+
+    /**
+     * Setis if localized template lookup is enabled or not.
+     */
+    public void setLocalizedLookup(boolean localizedLookup) {
+        // synchronized was moved here so that we don't advertise that it's 
thread-safe, as it's not.
+        synchronized (this) {
+            if (this.localizedLookup != localizedLookup) {
+                this.localizedLookup = localizedLookup;
+                clearTemplateCache();
+            }
+        }
+    }
+
+    /**
+     * Removes all entries from the cache, forcing reloading of templates on 
subsequent
+     * {@link #getTemplate(String, Locale, Object, String, boolean)} calls.
+     * 
+     * @param resetTemplateLoader
+     *            Whether to call {@link TemplateLoader#resetState()}. on the 
template loader.
+     */
+    public void clearTemplateCache(boolean resetTemplateLoader) {
+        synchronized (cacheStorage) {
+            cacheStorage.clear();
+            if (templateLoader != null && resetTemplateLoader) {
+                templateLoader.resetState();
+            }
+        }
+    }
+    
+    /**
+     * Same as {@link #clearTemplateCache(boolean)} with {@code true} {@code 
resetTemplateLoader} argument.
+     */
+    @Override
+    public void clearTemplateCache() {
+        synchronized (cacheStorage) {
+            cacheStorage.clear();
+            if (templateLoader != null) {
+                templateLoader.resetState();
+            }
+        }
+    }
+
+    /**
+     * Same as {@link #removeTemplateFromCache(String, Locale, Object, String, 
boolean)} with {@code null}
+     * {@code customLookupCondition}.
+     */
+    @Override
+    public void removeTemplateFromCache(
+            String name, Locale locale, String encoding, boolean parse) throws 
IOException {
+        removeTemplateFromCache(name, locale, null, encoding, parse);
+    }
+    
+    /**
+     * Removes an entry from the cache, hence forcing the re-loading of it 
when it's next time requested. (It doesn't
+     * delete the template file itself.) This is to give the application finer 
control over cache updating than
+     * {@link #setTemplateUpdateDelayMilliseconds(long)} alone does.
+     * 
+     * For the meaning of the parameters, see
+     * {@link Configuration#getTemplate(String, Locale, Object, String, 
boolean, boolean)}
+     */
+    public void removeTemplateFromCache(
+            String name, Locale locale, Object customLookupCondition, String 
encoding, boolean parse)
+    throws IOException {
+        if (name == null) {
+            throw new IllegalArgumentException("Argument \"name\" can't be 
null");
+        }
+        if (locale == null) {
+            throw new IllegalArgumentException("Argument \"locale\" can't be 
null");
+        }
+        if (encoding == null) {
+            throw new IllegalArgumentException("Argument \"encoding\" can't be 
null");
+        }
+        name = templateNameFormat.normalizeRootBasedName(name);
+        if (name != null && templateLoader != null) {
+            boolean debug = LOG.isDebugEnabled();
+            String debugPrefix = debug
+                    ? getDebugPrefix("removeTemplate", name, locale, 
customLookupCondition, encoding, parse)
+                    : null;
+            CachedResultKey tk = new CachedResultKey(name, locale, 
customLookupCondition, encoding, parse);
+            
+            if (isCacheStorageConcurrent) {
+                cacheStorage.remove(tk);
+            } else {
+                synchronized (cacheStorage) {
+                    cacheStorage.remove(tk);
+                }
+            }
+            if (debug) {
+                LOG.debug(debugPrefix + "Template was removed from the cache, 
if it was there");
+            }
+        }
+    }
+
+    private String getDebugPrefix(String operation, String name, Locale 
locale, Object customLookupCondition, String encoding,
+            boolean parse) {
+        return operation + " " + StringUtil.jQuoteNoXSS(name) + "("
+                + StringUtil.jQuoteNoXSS(locale)
+                + (customLookupCondition != null ? ", cond=" + 
StringUtil.jQuoteNoXSS(customLookupCondition) : "")
+                + ", " + encoding
+                + (parse ? ", parsed)" : ", unparsed]")
+                + ": ";
+    }    
+
+    /**
+     * Looks up according the {@link TemplateLookupStrategy} and then starts 
reading the template, if it was changed
+     * compared to the cached result, or if there was no cached result yet.
+     */
+    private 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 Object customLookupCondition;
+        private final String encoding;
+        private final boolean parse;
+
+        CachedResultKey(String name, Locale locale, Object 
customLookupCondition, String encoding, boolean parse) {
+            this.name = name;
+            this.locale = locale;
+            this.customLookupCondition = customLookupCondition;
+            this.encoding = encoding;
+            this.parse = parse;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof CachedResultKey) {
+                CachedResultKey tk = (CachedResultKey) o;
+                return
+                    parse == tk.parse &&
+                    name.equals(tk.name) &&
+                    locale.equals(tk.locale) &&
+                    nullSafeEquals(customLookupCondition, 
tk.customLookupCondition) &&
+                    encoding.equals(tk.encoding);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return
+                name.hashCode() ^
+                locale.hashCode() ^
+                encoding.hashCode() ^
+                (customLookupCondition != null ? 
customLookupCondition.hashCode() : 0) ^
+                Boolean.valueOf(!parse).hashCode();
+        }
+    }
+
+    /**
+     * Hold the a cached {@link #getTemplate(String, Locale, Object, String, 
boolean)} result and the associated
+     * information needed to check if the cached value is up to date.
+     * 
+     * <p>
+     * Note: this class is Serializable to allow custom 3rd party CacheStorage 
implementations to serialize/replicate
+     * them; FreeMarker code itself doesn't rely on its serializability.
+     * 
+     * @see CachedResultKey
+     */
+    private static final class CachedResult implements Cloneable, Serializable 
{
+        private static final long serialVersionUID = 1L;
+
+        Object templateOrException;
+        TemplateLoadingSource source;
+        Serializable version;
+        long lastChecked;
+        
+        @Override
+        public CachedResult clone() {
+            try {
+                return (CachedResult) super.clone();
+            } catch (CloneNotSupportedException e) {
+                throw new UndeclaredThrowableException(e);
+            }
+        }
+    }
+    
+    private class DefaultTemplateResolverTemplateLookupContext extends 
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<String>();
+            while (pathTokenizer.hasMoreTokens()) {
+                String pathStep = pathTokenizer.nextToken();
+                if (pathStep.equals(ASTERISKSTR)) {
+                    if (lastAsterisk != -1) {
+                        pathSteps.remove(lastAsterisk);
+                    }
+                    lastAsterisk = pathSteps.size();
+                }
+                pathSteps.add(pathStep);
+            }
+            if (lastAsterisk == -1) {  // if there was no real "*" step after 
all
+                return 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/b0e08ccb/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
new file mode 100644
index 0000000..46998ab
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java
@@ -0,0 +1,384 @@
+/*
+ * 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.SecurityUtilities;
+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 = 
SecurityUtilities.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];
+            this.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 (int i = 0; i < listing.length; i++) {
+                        final String listingEntry = listing[i];
+                        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 nameCaseChecked) {
+        // Ensure that the cache exists exactly when needed:
+        if (nameCaseChecked) {
+            if (correctCasePaths == null) {
+                correctCasePaths = new 
MruCacheStorage(CASE_CHECH_CACHE_HARD_SIZE, CASE_CHECK_CACHE__SOFT_SIZE);
+            }
+        } else {
+            correctCasePaths = null;
+        }
+        
+        this.emulateCaseSensitiveFileSystem = nameCaseChecked;
+    }
+
+    /**
+     * 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);
+        }
+        
+    }
+    
+}


Reply via email to