http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateLookupContext.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateLookupContext.java b/src/main/java/freemarker/cache/TemplateLookupContext.java index c605e15..72827e6 100644 --- a/src/main/java/freemarker/cache/TemplateLookupContext.java +++ b/src/main/java/freemarker/cache/TemplateLookupContext.java @@ -20,6 +20,7 @@ package freemarker.cache; import java.io.IOException; +import java.io.Serializable; import java.util.Locale; import freemarker.template.Configuration; @@ -35,18 +36,25 @@ public abstract class TemplateLookupContext { private final String templateName; private final Locale templateLocale; private final Object customLookupCondition; + private final TemplateLoadingSource cachedResultSource; + private final Serializable cachedResultVersion; /** * Finds the template source based on its <em>normalized</em> name; handles {@code *} steps (so called acquisition), - * otherwise it just calls {@link TemplateLoader#findTemplateSource(String)}. + * otherwise it just calls {@link TemplateLoader#load(String, TemplateLoadingSource, Serializable, + * TemplateLoaderSession)}. * * @param templateName * Must be a normalized name, like {@code "foo/bar/baaz.ftl"}. A name is not normalized when, among - * others, it starts with {@code /}, or contains {@code .} or {@code ..} paths steps, or it uses - * backslash ({@code \}) instead of {@code /}. A normalized name might contains "*" steps. + * others, it starts with {@code /}, or contains {@code .} or {@code ..} path steps, or it uses + * backslash ({@code \}) instead of {@code /}. A normalized name might contains "*" path steps + * (acquisition). * * @return The result of the lookup. Not {@code null}; check {@link TemplateLookupResult#isPositive()} to see if the - * lookup has found anything. + * lookup has found anything. Note that in a positive result the content of the template is possibly + * also already loaded (this is the case for {@link TemplateLoader}-s when the cached content is stale, but + * not for {@link TemplateLoader}-s). Hence discarding a positive result and looking for another can + * generate substantial extra I/O. */ public abstract TemplateLookupResult lookupWithAcquisitionStrategy(String templateName) throws IOException; @@ -62,10 +70,13 @@ public abstract class TemplateLookupContext { Locale templateLocale) throws IOException; /** Default visibility to prevent extending the class from outside this package. */ - TemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition) { + TemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition, + TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion) { this.templateName = templateName; this.templateLocale = templateLocale; this.customLookupCondition = customLookupCondition; + this.cachedResultSource = cachedResultSource; + this.cachedResultVersion = cachedResultVersion; } /** @@ -103,5 +114,13 @@ public abstract class TemplateLookupContext { public TemplateLookupResult createNegativeLookupResult() { return TemplateLookupResult.createNegativeResult(); } + + TemplateLoadingSource getCachedResultSource() { + return cachedResultSource; + } + + Serializable getCachedResultVersion() { + return cachedResultVersion; + } }
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateLookupResult.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateLookupResult.java b/src/main/java/freemarker/cache/TemplateLookupResult.java index cb7a58b..3fa29d3 100644 --- a/src/main/java/freemarker/cache/TemplateLookupResult.java +++ b/src/main/java/freemarker/cache/TemplateLookupResult.java @@ -37,9 +37,9 @@ public abstract class TemplateLookupResult { } /** Used internally to create the appropriate kind of result from the parameters. */ - static TemplateLookupResult from(String templateSourceName, Object templateSource) { - return templateSource != null - ? new PositiveTemplateLookupResult(templateSourceName, templateSource) + static TemplateLookupResult from(String templateSourceName, TemplateLoadingResult templateLoaderResult) { + return templateLoaderResult.getStatus() != TemplateLoadingResultStatus.NOT_FOUND + ? new PositiveTemplateLookupResult(templateSourceName, templateLoaderResult) : createNegativeResult(); } @@ -59,15 +59,15 @@ public abstract class TemplateLookupResult { public abstract boolean isPositive(); /** - * Used internally to extract the {@link TemplateLoader} source; {@code null} if - * {@link #isPositive()} is {@code false}. + * Used internally to extract the {@link TemplateLoadingResult}; {@code null} if {@link #isPositive()} is + * {@code false}. */ - abstract Object getTemplateSource(); + abstract TemplateLoadingResult getTemplateLoaderResult(); private static final class PositiveTemplateLookupResult extends TemplateLookupResult { private final String templateSourceName; - private final Object templateSource; + private final TemplateLoadingResult templateLoaderResult; /** * @param templateSourceName @@ -77,20 +77,16 @@ public abstract class TemplateLookupResult { * {@code "foo_de.ftl"}. Then this parameter must be {@code "foo_de.ftl"}, not {@code "foo.ftl"}. Not * {@code null}. * - * @param templateSource - * See {@link TemplateLoader#findTemplateSource(String)} to understand what that means. Not + * @param templateLoaderResult + * See {@link TemplateLoader#load} to understand what that means. Not * {@code null}. */ - private PositiveTemplateLookupResult(String templateSourceName, Object templateSource) { + private PositiveTemplateLookupResult(String templateSourceName, TemplateLoadingResult templateLoaderResult) { NullArgumentException.check("templateName", templateSourceName); - NullArgumentException.check("templateSource", templateSource); - - if (templateSource instanceof TemplateLookupResult) { - throw new IllegalArgumentException(); - } + NullArgumentException.check("templateLoaderResult", templateLoaderResult); this.templateSourceName = templateSourceName; - this.templateSource = templateSource; + this.templateLoaderResult = templateLoaderResult; } @Override @@ -99,8 +95,8 @@ public abstract class TemplateLookupResult { } @Override - Object getTemplateSource() { - return templateSource; + TemplateLoadingResult getTemplateLoaderResult() { + return templateLoaderResult; } @Override @@ -123,7 +119,7 @@ public abstract class TemplateLookupResult { } @Override - Object getTemplateSource() { + TemplateLoadingResult getTemplateLoaderResult() { return null; } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/URLTemplateLoader.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/URLTemplateLoader.java b/src/main/java/freemarker/cache/URLTemplateLoader.java index e975869..bd5219c 100644 --- a/src/main/java/freemarker/cache/URLTemplateLoader.java +++ b/src/main/java/freemarker/cache/URLTemplateLoader.java @@ -20,29 +20,30 @@ package freemarker.cache; +import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; +import java.io.Serializable; +import java.net.JarURLConnection; import java.net.URL; import java.net.URLConnection; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import freemarker.template.Configuration; /** * This is an abstract template loader that can load templates whose * location can be described by an URL. Subclasses only need to override - * the {@link #getURL(String)} method. Both {@link ClassTemplateLoader} and - * {@link WebappTemplateLoader} are (quite trivial) subclasses of this class. + * the {@link #getURL(String)} method. */ +// TODO JUnit test public abstract class URLTemplateLoader implements TemplateLoader { - private Boolean urlConnectionUsesCaches; + private static final Logger LOG = LoggerFactory.getLogger("freemarker.cache"); - public Object findTemplateSource(String name) - throws IOException { - URL url = getURL(name); - return url == null ? null : new URLTemplateSource(url, getURLConnectionUsesCaches()); - } + private Boolean urlConnectionUsesCaches = false; /** * Given a template name (plus potential locale decorations) retrieves @@ -54,22 +55,6 @@ public abstract class URLTemplateLoader implements TemplateLoader { */ protected abstract URL getURL(String name); - public long getLastModified(Object templateSource) { - return ((URLTemplateSource) templateSource).lastModified(); - } - - public Reader getReader(Object templateSource, String encoding) - throws IOException { - return new InputStreamReader( - ((URLTemplateSource) templateSource).getInputStream(), - encoding); - } - - public void closeTemplateSource(Object templateSource) - throws IOException { - ((URLTemplateSource) templateSource).close(); - } - /** * Can be used by subclasses to canonicalize URL path prefixes. * @param prefix the path prefix to canonicalize @@ -98,22 +83,94 @@ public abstract class URLTemplateLoader implements TemplateLoader { /** * Sets if {@link URLConnection#setUseCaches(boolean)} will be called, and with what value. By default this is - * {@code null}; see the behavior then below. The recommended value is {@code false}, so that FreeMarker can always - * reliably detect when a template was changed. The default is {@code null} only for backward compatibility, - * and certainly will be changed to {@code false} in 2.4.0. As FreeMarker has its own template cache with its - * own update delay setting ({@link Configuration#setTemplateUpdateDelay(int)}), setting this to {@code false} - * shouldn't cause performance problems. - * - * <p>Regarding {@code null} value: By default then {@link URLConnection#setUseCaches(boolean)} won't be called, - * and so the default of the {@link URLConnection} subclass will be in effect (usually {@code true}). That's the - * 2.3.0-compatible mode. However, if {@link Configuration#getIncompatibleImprovements()} is at least 2.3.21, then - * when {@code Configuration.getTemplate} is used, {@code null} will mean {@code false}. Note that this 2.3.21 trick - * only works if the template is loaded through {@code Configuration.getTemplate} (or {@link TemplateCache}). - * - * @since 2.3.21 + * {@code false}, becase FreeMarker has its own template cache with its own update delay setting + * ({@link Configuration#setTemplateUpdateDelay(int)}). If this is set to {@code null}, + * {@link URLConnection#setUseCaches(boolean)} won't be called. */ public void setURLConnectionUsesCaches(Boolean urlConnectionUsesCaches) { this.urlConnectionUsesCaches = urlConnectionUsesCaches; } + + @Override + public TemplateLoaderSession createSession() { + return null; + } + + @Override + public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom, + Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException { + URL url = getURL(name); + if (url == null) { + return TemplateLoadingResult.NOT_FOUND; + } + + URLConnection conn = url.openConnection(); + Boolean urlConnectionUsesCaches = getURLConnectionUsesCaches(); + if (urlConnectionUsesCaches != null) { + conn.setUseCaches(urlConnectionUsesCaches); + } + + // To prevent clustering issues, getLastModified(fallbackToJarLMD=false) + long lmd = getLastModified(conn, false); + Long version = lmd != -1 ? lmd : null; + + URLTemplateLoadingSource source = new URLTemplateLoadingSource(url); + + if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(source) + && Objects.equals(ifVersionDiffersFrom, version)) { + return TemplateLoadingResult.NOT_MODIFIED; + } + + return new TemplateLoadingResult(source, version, conn.getInputStream(), null); + } + + @Override + public void resetState() { + // Do nothing + } + + /** + * {@link URLConnection#getLastModified()} with JDK bug workarounds. Because of JDK-6956385, for files inside a jar, + * it returns the last modification time of the jar itself, rather than the last modification time of the file + * inside the jar. + * + * @param fallbackToJarLMD + * Tells if the file is in side jar, then we should return the last modification time of the jar itself, + * or -1 (to work around JDK-6956385). + */ + public static long getLastModified(URLConnection conn, boolean fallbackToJarLMD) throws IOException { + if (conn instanceof JarURLConnection) { + // There is a bug in sun's jar url connection that causes file handle leaks when calling getLastModified() + // (see https://bugs.openjdk.java.net/browse/JDK-6956385). + // Since the time stamps of jar file contents can't vary independent from the jar file timestamp, just use + // the jar file timestamp + if (fallbackToJarLMD) { + URL jarURL = ((JarURLConnection) conn).getJarFileURL(); + if (jarURL.getProtocol().equals("file")) { + // Return the last modified time of the underlying file - saves some opening and closing + return new File(jarURL.getFile()).lastModified(); + } else { + // Use the URL mechanism + URLConnection jarConn = null; + try { + jarConn = jarURL.openConnection(); + return jarConn.getLastModified(); + } finally { + try { + if (jarConn != null) { + jarConn.getInputStream().close(); + } + } catch (IOException e) { + LOG.warn("Failed to close URL connection for: {}", conn, e); + } + } + } + } else { + return -1; + } + } else { + return conn.getLastModified(); + } + } } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/URLTemplateLoadingSource.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/URLTemplateLoadingSource.java b/src/main/java/freemarker/cache/URLTemplateLoadingSource.java new file mode 100644 index 0000000..bf84bef --- /dev/null +++ b/src/main/java/freemarker/cache/URLTemplateLoadingSource.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package freemarker.cache; + +import java.net.URL; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import freemarker.template.utility.NullArgumentException; + +@SuppressWarnings("serial") +public class URLTemplateLoadingSource implements TemplateLoadingSource { + + private final URL url; + + public URLTemplateLoadingSource(URL url) { + NullArgumentException.check("url", url); + this.url = url; + } + + public URL getUrl() { + return url; + } + + @Override + public int hashCode() { + return url.hashCode(); + } + + @Override + @SuppressFBWarnings("EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS") + public boolean equals(Object obj) { + return url.equals(obj); + } + + @Override + public String toString() { + return url.toString(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/URLTemplateSource.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/URLTemplateSource.java b/src/main/java/freemarker/cache/URLTemplateSource.java deleted file mode 100644 index 716b642..0000000 --- a/src/main/java/freemarker/cache/URLTemplateSource.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package freemarker.cache; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.URL; -import java.net.URLConnection; - -/** - * Wraps a <code>java.net.URL</code>, and implements methods required for a typical template source. - */ -class URLTemplateSource { - private final URL url; - private URLConnection conn; - private InputStream inputStream; - private Boolean useCaches; - - /** - * @param useCaches {@code null} if this aspect wasn't set in the parent {@link TemplateLoader}. - */ - URLTemplateSource(URL url, Boolean useCaches) throws IOException { - this.url = url; - this.conn = url.openConnection(); - this.useCaches = useCaches; - if (useCaches != null) { - conn.setUseCaches(useCaches.booleanValue()); - } - } - - @Override - public boolean equals(Object o) { - if (o instanceof URLTemplateSource) { - return url.equals(((URLTemplateSource) o).url); - } else { - return false; - } - } - - @Override - public int hashCode() { - return url.hashCode(); - } - - @Override - public String toString() { - return url.toString(); - } - - long lastModified() { - if (conn instanceof JarURLConnection) { - // There is a bug in sun's jar url connection that causes file handle leaks when calling getLastModified() - // Since the time stamps of jar file contents can't vary independent from the jar file timestamp, just use - // the jar file timestamp - URL jarURL = ((JarURLConnection) conn).getJarFileURL(); - if (jarURL.getProtocol().equals("file")) { - // Return the last modified time of the underlying file - saves some opening and closing - return new File(jarURL.getFile()).lastModified(); - } else { - // Use the URL mechanism - URLConnection jarConn = null; - try { - jarConn = jarURL.openConnection(); - return jarConn.getLastModified(); - } catch (IOException e) { - return -1; - } finally { - try { - if (jarConn != null) jarConn.getInputStream().close(); - } catch (IOException e) { } - } - } - } else { - long lastModified = conn.getLastModified(); - if (lastModified == -1L && url.getProtocol().equals("file")) { - // Hack for obtaining accurate last modified time for - // URLs that point to the local file system. This is fixed - // in JDK 1.4, but prior JDKs returns -1 for file:// URLs. - return new File(url.getFile()).lastModified(); - } else { - return lastModified; - } - } - } - - InputStream getInputStream() throws IOException { - if (inputStream != null) { - // Ensure that the returned InputStream reads from the beginning of the resource when getInputStream() - // is called for the second time: - try { - inputStream.close(); - } catch (IOException e) { - // Ignore; this is maybe because it was closed for the 2nd time now - } - this.conn = url.openConnection(); - } - inputStream = conn.getInputStream(); - return inputStream; - } - - void close() throws IOException { - try { - if (inputStream != null) { - inputStream.close(); - } else { - conn.getInputStream().close(); - } - } finally { - inputStream = null; - conn = null; - } - } - - Boolean getUseCaches() { - return useCaches; - } - - void setUseCaches(boolean useCaches) { - if (this.conn != null) { - conn.setUseCaches(useCaches); - this.useCaches = Boolean.valueOf(useCaches); - } - } - -} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/WebAppTemplateLoader.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/WebAppTemplateLoader.java b/src/main/java/freemarker/cache/WebAppTemplateLoader.java new file mode 100644 index 0000000..1ddaa68 --- /dev/null +++ b/src/main/java/freemarker/cache/WebAppTemplateLoader.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package freemarker.cache; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Objects; + +import javax.servlet.ServletContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import freemarker.template.Configuration; +import freemarker.template.utility.CollectionUtils; +import freemarker.template.utility.NullArgumentException; +import freemarker.template.utility.StringUtil; + +/** + * A {@link TemplateLoader} that uses streams reachable through {@link ServletContext#getResource(String)} as its source + * of templates. + */ +public class WebAppTemplateLoader implements TemplateLoader { + + private static final Logger LOG = LoggerFactory.getLogger("freemarker.cache"); + + private final ServletContext servletContext; + private final String subdirPath; + + private Boolean urlConnectionUsesCaches = false; + + private boolean attemptFileAccess = true; + + /** + * Creates a template loader that will use the specified servlet context to load the resources. It will use + * the base path of <code>"/"</code> meaning templates will be resolved relative to the servlet context root + * location. + * + * @param servletContext + * the servlet context whose {@link ServletContext#getResource(String)} will be used to load the + * templates. + */ + public WebAppTemplateLoader(ServletContext servletContext) { + this(servletContext, "/"); + } + + /** + * Creates a template loader that will use the specified servlet context to load the resources. It will use the + * specified base path, which is interpreted relatively to the context root (does not mater if you start it with "/" + * or not). Path components should be separated by forward slashes independently of the separator character used by + * the underlying operating system. + * + * @param servletContext + * the servlet context whose {@link ServletContext#getResource(String)} will be used to load the + * templates. + * @param subdirPath + * the base path to template resources. + */ + public WebAppTemplateLoader(ServletContext servletContext, String subdirPath) { + NullArgumentException.check("servletContext", servletContext); + NullArgumentException.check("subdirPath", subdirPath); + + subdirPath = subdirPath.replace('\\', '/'); + if (!subdirPath.endsWith("/")) { + subdirPath += "/"; + } + if (!subdirPath.startsWith("/")) { + subdirPath = "/" + subdirPath; + } + this.subdirPath = subdirPath; + this.servletContext = servletContext; + } + + /** + * Getter pair of {@link #setURLConnectionUsesCaches(Boolean)}. + * + * @since 2.3.21 + */ + public Boolean getURLConnectionUsesCaches() { + return urlConnectionUsesCaches; + } + + /** + * It does the same as {@link URLTemplateLoader#setURLConnectionUsesCaches(Boolean)}; see there. + * + * @since 2.3.21 + */ + public void setURLConnectionUsesCaches(Boolean urlConnectionUsesCaches) { + this.urlConnectionUsesCaches = urlConnectionUsesCaches; + } + + /** + * Show class name and some details that are useful in template-not-found errors. + * + * @since 2.3.21 + */ + @Override + public String toString() { + return TemplateLoaderUtils.getClassNameForToString(this) + + "(subdirPath=" + StringUtil.jQuote(subdirPath) + + ", servletContext={contextPath=" + StringUtil.jQuote(getContextPath()) + + ", displayName=" + StringUtil.jQuote(servletContext.getServletContextName()) + "})"; + } + + /** Gets the context path if we are on Servlet 2.5+, or else returns failure description string. */ + private String getContextPath() { + try { + Method m = servletContext.getClass().getMethod("getContextPath", CollectionUtils.EMPTY_CLASS_ARRAY); + return (String) m.invoke(servletContext, CollectionUtils.EMPTY_OBJECT_ARRAY); + } catch (Throwable e) { + return "[can't query before Serlvet 2.5]"; + } + } + + /** + * Getter pair of {@link #setAttemptFileAccess(boolean)}. + * + * @since 2.3.23 + */ + public boolean getAttemptFileAccess() { + return attemptFileAccess; + } + + /** + * Specifies that before loading templates with {@link ServletContext#getResource(String)}, it should try to load + * the template as {@link File}; default is {@code true}, though it's not always recommended anymore. This is a + * workaround for the case when the servlet container doesn't show template modifications after the template was + * already loaded earlier. But it's certainly better to counter this problem by disabling the URL connection cache + * with {@link #setURLConnectionUsesCaches(Boolean)}, which is also the default behavior with + * {@link Configuration#setIncompatibleImprovements(freemarker.template.Version) incompatible_improvements} 2.3.21 + * and later. + * + * @since 2.3.23 + */ + public void setAttemptFileAccess(boolean attemptLoadingFromFile) { + this.attemptFileAccess = attemptLoadingFromFile; + } + + @Override + public TemplateLoaderSession createSession() { + return null; + } + + @Override + public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom, + Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException { + WebAppTemplateLoadingSource source = createSource(name); + if (source == null) { + return TemplateLoadingResult.NOT_FOUND; + } + + if (source.url != null) { + URLConnection conn = source.url.openConnection(); + Boolean urlConnectionUsesCaches = getURLConnectionUsesCaches(); + if (urlConnectionUsesCaches != null) { + conn.setUseCaches(urlConnectionUsesCaches); + } + + // To prevent clustering issues, getLastModified(fallbackToJarLMD=false) + long lmd = URLTemplateLoader.getLastModified(conn, false); + 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, conn.getInputStream(), null); + } else { // source.file != null + long lmd = source.file.lastModified(); + 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, new FileInputStream(source.file), null); + } + } + + private WebAppTemplateLoadingSource createSource(String name) { + String fullPath = subdirPath + name; + + if (attemptFileAccess) { + // First try to open as plain file (to bypass servlet container resource caches). + try { + String realPath = servletContext.getRealPath(fullPath); + if (realPath != null) { + File file = new File(realPath); + if (file.canRead() && file.isFile()) { + return new WebAppTemplateLoadingSource(file); + } + } + } catch (SecurityException e) { + ;// ignore + } + } + + // If it fails, try to open it with servletContext.getResource. + URL url = null; + try { + url = servletContext.getResource(fullPath); + } catch (MalformedURLException e) { + if (LOG.isWarnEnabled()) { + LOG.warn("Could not retrieve resource " + StringUtil.jQuoteNoXSS(fullPath), e); + } + return null; + } + return url == null ? null : new WebAppTemplateLoadingSource(url); + } + + @Override + public void resetState() { + // Do nothing + } + + @SuppressWarnings("serial") + private class WebAppTemplateLoadingSource implements TemplateLoadingSource { + private final File file; + private final URL url; + + WebAppTemplateLoadingSource(File file) { + this.file = file; + this.url = null; + } + + WebAppTemplateLoadingSource(URL url) { + this.file = null; + this.url = url; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getOuterType().hashCode(); + result = prime * result + ((file == null) ? 0 : file.hashCode()); + result = prime * result + ((url == null) ? 0 : url.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + WebAppTemplateLoadingSource other = (WebAppTemplateLoadingSource) obj; + if (!getOuterType().equals(other.getOuterType())) + return false; + if (file == null) { + if (other.file != null) + return false; + } else if (!file.equals(other.file)) + return false; + if (url == null) { + if (other.url != null) + return false; + } else if (!url.equals(other.url)) + return false; + return true; + } + + private WebAppTemplateLoader getOuterType() { + return WebAppTemplateLoader.this; + } + + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/WebappTemplateLoader.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/WebappTemplateLoader.java b/src/main/java/freemarker/cache/WebappTemplateLoader.java deleted file mode 100644 index 57fb1f4..0000000 --- a/src/main/java/freemarker/cache/WebappTemplateLoader.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package freemarker.cache; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URL; - -import javax.servlet.ServletContext; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import freemarker.template.Configuration; -import freemarker.template.utility.CollectionUtils; -import freemarker.template.utility.StringUtil; - -/** - * A {@link TemplateLoader} that uses streams reachable through {@link ServletContext#getResource(String)} as its source - * of templates. - */ -public class WebappTemplateLoader implements TemplateLoader { - - private static final Logger LOG = LoggerFactory.getLogger("freemarker.cache"); - - private final ServletContext servletContext; - private final String subdirPath; - - private Boolean urlConnectionUsesCaches; - - private boolean attemptFileAccess = true; - - /** - * Creates a template loader that will use the specified servlet context to load the resources. It will use - * the base path of <code>"/"</code> meaning templates will be resolved relative to the servlet context root - * location. - * - * @param servletContext - * the servlet context whose {@link ServletContext#getResource(String)} will be used to load the - * templates. - */ - public WebappTemplateLoader(ServletContext servletContext) { - this(servletContext, "/"); - } - - /** - * Creates a template loader that will use the specified servlet context to load the resources. It will use the - * specified base path, which is interpreted relatively to the context root (does not mater if you start it with "/" - * or not). Path components should be separated by forward slashes independently of the separator character used by - * the underlying operating system. - * - * @param servletContext - * the servlet context whose {@link ServletContext#getResource(String)} will be used to load the - * templates. - * @param subdirPath - * the base path to template resources. - */ - public WebappTemplateLoader(ServletContext servletContext, String subdirPath) { - if (servletContext == null) { - throw new IllegalArgumentException("servletContext == null"); - } - if (subdirPath == null) { - throw new IllegalArgumentException("path == null"); - } - - subdirPath = subdirPath.replace('\\', '/'); - if (!subdirPath.endsWith("/")) { - subdirPath += "/"; - } - if (!subdirPath.startsWith("/")) { - subdirPath = "/" + subdirPath; - } - this.subdirPath = subdirPath; - this.servletContext = servletContext; - } - - @Override - public Object findTemplateSource(String name) throws IOException { - String fullPath = subdirPath + name; - - if (attemptFileAccess) { - // First try to open as plain file (to bypass servlet container resource caches). - try { - String realPath = servletContext.getRealPath(fullPath); - if (realPath != null) { - File file = new File(realPath); - if (file.canRead() && file.isFile()) { - return file; - } - } - } catch (SecurityException e) { - ;// ignore - } - } - - // If it fails, try to open it with servletContext.getResource. - URL url = null; - try { - url = servletContext.getResource(fullPath); - } catch (MalformedURLException e) { - if (LOG.isWarnEnabled()) { - LOG.warn("Could not retrieve resource " + StringUtil.jQuoteNoXSS(fullPath), e); - } - return null; - } - return url == null ? null : new URLTemplateSource(url, getURLConnectionUsesCaches()); - } - - @Override - public long getLastModified(Object templateSource) { - if (templateSource instanceof File) { - return ((File) templateSource).lastModified(); - } else { - return ((URLTemplateSource) templateSource).lastModified(); - } - } - - @Override - public Reader getReader(Object templateSource, String encoding) - throws IOException { - if (templateSource instanceof File) { - return new InputStreamReader( - new FileInputStream((File) templateSource), - encoding); - } else { - return new InputStreamReader( - ((URLTemplateSource) templateSource).getInputStream(), - encoding); - } - } - - @Override - public void closeTemplateSource(Object templateSource) throws IOException { - if (templateSource instanceof File) { - // Do nothing. - } else { - ((URLTemplateSource) templateSource).close(); - } - } - - /** - * Getter pair of {@link #setURLConnectionUsesCaches(Boolean)}. - * - * @since 2.3.21 - */ - public Boolean getURLConnectionUsesCaches() { - return urlConnectionUsesCaches; - } - - /** - * It does the same as {@link URLTemplateLoader#setURLConnectionUsesCaches(Boolean)}; see there. - * - * @since 2.3.21 - */ - public void setURLConnectionUsesCaches(Boolean urlConnectionUsesCaches) { - this.urlConnectionUsesCaches = urlConnectionUsesCaches; - } - - /** - * Show class name and some details that are useful in template-not-found errors. - * - * @since 2.3.21 - */ - @Override - public String toString() { - return TemplateLoaderUtils.getClassNameForToString(this) - + "(subdirPath=" + StringUtil.jQuote(subdirPath) - + ", servletContext={contextPath=" + StringUtil.jQuote(getContextPath()) - + ", displayName=" + StringUtil.jQuote(servletContext.getServletContextName()) + "})"; - } - - /** Gets the context path if we are on Servlet 2.5+, or else returns failure description string. */ - private String getContextPath() { - try { - Method m = servletContext.getClass().getMethod("getContextPath", CollectionUtils.EMPTY_CLASS_ARRAY); - return (String) m.invoke(servletContext, CollectionUtils.EMPTY_OBJECT_ARRAY); - } catch (Throwable e) { - return "[can't query before Serlvet 2.5]"; - } - } - - /** - * Getter pair of {@link #setAttemptFileAccess(boolean)}. - * - * @since 2.3.23 - */ - public boolean getAttemptFileAccess() { - return attemptFileAccess; - } - - /** - * Specifies that before loading templates with {@link ServletContext#getResource(String)}, it should try to load - * the template as {@link File}; default is {@code true}, though it's not always recommended anymore. This is a - * workaround for the case when the servlet container doesn't show template modifications after the template was - * already loaded earlier. But it's certainly better to counter this problem by disabling the URL connection cache - * with {@link #setURLConnectionUsesCaches(Boolean)}, which is also the default behavior with - * {@link Configuration#setIncompatibleImprovements(freemarker.template.Version) incompatible_improvements} 2.3.21 - * and later. - * - * @since 2.3.23 - */ - public void setAttemptFileAccess(boolean attemptLoadingFromFile) { - this.attemptFileAccess = attemptLoadingFromFile; - } - -} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/core/BuiltInsForStringsMisc.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java index 1b67eed..4d7634b 100644 --- a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java +++ b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java @@ -80,7 +80,7 @@ class BuiltInsForStringsMisc { } FMParser parser = new FMParser( - parentTemplate, false, tkMan, pCfg); + parentTemplate, false, tkMan, pCfg, TemplateSpecifiedEncodingHandler.DEFAULT); exp = parser.Expression(); } catch (TokenMgrError e) { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/core/Configurable.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java index 5dc7c49..b5486ee 100644 --- a/src/main/java/freemarker/core/Configurable.java +++ b/src/main/java/freemarker/core/Configurable.java @@ -2145,7 +2145,7 @@ public class Configurable { * <li><p>{@code "template_loader"}: * See: {@link Configuration#setTemplateLoader(TemplateLoader)}. * <br>String value: {@code "default"} (case insensitive) for the default, or else interpreted as an - * <a href="#fm_obe">object builder expression</a>. + * <a href="#fm_obe">object builder expression</a>. {@code "null"} is also allowed. * * <li><p>{@code "template_lookup_strategy"}: * See: {@link Configuration#setTemplateLookupStrategy(freemarker.cache.TemplateLookupStrategy)}. http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/core/Environment.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java index 740a9a9..7e61587 100644 --- a/src/main/java/freemarker/core/Environment.java +++ b/src/main/java/freemarker/core/Environment.java @@ -2416,7 +2416,7 @@ public final class Environment extends Configurable { } /** - * Emulates <code>include</code> directive, except that <code>name</code> must be tempate root relative. + * Emulates <code>include</code> directive, except that <code>name</code> must be template root relative. * * <p> * It's the same as <code>include(getTemplateForInclusion(name, encoding, parse))</code>. But, you may want to @@ -2488,7 +2488,7 @@ public final class Environment extends Configurable { private String getIncludedTemplateEncoding() { String encoding; - // This branch shouldn't exist, as it doesn't make much sense to inherit encoding. But we have to keep BC. + // [FM3] This branch shouldn't exist, as it doesn't make much sense to inherit encoding. But we have to keep BC. encoding = getTemplate().getEncoding(); if (encoding == null) { encoding = configuration.getEncoding(this.getLocale()); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/core/MarkReleaserTemplateSpecifiedEncodingHandler.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/MarkReleaserTemplateSpecifiedEncodingHandler.java b/src/main/java/freemarker/core/MarkReleaserTemplateSpecifiedEncodingHandler.java new file mode 100644 index 0000000..9fb8050 --- /dev/null +++ b/src/main/java/freemarker/core/MarkReleaserTemplateSpecifiedEncodingHandler.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package freemarker.core; + +import java.io.InputStream; + +import freemarker.template.Template.WrongEncodingException; + +/** + * A {@link TemplateSpecifiedEncodingHandler} that discards the mark of the specified {@link InputStream} when + * the template parsing gets to a point where it's known that we can't receive a template specified encoding anymore. + * This allows freeing up the mark buffer early during parsing. + * + * @since 2.3.16 + */ +public class MarkReleaserTemplateSpecifiedEncodingHandler implements TemplateSpecifiedEncodingHandler { + + private final InputStream markedInputStream; + + /** + * @param markedInputStream Input stream with marked template content start position + */ + public MarkReleaserTemplateSpecifiedEncodingHandler(InputStream markedInputStream) { + this.markedInputStream = markedInputStream; + } + + public void handle(String templateSpecificEncoding, String constructorSpecifiedEncoding) + throws WrongEncodingException { + TemplateSpecifiedEncodingHandler.DEFAULT.handle(templateSpecificEncoding, constructorSpecifiedEncoding); + + // There was no WrongEncodingException exception, release mark buffer: + markedInputStream.mark(0); // + } + + public InputStream getMarkedInputStream() { + return markedInputStream; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/core/StringLiteral.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/StringLiteral.java b/src/main/java/freemarker/core/StringLiteral.java index da1c1bd..a81f726 100644 --- a/src/main/java/freemarker/core/StringLiteral.java +++ b/src/main/java/freemarker/core/StringLiteral.java @@ -63,7 +63,8 @@ final class StringLiteral extends Expression implements TemplateScalarModel { FMParserTokenManager tkMan = new FMParserTokenManager( simpleCharacterStream); - FMParser parser = new FMParser(parentTemplate, false, tkMan, pcfg); + FMParser parser = new FMParser(parentTemplate, false, tkMan, pcfg, + TemplateSpecifiedEncodingHandler.DEFAULT); // We continue from the parent parser's current state: parser.setupStringLiteralMode(parentTkMan, outputFormat); try { @@ -129,6 +130,7 @@ final class StringLiteral extends Expression implements TemplateScalarModel { } } + @Override public String getAsString() { return value; } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/core/TemplateConfiguration.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/TemplateConfiguration.java b/src/main/java/freemarker/core/TemplateConfiguration.java index 353d453..019f547 100644 --- a/src/main/java/freemarker/core/TemplateConfiguration.java +++ b/src/main/java/freemarker/core/TemplateConfiguration.java @@ -49,7 +49,7 @@ import freemarker.template.utility.NullArgumentException; * <p> * Note that the result value of the reader methods (getter and "is" methods) is usually not useful unless the value of * that setting was already set on this object. Otherwise you will get the value from the parent {@link Configuration}, - * which is {@link Configuration#getDefaultConfiguration()} before this object is associated to a {@link Configuration}. + * or an {@link IllegalStateException} before this object is associated to a {@link Configuration}. * * <p> * If you are using this class for your own template loading and caching solution, rather than with the standard one, @@ -146,6 +146,11 @@ public final class TemplateConfiguration extends Configurable implements ParserC public Configuration getParentConfiguration() { return parentConfigurationSet ? (Configuration) getParent() : null; } + + private Configuration getNonNullParentConfiguration() { + checkParentConfigurationSet(); + return (Configuration) getParent(); + } /** * Set all settings in this {@link TemplateConfiguration} that were set in the parameter @@ -271,8 +276,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC * If the parent configuration wasn't yet set. */ public void apply(Template template) { - checkParentConfigurationSet(); - Configuration cfg = getParentConfiguration(); + Configuration cfg = getNonNullParentConfiguration(); if (template.getConfiguration() != cfg) { // This is actually not a problem right now, but for future BC we enforce this. throw new IllegalArgumentException( @@ -377,7 +381,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC */ @Override public int getTagSyntax() { - return tagSyntax != null ? tagSyntax.intValue() : getParentConfiguration().getTagSyntax(); + return tagSyntax != null ? tagSyntax.intValue() : getNonNullParentConfiguration().getTagSyntax(); } /** @@ -400,7 +404,8 @@ public final class TemplateConfiguration extends Configurable implements ParserC */ @Override public int getNamingConvention() { - return namingConvention != null ? namingConvention.intValue() : getParentConfiguration().getNamingConvention(); + return namingConvention != null ? namingConvention.intValue() + : getNonNullParentConfiguration().getNamingConvention(); } /** @@ -423,7 +428,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC @Override public boolean getWhitespaceStripping() { return whitespaceStripping != null ? whitespaceStripping.booleanValue() - : getParentConfiguration().getWhitespaceStripping(); + : getNonNullParentConfiguration().getWhitespaceStripping(); } /** @@ -447,7 +452,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC @Override public int getAutoEscapingPolicy() { return autoEscapingPolicy != null ? autoEscapingPolicy.intValue() - : getParentConfiguration().getAutoEscapingPolicy(); + : getNonNullParentConfiguration().getAutoEscapingPolicy(); } /** @@ -470,7 +475,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC */ @Override public OutputFormat getOutputFormat() { - return outputFormat != null ? outputFormat : getParentConfiguration().getOutputFormat(); + return outputFormat != null ? outputFormat : getNonNullParentConfiguration().getOutputFormat(); } /** @@ -493,7 +498,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC @Override public boolean getRecognizeStandardFileExtensions() { return recognizeStandardFileExtensions != null ? recognizeStandardFileExtensions.booleanValue() - : getParentConfiguration().getRecognizeStandardFileExtensions(); + : getNonNullParentConfiguration().getRecognizeStandardFileExtensions(); } /** @@ -502,7 +507,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC public boolean isRecognizeStandardFileExtensionsSet() { return recognizeStandardFileExtensions != null; } - + @Override public void setStrictBeanModels(boolean strict) { throw new UnsupportedOperationException( @@ -510,7 +515,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC } public String getEncoding() { - return encoding != null ? encoding : getParentConfiguration().getDefaultEncoding(); + return encoding != null ? encoding : getNonNullParentConfiguration().getDefaultEncoding(); } /** @@ -552,7 +557,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC @Override public int getTabSize() { return tabSize != null ? tabSize.intValue() - : getParentConfiguration().getTabSize(); + : getNonNullParentConfiguration().getTabSize(); } /** @@ -573,8 +578,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC */ @Override public Version getIncompatibleImprovements() { - checkParentConfigurationSet(); - return getParentConfiguration().getIncompatibleImprovements(); + return getNonNullParentConfiguration().getIncompatibleImprovements(); } private void checkParentConfigurationSet() { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/core/TemplateSpecifiedEncodingHandler.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/TemplateSpecifiedEncodingHandler.java b/src/main/java/freemarker/core/TemplateSpecifiedEncodingHandler.java new file mode 100644 index 0000000..2c3f0bf --- /dev/null +++ b/src/main/java/freemarker/core/TemplateSpecifiedEncodingHandler.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 freemarker.core; + +import freemarker.template.Template; +import freemarker.template.Template.WrongEncodingException; + +/** + * Specifies the behavior when the template specifies its own encoding (via {@code <#ftl encoding=...>}) in the template + * content itself, and also when it doesn't do that. + */ +public interface TemplateSpecifiedEncodingHandler { + + TemplateSpecifiedEncodingHandler DEFAULT = new TemplateSpecifiedEncodingHandler() { + + @Override + public void handle(String templateSpecificEncoding, String constructorSpecifiedEncoding) + throws WrongEncodingException { + if (constructorSpecifiedEncoding != null && templateSpecificEncoding != null + && !constructorSpecifiedEncoding.equalsIgnoreCase(templateSpecificEncoding)) { + throw new Template.WrongEncodingException(templateSpecificEncoding, constructorSpecifiedEncoding); + } + } + + }; + + /** + * Called once during template parsing, either when the {@code #ftl} directive is processed, or near the beginning + * of the template processing when there's no {@code #ftl} directive in the template. + * + * @param templateSpecificEncoding + * The encoding specified via {@code <#ftl encoding=...>}, or {@code null} if that was missing (either + * the {@code encoding} parameter or the whole {@code #ftl} directive). + * @param constructorSpecifiedEncoding + * The encoding specified to the {@link Template} constructor; also the value of + * {@link Template#getEncoding()}. If there was an encoding used for decoding the template file, it + * should be that, or if there was no encoding, it should be {@code null}. + * + * @throws WrongEncodingException + * If the template "file" has to be re-read and the {@link Template} re-created with the encoding + * specified in the constructor of {@link WrongEncodingException}. + */ + void handle(String templateSpecificEncoding, String constructorSpecifiedEncoding) throws WrongEncodingException; + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/core/_CoreAPI.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java index 56a81b6..9f75ac5 100644 --- a/src/main/java/freemarker/core/_CoreAPI.java +++ b/src/main/java/freemarker/core/_CoreAPI.java @@ -215,8 +215,9 @@ public class _CoreAPI { return te.getChild(index); } - public static FMParser newFMParser(Template template, Reader reader, ParserConfiguration pCfg) { - return new FMParser(template, reader, pCfg); + public static FMParser newFMParser(Template template, Reader reader, ParserConfiguration pCfg, + TemplateSpecifiedEncodingHandler templateSpecifiedEncodingHandler) { + return new FMParser(template, reader, pCfg, templateSpecifiedEncodingHandler); } } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/ext/servlet/FreemarkerServlet.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/ext/servlet/FreemarkerServlet.java b/src/main/java/freemarker/ext/servlet/FreemarkerServlet.java index ea78ac4..cf15f02 100644 --- a/src/main/java/freemarker/ext/servlet/FreemarkerServlet.java +++ b/src/main/java/freemarker/ext/servlet/FreemarkerServlet.java @@ -22,6 +22,7 @@ package freemarker.ext.servlet; import java.io.IOException; import java.io.Writer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -48,7 +49,7 @@ import freemarker.cache.ClassTemplateLoader; import freemarker.cache.FileTemplateLoader; import freemarker.cache.MultiTemplateLoader; import freemarker.cache.TemplateLoader; -import freemarker.cache.WebappTemplateLoader; +import freemarker.cache.WebAppTemplateLoader; import freemarker.core.Configurable; import freemarker.core.Environment; import freemarker.core.OutputFormat; @@ -129,8 +130,8 @@ import freemarker.template.utility.StringUtil; * Any of the above can have a {@code ?setting(name=value, ...)} postfix to set the JavaBeans properties of the * {@link TemplateLoader} created. For example, * {@code /templates?settings(attemptFileAccess=false, URLConnectionUsesCaches=true)} calls - * {@link WebappTemplateLoader#setAttemptFileAccess(boolean)} and - * {@link WebappTemplateLoader#setURLConnectionUsesCaches(Boolean)} to tune the {@link WebappTemplateLoader}. For + * {@link WebAppTemplateLoader#setAttemptFileAccess(boolean)} and + * {@link WebAppTemplateLoader#setURLConnectionUsesCaches(Boolean)} to tune the {@link WebAppTemplateLoader}. For * backward compatibility (not recommended!), you can use the {@code class://} prefix, like in * <tt>class://com/example/templates</tt> format, which is similar to {@code classpath:}, except that it uses the * defining class loader of this servlet's class. This can cause template-not-found errors, if that class (in @@ -206,8 +207,9 @@ import freemarker.template.utility.StringUtil; * <li>{@value #INIT_PARAM_VALUE_FROM_TEMPLATE}: This should be used in most applications, but it's not the default for * backward compatibility. It reads the {@link Configurable#getOutputEncoding()} setting of the template (note that the * template usually just inherits that from the {@link Configuration}), and if that's not set, then reads the source - * charset of the template, just like {@value #INIT_PARAM_VALUE_LEGACY}. Then it passes the charset acquired this way to - * {@link HttpServletResponse#setCharacterEncoding(String)} and {@link Environment#setOutputEncoding(String)}. (It + * charset of the template, just like {@value #INIT_PARAM_VALUE_LEGACY}, and if that's {@code null} (which happens if + * the template was loaded from a non-binary source) then it will be UTF-8. Then it passes the charset acquired this way + * to {@link HttpServletResponse#setCharacterEncoding(String)} and {@link Environment#setOutputEncoding(String)}. (It * doesn't call the legacy {@link HttpServletResponse#setContentType(String)} API to set the charset.) (Note that if the * template has a {@code content_type} template attribute (which is deprecated) that specifies a charset, it will be * used as the output charset of that template.) @@ -747,7 +749,7 @@ public class FreemarkerServlet extends HttpServlet { /** * Create the template loader. The default implementation will create a {@link ClassTemplateLoader} if the template * path starts with {@code "class://"}, a {@link FileTemplateLoader} if the template path starts with - * {@code "file://"}, and a {@link WebappTemplateLoader} otherwise. Also, if + * {@code "file://"}, and a {@link WebAppTemplateLoader} otherwise. Also, if * {@link Configuration#Configuration(freemarker.template.Version) incompatible_improvements} is 2.3.22 or higher, * it will create a {@link MultiTemplateLoader} if the template path starts with {@code "["}. * @@ -839,7 +841,7 @@ public class FreemarkerServlet extends HttpServlet { if (responseCharacterEncoding == ResponseCharacterEncoding.LEGACY && !contentType.containsCharset) { // In legacy mode we don't call response.setCharacterEncoding, so the charset must be set here: response.setContentType( - contentType.httpHeaderValue + "; charset=" + getTemplateSpecificOutputEncoding(template)); + contentType.httpHeaderValue + "; charset=" + getOutputEncodingForTemplate(template)); } else { response.setContentType(contentType.httpHeaderValue); } @@ -851,7 +853,7 @@ public class FreemarkerServlet extends HttpServlet { // Using the Servlet 2.4 way of setting character encoding. if (responseCharacterEncoding != ResponseCharacterEncoding.FORCE_CHARSET) { if (!tempSpecContentTypeContainsCharset) { - response.setCharacterEncoding(getTemplateSpecificOutputEncoding(template)); + response.setCharacterEncoding(getOutputEncodingForTemplate(template)); } } else { response.setCharacterEncoding(forcedResponseCharacterEncoding.name()); @@ -911,10 +913,13 @@ public class FreemarkerServlet extends HttpServlet { env.process(); } - private String getTemplateSpecificOutputEncoding(Template template) { + private String getOutputEncodingForTemplate(Template template) { String outputEncoding = responseCharacterEncoding == ResponseCharacterEncoding.LEGACY ? null : template.getOutputEncoding(); - return outputEncoding != null ? outputEncoding : template.getEncoding(); + // [FM3] Don't use template.getEncoding() here; it might can't encode the dynamic values inserted. + return outputEncoding != null ? outputEncoding + : template.getEncoding() != null ? template.getEncoding() + : StandardCharsets.UTF_8.name(); } private ContentType getTemplateSpecificContentType(final Template template) { @@ -928,7 +933,9 @@ public class FreemarkerServlet extends HttpServlet { if (outputFormatMimeType != null) { if (responseCharacterEncoding == ResponseCharacterEncoding.LEGACY) { // In legacy mode we won't call serlvetResponse.setCharacterEncoding(...), so: - return new ContentType(outputFormatMimeType + "; charset=" + getTemplateSpecificOutputEncoding(template), true); + return new ContentType( + outputFormatMimeType + "; charset=" + getOutputEncodingForTemplate(template), + true); } else { return new ContentType(outputFormatMimeType, false); } @@ -1630,6 +1637,7 @@ public class FreemarkerServlet extends HttpServlet { } private enum ResponseCharacterEncoding implements InitParamValueEnum { + // [FM3] Get rid of LEGACY LEGACY(INIT_PARAM_VALUE_LEGACY), FROM_TEMPLATE(INIT_PARAM_VALUE_FROM_TEMPLATE), DO_NOT_SET(INIT_PARAM_VALUE_DO_NOT_SET), http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/ext/servlet/InitParamParser.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/ext/servlet/InitParamParser.java b/src/main/java/freemarker/ext/servlet/InitParamParser.java index c7c4f98..feb5b98 100644 --- a/src/main/java/freemarker/ext/servlet/InitParamParser.java +++ b/src/main/java/freemarker/ext/servlet/InitParamParser.java @@ -34,7 +34,7 @@ import freemarker.cache.ClassTemplateLoader; import freemarker.cache.FileTemplateLoader; import freemarker.cache.MultiTemplateLoader; import freemarker.cache.TemplateLoader; -import freemarker.cache.WebappTemplateLoader; +import freemarker.cache.WebAppTemplateLoader; import freemarker.core._ObjectBuilderSettingEvaluator; import freemarker.core._SettingEvaluationEnvironment; import freemarker.template.Configuration; @@ -101,7 +101,7 @@ final class InitParamParser { && cfg.getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_3_22) { throw new TemplatePathParsingException("Template paths starting with \"{\" are reseved for future purposes"); } else { - templateLoader = new WebappTemplateLoader(srvCtx, pureTemplatePath); + templateLoader = new WebAppTemplateLoader(srvCtx, pureTemplatePath); } if (settingAssignmentsStart != -1) { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/template/Configuration.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java index ea188dd..b2f9b0e 100644 --- a/src/main/java/freemarker/template/Configuration.java +++ b/src/main/java/freemarker/template/Configuration.java @@ -845,7 +845,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf private void createTemplateCache() { cache = new TemplateCache( - getDefaultTemplateLoader(), + null, getDefaultCacheStorage(), getDefaultTemplateLookupStrategy(), getDefaultTemplateNameFormat(), @@ -862,7 +862,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf TemplateCache oldCache = cache; cache = new TemplateCache( loader, storage, templateLookupStrategy, templateNameFormat, templateConfigurations, this); - cache.clear(); // for fully BC behavior + cache.clear(false); cache.setDelay(oldCache.getDelay()); cache.setLocalizedLookup(localizedLookup); } @@ -873,41 +873,6 @@ public class Configuration extends Configurable implements Cloneable, ParserConf getTemplateConfigurations()); } - private TemplateLoader getDefaultTemplateLoader() { - return createDefaultTemplateLoader(getIncompatibleImprovements(), getTemplateLoader()); - } - - static TemplateLoader createDefaultTemplateLoader(Version incompatibleImprovements) { - return createDefaultTemplateLoader(incompatibleImprovements, null); - } - - private static TemplateLoader createDefaultTemplateLoader( - Version incompatibleImprovements, TemplateLoader existingTemplateLoader) { - if (incompatibleImprovements.intValue() < _TemplateAPI.VERSION_INT_2_3_21) { - if (existingTemplateLoader instanceof LegacyDefaultFileTemplateLoader) { - return existingTemplateLoader; - } - try { - return new LegacyDefaultFileTemplateLoader(); - } catch (Exception e) { - LOG.warn("Couldn't create legacy default TemplateLoader which accesses the current directory. " - + "(Use new Configuration(Configuration.VERSION_2_3_21) or higher to avoid this.)", e); - return null; - } - } else { - return null; - } - } - - // [FM3] Remove - private static class LegacyDefaultFileTemplateLoader extends FileTemplateLoader { - - public LegacyDefaultFileTemplateLoader() throws IOException { - super(); - } - - } - private TemplateLookupStrategy getDefaultTemplateLookupStrategy() { return getDefaultTemplateLookupStrategy(getIncompatibleImprovements()); } @@ -1179,7 +1144,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf */ public void unsetTemplateLoader() { if (templateLoaderExplicitlySet) { - setTemplateLoader(getDefaultTemplateLoader()); + setTemplateLoader(null); templateLoaderExplicitlySet = false; } } @@ -1425,9 +1390,9 @@ public class Configuration extends Configurable implements Cloneable, ParserConf /** * Sets the servlet context from which to load templates. - * This is equivalent to {@code setTemplateLoader(new WebappTemplateLoader(sctxt, path))} - * or {@code setTemplateLoader(new WebappTemplateLoader(sctxt))} if {@code path} was - * {@code null}, so see {@code freemarker.cache.WebappTemplateLoader} for more details. + * This is equivalent to {@code setTemplateLoader(new WebAppTemplateLoader(sctxt, path))} + * or {@code setTemplateLoader(new WebAppTemplateLoader(sctxt))} if {@code path} was + * {@code null}, so see {@code freemarker.cache.WebAppTemplateLoader} for more details. * * @param servletContext the {@code javax.servlet.ServletContext} object. (The declared type is {@link Object} * to prevent class loading error when using FreeMarker in an environment where @@ -1439,7 +1404,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf public void setServletContextForTemplateLoading(Object servletContext, String path) { try { // Don't introduce linking-time dependency on servlets - final Class webappTemplateLoaderClass = ClassUtil.forName("freemarker.cache.WebappTemplateLoader"); + final Class webappTemplateLoaderClass = ClassUtil.forName("freemarker.cache.WebAppTemplateLoader"); // Don't introduce linking-time dependency on servlets final Class servletContextClass = ClassUtil.forName("javax.servlet.ServletContext"); @@ -2943,7 +2908,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf unsetTemplateLoader(); } else { setTemplateLoader((TemplateLoader) _ObjectBuilderSettingEvaluator.eval( - value, TemplateLoader.class, false, _SettingEvaluationEnvironment.getCurrent())); + value, TemplateLoader.class, true, _SettingEvaluationEnvironment.getCurrent())); } } else if (TEMPLATE_LOOKUP_STRATEGY_KEY_SNAKE_CASE.equals(name) || TEMPLATE_LOOKUP_STRATEGY_KEY_CAMEL_CASE.equals(name)) { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/template/Template.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java index 31149f9..ac9bde2 100644 --- a/src/main/java/freemarker/template/Template.java +++ b/src/main/java/freemarker/template/Template.java @@ -50,10 +50,12 @@ import freemarker.core.ParseException; import freemarker.core.ParserConfiguration; import freemarker.core.TemplateConfiguration; import freemarker.core.TemplateElement; +import freemarker.core.TemplateSpecifiedEncodingHandler; import freemarker.core.TextBlock; import freemarker.core.TokenMgrError; import freemarker.core._CoreAPI; import freemarker.debug.impl.DebuggerService; +import freemarker.template.utility.NullArgumentException; /** * <p> @@ -136,11 +138,7 @@ public class Template extends Configurable { /** * Convenience constructor for {@link #Template(String, String, Reader, Configuration, String) Template(name, null, * reader, cfg, encoding)}. - * - * @deprecated In most applications, use {@link #Template(String, Reader, Configuration)} instead, which doesn't - * specify the encoding. */ - @Deprecated public Template(String name, Reader reader, Configuration cfg, String encoding) throws IOException { this(name, null, reader, cfg, encoding); } @@ -168,7 +166,9 @@ public class Template extends Configurable { * See {@link #getSourceName()} for the meaning. Can be {@code null}, in which case * {@link #getSourceName()} will return the same as {@link #getName()}. * @param reader - * The character stream to read from. It will always be closed ({@link Reader#close()}) by this method. + * The character stream to read from. The {@link Reader} is <em>not</em> closed by this method (unlike + * in FreeMarker 2.x.x), so be sure that it's closed somewhere. (Except of course, readers like + * {@link StringReader} need not be closed.) * @param cfg * The Configuration object that this Template is associated with. If this is {@code null}, the "default" * {@link Configuration} object is used, which is highly discouraged, because it can easily lead to @@ -186,20 +186,17 @@ public class Template extends Configurable { * recommended). * * @param encoding - * This is the encoding that we are supposed to be using. But it's not really necessary because we have a - * {@link Reader} which is already decoded, but it's kept as meta-info. It also has an impact when - * {@code #include}-ing/{@code #import}-ing another template from this template, as its default encoding - * will be this. But this behavior of said directives is considered to be harmful, and will be probably - * phased out. Until that, it's better to leave this on {@code null}, so that the encoding will come from - * the {@link Configuration}. Note that if this is non-{@code null} and there's an {@code #ftl} header - * with encoding, they must match, or else a {@link WrongEncodingException} is thrown. - * - * @deprecated In most applications, use {@link #Template(String, String, Reader, Configuration)} instead, which - * doesn't specify the encoding. + * This is the encoding that we are supposed to be using. At the first glance it's unnecessary because we + * already have a {@link Reader} (so decoding with the charset has already happened), however, if this is + * non-{@code null} and there's an {@code #ftl} header with {@code encoding} parameter, they must match, + * or else a {@link WrongEncodingException} is thrown. Thus, it should be set if to decode the template, + * we were using an encoding (a charset), otherwise it should be {@code null}. It's also kept as + * meta-info (returned by {@link #getEncoding()}). It also has an impact when {@code #include}-ing or + * {@code #import}-ing another template from this template, as its default encoding will be this. But + * this behavior of said directives is considered to be harmful, and will be probably phased out. * * @since 2.3.22 */ - @Deprecated public Template( String name, String sourceName, Reader reader, Configuration cfg, String encoding) throws IOException { this(name, sourceName, reader, cfg, null, encoding); @@ -223,18 +220,35 @@ public class Template extends Configurable { * {@link Configurable} settings will be set too, because this constructor only uses it as a * {@link ParserConfiguration}. * @param encoding - * Same as in {@link #Template(String, String, Reader, Configuration, String)}. When it's non-{@code - * null}, it overrides the value coming from the {@link TemplateConfiguration#getEncoding()} method of - * the {@code templateConfiguration} parameter. + * Same as in {@link #Template(String, String, Reader, Configuration, String)}. * * @since 2.3.24 */ - public Template( - String name, String sourceName, Reader reader, - Configuration cfg, ParserConfiguration customParserConfiguration, - String encoding) throws IOException { + public Template( + String name, String sourceName, Reader reader, + Configuration cfg, ParserConfiguration customParserConfiguration, + String encoding) throws IOException { + this(name, sourceName, reader, cfg, customParserConfiguration, encoding, + TemplateSpecifiedEncodingHandler.DEFAULT); + } + + /** + * Same as {@link #Template(String, String, Reader, Configuration, ParserConfiguration, String)}, but allows + * specifying a non-default (non-{@link TemplateSpecifiedEncodingHandler#DEFAULT}) behavior regarding encoding + * specified in the template content. + * + * @param templateSpecifiedEncodingHandler Not {@code null}. + * + * @since 2.3.26 + */ + public Template( + String name, String sourceName, Reader reader, + Configuration cfg, ParserConfiguration customParserConfiguration, + String encoding, TemplateSpecifiedEncodingHandler templateSpecifiedEncodingHandler) throws IOException { this(name, sourceName, cfg, customParserConfiguration); - + + NullArgumentException.check("templateSpecifiedEncodingHandler", templateSpecifiedEncodingHandler); + this.setEncoding(encoding); LineTableBuilder ltbReader; try { @@ -247,7 +261,8 @@ public class Template extends Configurable { reader = ltbReader; try { - parser = _CoreAPI.newFMParser(this, reader, actualParserConfiguration); + parser = _CoreAPI.newFMParser( + this, reader, actualParserConfiguration, templateSpecifiedEncodingHandler); try { this.rootElement = parser.Root(); } catch (IndexOutOfBoundsException exc) { @@ -271,8 +286,6 @@ public class Template extends Configurable { } catch (ParseException e) { e.setTemplateName(getSourceName()); throw e; - } finally { - reader.close(); } // Throws any exception that JavaCC has silently treated as EOF: @@ -606,7 +619,8 @@ public class Template extends Configurable { } /** - * Returns the default character encoding used for reading included files. + * Returns the default character encoding used for reading included/imported files; if {@code null}, then + * the encoding returned by {@link Configuration#getEncoding(java.util.Locale)} should be used instead. */ public String getEncoding() { return this.encoding; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/template/_TemplateAPI.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java index 52606f6..bf6b227 100644 --- a/src/main/java/freemarker/template/_TemplateAPI.java +++ b/src/main/java/freemarker/template/_TemplateAPI.java @@ -22,7 +22,6 @@ package freemarker.template; import java.util.Set; import freemarker.cache.CacheStorage; -import freemarker.cache.TemplateLoader; import freemarker.cache.TemplateLookupStrategy; import freemarker.cache.TemplateNameFormat; import freemarker.core.Expression; @@ -83,10 +82,6 @@ public class _TemplateAPI { return Configuration.getDefaultLogTemplateExceptions(incompatibleImprovements); } - public static TemplateLoader createDefaultTemplateLoader(Version incompatibleImprovements) { - return Configuration.createDefaultTemplateLoader(incompatibleImprovements); - } - public static CacheStorage createDefaultCacheStorage(Version incompatibleImprovements) { return Configuration.createDefaultCacheStorage(incompatibleImprovements); }
