/* * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky * * Licensed 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 freemarker.log.Logger; 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 = Logger.getLogger("freemarker.cache"); private final ServletContext servletContext; private final String subdirPath; private Boolean urlConnectionUsesCaches; private boolean attemptFileAccess = true; /** * Creates a resource template cache 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; } 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) { LOG.warn("Could not retrieve resource " + StringUtil.jQuoteNoXSS(fullPath), e); return null; } return url == null ? null : new URLTemplateSource(url, getURLConnectionUsesCaches()); } public long getLastModified(Object templateSource) { if (templateSource instanceof File) { return ((File) templateSource).lastModified(); } else { return ((URLTemplateSource) templateSource).lastModified(); } } 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); } } 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; } }