| /* |
| * Copyright (c) 2003 The Visigoth Software Society. All rights |
| * reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in |
| * the documentation and/or other materials provided with the |
| * distribution. |
| * |
| * 3. The end-user documentation included with the redistribution, if |
| * any, must include the following acknowledgement: |
| * "This product includes software developed by the |
| * Visigoth Software Society (http://www.visigoths.org/)." |
| * Alternately, this acknowledgement may appear in the software itself, |
| * if and wherever such third-party acknowledgements normally appear. |
| * |
| * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the |
| * project contributors may be used to endorse or promote products derived |
| * from this software without prior written permission. For written |
| * permission, please contact visigoths@visigoths.org. |
| * |
| * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth" |
| * nor may "FreeMarker" or "Visigoth" appear in their names |
| * without prior written permission of the Visigoth Software Society. |
| * |
| * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR |
| * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
| * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT |
| * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
| * SUCH DAMAGE. |
| * ==================================================================== |
| * |
| * This software consists of voluntary contributions made by many |
| * individuals on behalf of the Visigoth Software Society. For more |
| * information on the Visigoth Software Society, please see |
| * http://www.visigoths.org/ |
| */ |
| |
| package freemarker.cache; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.io.Serializable; |
| import java.io.StringWriter; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.StringTokenizer; |
| |
| import freemarker.core.Environment; |
| import freemarker.log.Logger; |
| import freemarker.template.Configuration; |
| import freemarker.template.Template; |
| import freemarker.template.utility.StringUtil; |
| import freemarker.template.utility.UndeclaredThrowableException; |
| |
| /** |
| * 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)}. |
| * |
| * @author Attila Szegedi |
| */ |
| public class TemplateCache |
| { |
| private static final String ASTERISKSTR = "*"; |
| private static final String LOCALE_SEPARATOR = "_"; |
| private static final char ASTERISK = '*'; |
| private static final String CURRENT_DIR_PATH_PREFIX = "./"; |
| private static final String CURRENT_DIR_PATH = "/./"; |
| private static final String PARENT_DIR_PATH_PREFIX = "../"; |
| private static final String PARENT_DIR_PATH = "/../"; |
| private static final char SLASH = '/'; |
| private static final Logger logger = Logger.getLogger("freemarker.cache"); |
| |
| private final TemplateLoader templateLoader; |
| |
| /** Here we keep our cached templates */ |
| private final CacheStorage storage; |
| private final boolean isStorageConcurrent; |
| /** The default refresh delay in milliseconds. */ |
| private long delay = 5000; |
| /** Specifies if localized template lookup is enabled or not */ |
| private boolean localizedLookup = true; |
| |
| private Configuration config; |
| |
| /** |
| * Returns a template cache that will first try to load a template from |
| * the file system relative to the current user directory (i.e. the value |
| * of the system property <code>user.dir</code>), then from the classpath. |
| * |
| * @deprecated Use {@link #TemplateCache(TemplateLoader)} instead. The default loader is useless in most |
| * applications, also it can mean a security risk. |
| */ |
| public TemplateCache() |
| { |
| this(createDefaultTemplateLoader()); |
| } |
| |
| private static TemplateLoader createDefaultTemplateLoader() { |
| try { |
| return new FileTemplateLoader(); |
| } catch(Exception e) { |
| logger.warn("Could not create a file template loader for current directory", e); |
| return null; |
| } |
| } |
| |
| /** |
| * Creates a new template cache with a custom template loader that is used |
| * to load the templates. |
| * @param templateLoader the template loader to use. |
| */ |
| public TemplateCache(TemplateLoader templateLoader) |
| { |
| this(templateLoader, new SoftCacheStorage()); |
| } |
| |
| /** |
| * Creates a new template cache with a custom template loader that is used |
| * to load the templates. |
| * @param templateLoader the template loader to use. |
| */ |
| public TemplateCache(TemplateLoader templateLoader, CacheStorage cacheStorage) |
| { |
| this.templateLoader = templateLoader; |
| this.storage = cacheStorage; |
| if(cacheStorage == null) |
| { |
| throw new IllegalArgumentException("storage == null"); |
| } |
| isStorageConcurrent = cacheStorage instanceof ConcurrentCacheStorage && |
| ((ConcurrentCacheStorage)cacheStorage).isConcurrent(); |
| } |
| |
| /** |
| * Sets the configuration object to which this cache belongs. This |
| * method is called by the configuration itself to establish the |
| * relation, and should not be called by users. |
| */ |
| public void setConfiguration(Configuration config) |
| { |
| this.config = config; |
| clear(); |
| } |
| |
| public TemplateLoader getTemplateLoader() |
| { |
| return templateLoader; |
| } |
| |
| public CacheStorage getCacheStorage() |
| { |
| return storage; |
| } |
| |
| /** |
| * 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>For the meaning of the parameters see {@link Configuration#getTemplate(String, Locale, String, boolean)}. |
| * |
| * @return the loaded template, or {@code null} if the template was not found. |
| */ |
| public Template getTemplate(String name, Locale locale, String encoding, boolean parseAsFTL) |
| 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 = normalizeName(name); |
| if(name == null) { |
| return null; |
| } |
| Template result = null; |
| if (templateLoader != null) { |
| result = getTemplate(templateLoader, name, locale, encoding, parseAsFTL); |
| } |
| return result; |
| } |
| |
| private Template getTemplate(TemplateLoader loader, String name, Locale locale, String encoding, boolean parse) |
| throws IOException |
| { |
| boolean debug = logger.isDebugEnabled(); |
| String debugName = debug |
| ? buildDebugName(name, locale, encoding, parse) |
| : null; |
| TemplateKey tk = new TemplateKey(name, locale, encoding, parse); |
| |
| CachedTemplate cachedTemplate; |
| if(isStorageConcurrent) { |
| cachedTemplate = (CachedTemplate)storage.get(tk); |
| } |
| else { |
| synchronized(storage) { |
| cachedTemplate = (CachedTemplate)storage.get(tk); |
| } |
| } |
| long now = System.currentTimeMillis(); |
| long lastModified = -1L; |
| Object newlyFoundSource = null; |
| boolean rethrown = false; |
| try { |
| if (cachedTemplate != null) { |
| // If we're within the refresh delay, return the cached copy |
| if (now - cachedTemplate.lastChecked < delay) { |
| if(debug) { |
| logger.debug(debugName + " cached copy not yet stale; using cached."); |
| } |
| // Can be null, indicating a cached negative lookup |
| Object t = cachedTemplate.templateOrException; |
| if(t instanceof Template || t == null) { |
| return (Template)t; |
| } |
| else if(t instanceof RuntimeException) { |
| throwLoadFailedException((RuntimeException)t); |
| } |
| else if(t instanceof IOException) { |
| rethrown = true; |
| throwLoadFailedException((IOException)t); |
| } |
| throw new RuntimeException("t is " + t.getClass().getName()); |
| } |
| // Clone as the instance bound to the map should be treated as |
| // immutable to ensure proper concurrent semantics |
| cachedTemplate = cachedTemplate.cloneCachedTemplate(); |
| // Update the last-checked flag |
| cachedTemplate.lastChecked = now; |
| |
| // Find the template source |
| newlyFoundSource = findTemplateSource(name, locale); |
| |
| // Template source was removed |
| if (newlyFoundSource == null) { |
| if(debug) { |
| logger.debug(debugName + " no source found."); |
| } |
| storeNegativeLookup(tk, cachedTemplate, null); |
| return null; |
| } |
| |
| // If the source didn't change and its last modified date |
| // also didn't change, return the cached version. |
| lastModified = loader.getLastModified(newlyFoundSource); |
| boolean lastModifiedNotChanged = lastModified == cachedTemplate.lastModified; |
| boolean sourceEquals = newlyFoundSource.equals(cachedTemplate.source); |
| if(lastModifiedNotChanged && sourceEquals) { |
| if(debug) { |
| logger.debug(debugName + " using cached since " + |
| newlyFoundSource + " didn't change."); |
| } |
| storeCached(tk, cachedTemplate); |
| return (Template)cachedTemplate.templateOrException; |
| } |
| else { |
| if(debug && !sourceEquals) { |
| logger.debug("Updating source, info for cause: " + |
| "sourceEquals=" + sourceEquals + |
| ", newlyFoundSource=" + StringUtil.jQuoteNoXSS(newlyFoundSource) + |
| ", cachedTemplate.source=" + StringUtil.jQuoteNoXSS(cachedTemplate.source)); |
| } |
| if(debug && !lastModifiedNotChanged) { |
| logger.debug("Updating source, info for cause: " + |
| "lastModifiedNotChanged=" + lastModifiedNotChanged + |
| ", cache lastModified=" + cachedTemplate.lastModified + |
| " != file lastModified=" + lastModified); |
| } |
| // Update the source |
| cachedTemplate.source = newlyFoundSource; |
| } |
| } |
| else { |
| if(debug) { |
| logger.debug("Could not find template in cache, " + |
| "creating new one; id=[" + |
| StringUtil.jQuoteNoXSS(tk.name) + "[" + |
| StringUtil.jQuoteNoXSS(tk.locale) + "," + |
| tk.encoding + (tk.parse ? ",parsed] " : ",unparsed] ") + |
| "]"); |
| } |
| |
| // Construct a new CachedTemplate entry. Note we set the |
| // cachedTemplate.lastModified to Long.MIN_VALUE. This is |
| // a flag that signs it has to be explicitly queried later on. |
| cachedTemplate = new CachedTemplate(); |
| cachedTemplate.lastChecked = now; |
| newlyFoundSource = findTemplateSource(name, locale); |
| if (newlyFoundSource == null) { |
| storeNegativeLookup(tk, cachedTemplate, null); |
| return null; |
| } |
| cachedTemplate.source = newlyFoundSource; |
| cachedTemplate.lastModified = lastModified = Long.MIN_VALUE; |
| } |
| if(debug) { |
| logger.debug("Compiling FreeMarker template " + |
| debugName + " from " + StringUtil.jQuoteNoXSS(newlyFoundSource)); |
| } |
| // If we get here, then we need to (re)load the template |
| Object source = cachedTemplate.source; |
| Template t = loadTemplate(loader, name, locale, encoding, parse, source); |
| cachedTemplate.templateOrException = t; |
| cachedTemplate.lastModified = |
| lastModified == Long.MIN_VALUE |
| ? loader.getLastModified(source) |
| : lastModified; |
| storeCached(tk, cachedTemplate); |
| return t; |
| } |
| catch(RuntimeException e) { |
| if (cachedTemplate != null) { |
| storeNegativeLookup(tk, cachedTemplate, e); |
| } |
| throw e; |
| } |
| catch(IOException e) { |
| if(!rethrown) { |
| storeNegativeLookup(tk, cachedTemplate, e); |
| } |
| throw e; |
| } |
| finally { |
| if(newlyFoundSource != null) { |
| loader.closeTemplateSource(newlyFoundSource); |
| } |
| } |
| } |
| |
| 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; |
| } |
| } |
| |
| private void throwLoadFailedException(Exception e) throws IOException { |
| IOException ioe; |
| if(INIT_CAUSE != null) { |
| ioe = new IOException("There was an error loading the " + |
| "template on an earlier attempt; it's attached as a cause"); |
| try { |
| INIT_CAUSE.invoke(ioe, new Object[] { e }); |
| } catch(RuntimeException ex) { |
| throw ex; |
| } catch(Exception ex) { |
| throw new UndeclaredThrowableException(ex); |
| } |
| } |
| else { |
| ioe = new IOException("There was an error loading the " + |
| "template on an earlier attempt: " + e.getClass().getName() + |
| ": " + e.getMessage()); |
| } |
| throw ioe; |
| } |
| |
| private void storeNegativeLookup(TemplateKey tk, |
| CachedTemplate cachedTemplate, Exception e) { |
| cachedTemplate.templateOrException = e; |
| cachedTemplate.source = null; |
| cachedTemplate.lastModified = 0L; |
| storeCached(tk, cachedTemplate); |
| } |
| |
| private void storeCached(TemplateKey tk, CachedTemplate cachedTemplate) { |
| if(isStorageConcurrent) { |
| storage.put(tk, cachedTemplate); |
| } |
| else { |
| synchronized(storage) { |
| storage.put(tk, cachedTemplate); |
| } |
| } |
| } |
| |
| private Template loadTemplate(TemplateLoader loader, String name, Locale locale, String encoding, |
| boolean parse, Object source) |
| throws IOException |
| { |
| Template template; |
| Reader reader = loader.getReader(source, encoding); |
| try |
| { |
| if(parse) |
| { |
| try { |
| template = new Template(name, reader, config, encoding); |
| } |
| catch (Template.WrongEncodingException wee) { |
| encoding = wee.specifiedEncoding; |
| reader = loader.getReader(source, encoding); |
| template = new Template(name, reader, config, encoding); |
| } |
| template.setLocale(locale); |
| } |
| else |
| { |
| // Read the contents into a StringWriter, then construct a single-textblock |
| // template from it. |
| StringWriter sw = new StringWriter(); |
| char[] buf = new char[4096]; |
| for(;;) |
| { |
| int charsRead = reader.read(buf); |
| if (charsRead > 0) |
| { |
| sw.write(buf, 0, charsRead); |
| } |
| else if(charsRead == -1) |
| { |
| break; |
| } |
| } |
| template = Template.getPlainTextTemplate(name, sw.toString(), config); |
| template.setLocale(locale); |
| } |
| template.setEncoding(encoding); |
| } |
| finally |
| { |
| reader.close(); |
| } |
| return template; |
| } |
| |
| /** |
| * Gets the delay in milliseconds between checking for newer versions of a |
| * template source. |
| * @return the current value of the delay |
| */ |
| public synchronized long getDelay() |
| { |
| return delay; |
| } |
| |
| /** |
| * Sets the delay in milliseconds between checking for newer versions of a |
| * template sources. |
| * @param delay the new value of the delay |
| */ |
| public synchronized void setDelay(long delay) |
| { |
| this.delay = delay; |
| } |
| |
| /** |
| * Returns if localized template lookup is enabled or not. |
| */ |
| public synchronized boolean getLocalizedLookup() |
| { |
| return localizedLookup; |
| } |
| |
| /** |
| * Setis if localized template lookup is enabled or not. |
| */ |
| public synchronized void setLocalizedLookup(boolean localizedLookup) |
| { |
| this.localizedLookup = localizedLookup; |
| } |
| |
| /** |
| * Removes all entries from the cache, forcing reloading of templates |
| * on subsequent {@link #getTemplate(String, Locale, String, boolean)} |
| * calls. If the configured template loader is |
| * {@link StatefulTemplateLoader stateful}, then its |
| * {@link StatefulTemplateLoader#resetState()} method is invoked as well. |
| */ |
| public void clear() |
| { |
| synchronized (storage) { |
| storage.clear(); |
| if(templateLoader instanceof StatefulTemplateLoader) { |
| ((StatefulTemplateLoader)templateLoader).resetState(); |
| } |
| } |
| } |
| |
| /** |
| * Removes an entry from the cache, hence forcing the re-loading of it when |
| * it's next time requested. It doesn't delete the template file itself. |
| * This is to give the application finer control over cache updating than |
| * {@link #setDelay(long)} alone does. |
| * |
| * For the meaning of the parameters, see |
| * {@link #getTemplate(String, Locale, String, boolean)}. |
| */ |
| public void removeTemplate( |
| String name, Locale locale, 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 = normalizeName(name); |
| if(name != null && templateLoader != null) { |
| boolean debug = logger.isDebugEnabled(); |
| String debugName = debug |
| ? buildDebugName(name, locale, encoding, parse) |
| : null; |
| TemplateKey tk = new TemplateKey(name, locale, encoding, parse); |
| |
| if(isStorageConcurrent) { |
| storage.remove(tk); |
| } else { |
| synchronized(storage) { |
| storage.remove(tk); |
| } |
| } |
| logger.debug(debugName + " was removed from the cache, if it was there"); |
| } |
| } |
| |
| private String buildDebugName(String name, Locale locale, String encoding, |
| boolean parse) { |
| return StringUtil.jQuoteNoXSS(name) + "[" |
| + StringUtil.jQuoteNoXSS(locale) + "," + encoding |
| + (parse ? ",parsed] " : ",unparsed]"); |
| } |
| |
| /** |
| * Resolves a path-like reference to a template (like the one used in {@code #include} or {@code #import}), assuming |
| * a current directory. This gives a full, even if non-normalized template name, that could be used for |
| * {@link #getTemplate(String, Locale, String, boolean)}. This is mostly used when a template refers to another |
| * template. |
| * |
| * @param targetTemplatePath If starts with "/" or contains "://", it's an absolute path and {@code currentDir} |
| * will be ignored, otherwise it's interpreted as relative to {@code currentDir} |
| * @param currentTemplateDir must end with "/", might contains "://". |
| */ |
| public static String getFullTemplatePath(Environment env, String currentTemplateDir, String targetTemplatePath) |
| { |
| if (!env.isClassicCompatible()) { |
| if (targetTemplatePath.indexOf("://") >0) { |
| ; |
| } |
| else if (targetTemplatePath.length() > 0 && targetTemplatePath.charAt(0) == '/') { |
| int protIndex = currentTemplateDir.indexOf("://"); |
| if (protIndex >0) { |
| targetTemplatePath = currentTemplateDir.substring(0, protIndex + 2) + targetTemplatePath; |
| } else { |
| targetTemplatePath = targetTemplatePath.substring(1); |
| } |
| } |
| else { |
| targetTemplatePath = currentTemplateDir + targetTemplatePath; |
| } |
| } |
| return targetTemplatePath; |
| } |
| |
| private Object findTemplateSource(String name, Locale locale) |
| throws |
| IOException |
| { |
| if (localizedLookup) { |
| int lastDot = name.lastIndexOf('.'); |
| String prefix = lastDot == -1 ? name : name.substring(0, lastDot); |
| String suffix = lastDot == -1 ? "" : name.substring(lastDot); |
| String localeName = LOCALE_SEPARATOR + locale.toString(); |
| StringBuffer buf = new StringBuffer(name.length() + localeName.length()); |
| buf.append(prefix); |
| for (;;) |
| { |
| buf.setLength(prefix.length()); |
| String path = buf.append(localeName).append(suffix).toString(); |
| Object templateSource = acquireTemplateSource(path); |
| if (templateSource != null) |
| { |
| return templateSource; |
| } |
| int lastUnderscore = localeName.lastIndexOf('_'); |
| if (lastUnderscore == -1) |
| break; |
| localeName = localeName.substring(0, lastUnderscore); |
| } |
| return null; |
| } |
| else |
| { |
| return acquireTemplateSource(name); |
| } |
| } |
| |
| private Object acquireTemplateSource(String path) throws IOException |
| { |
| int asterisk = path.indexOf(ASTERISK); |
| // Shortcut in case there is no acquisition |
| if(asterisk == -1) |
| { |
| return templateLoader.findTemplateSource(path); |
| } |
| StringTokenizer tok = new StringTokenizer(path, "/"); |
| int lastAsterisk = -1; |
| List tokpath = new ArrayList(); |
| while(tok.hasMoreTokens()) |
| { |
| String pathToken = tok.nextToken(); |
| if(pathToken.equals(ASTERISKSTR)) |
| { |
| if(lastAsterisk != -1) |
| { |
| tokpath.remove(lastAsterisk); |
| } |
| lastAsterisk = tokpath.size(); |
| } |
| tokpath.add(pathToken); |
| } |
| if (lastAsterisk == -1) { // if there was no real "*" step after all |
| return templateLoader.findTemplateSource(path); |
| } |
| String basePath = concatPath(tokpath, 0, lastAsterisk); |
| String resourcePath = concatPath(tokpath, lastAsterisk + 1, tokpath.size()); |
| if(resourcePath.endsWith("/")) |
| { |
| resourcePath = resourcePath.substring(0, resourcePath.length() - 1); |
| } |
| StringBuffer buf = new StringBuffer(path.length()).append(basePath); |
| int l = basePath.length(); |
| boolean debug = logger.isDebugEnabled(); |
| for(;;) |
| { |
| String fullPath = buf.append(resourcePath).toString(); |
| if(debug) |
| { |
| logger.debug("Trying to find template source " |
| + StringUtil.jQuoteNoXSS(fullPath)); |
| } |
| Object templateSource = templateLoader.findTemplateSource(fullPath); |
| if(templateSource != null) |
| { |
| return templateSource; |
| } |
| if(l == 0) |
| { |
| return null; |
| } |
| l = basePath.lastIndexOf(SLASH, l - 2) + 1; |
| buf.setLength(l); |
| } |
| } |
| |
| private String concatPath(List path, int from, int to) |
| { |
| StringBuffer buf = new StringBuffer((to - from) * 16); |
| for(int i = from; i < to; ++i) |
| { |
| buf.append(path.get(i)).append('/'); |
| } |
| return buf.toString(); |
| } |
| |
| private static String normalizeName(String name) { |
| // Disallow 0 for security reasons. |
| if (name.indexOf(0) != -1) return null; |
| |
| for(;;) { |
| int parentDirPathLoc = name.indexOf(PARENT_DIR_PATH); |
| if(parentDirPathLoc == 0) { |
| // If it starts with /../, then it reaches outside the template |
| // root. |
| return null; |
| } |
| if(parentDirPathLoc == -1) { |
| if(name.startsWith(PARENT_DIR_PATH_PREFIX)) { |
| // Another attempt to reach out of template root. |
| return null; |
| } |
| break; |
| } |
| int previousSlashLoc = name.lastIndexOf(SLASH, parentDirPathLoc - 1); |
| name = name.substring(0, previousSlashLoc + 1) + |
| name.substring(parentDirPathLoc + PARENT_DIR_PATH.length()); |
| } |
| for(;;) { |
| int currentDirPathLoc = name.indexOf(CURRENT_DIR_PATH); |
| if(currentDirPathLoc == -1) { |
| if(name.startsWith(CURRENT_DIR_PATH_PREFIX)) { |
| name = name.substring(CURRENT_DIR_PATH_PREFIX.length()); |
| } |
| break; |
| } |
| name = name.substring(0, currentDirPathLoc) + |
| name.substring(currentDirPathLoc + CURRENT_DIR_PATH.length() - 1); |
| } |
| // Editing can leave us with a leading slash; strip it. |
| if(name.length() > 1 && name.charAt(0) == SLASH) { |
| name = name.substring(1); |
| } |
| return name; |
| } |
| |
| /** |
| * This class holds a (name, locale) pair and is used as the key in |
| * the cached templates map. |
| */ |
| private static final class TemplateKey |
| { |
| private final String name; |
| private final Locale locale; |
| private final String encoding; |
| private final boolean parse; |
| |
| TemplateKey(String name, Locale locale, String encoding, boolean parse) |
| { |
| this.name = name; |
| this.locale = locale; |
| this.encoding = encoding; |
| this.parse = parse; |
| } |
| |
| public boolean equals(Object o) |
| { |
| if (o instanceof TemplateKey) |
| { |
| TemplateKey tk = (TemplateKey)o; |
| return |
| parse == tk.parse && |
| name.equals(tk.name) && |
| locale.equals(tk.locale) && |
| encoding.equals(tk.encoding); |
| } |
| return false; |
| } |
| |
| public int hashCode() |
| { |
| return |
| name.hashCode() ^ |
| locale.hashCode() ^ |
| encoding.hashCode() ^ |
| (parse ? Boolean.FALSE : Boolean.TRUE).hashCode(); |
| } |
| } |
| |
| /** |
| * This class holds the cached template and associated information |
| * (the source object, and the last-checked and last-modified timestamps). |
| * It is used as the value in the cached templates map. Note: this class |
| * is Serializable to allow custom 3rd party CacheStorage implementations |
| * to serialize/replicate them (see tracker issue #1926150); FreeMarker |
| * code itself doesn't rely on its serializability. |
| */ |
| private static final class CachedTemplate implements Cloneable, Serializable |
| { |
| private static final long serialVersionUID = 1L; |
| |
| Object templateOrException; |
| Object source; |
| long lastChecked; |
| long lastModified; |
| |
| public CachedTemplate cloneCachedTemplate() { |
| try { |
| return (CachedTemplate)super.clone(); |
| } |
| catch(CloneNotSupportedException e) { |
| throw new UndeclaredThrowableException(e); |
| } |
| } |
| } |
| } |