http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java new file mode 100644 index 0000000..ea1cc63 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/DefaultTemplateResolver.java @@ -0,0 +1,904 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core.templateresolver.impl; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.Template; +import org.apache.freemarker.core.TemplateConfiguration; +import org.apache.freemarker.core.TemplateLanguage; +import org.apache.freemarker.core.TemplateNotFoundException; +import org.apache.freemarker.core.WrongTemplateCharsetException; +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core.templateresolver.CacheStorage; +import org.apache.freemarker.core.templateresolver.GetTemplateResult; +import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; +import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory; +import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactoryException; +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.templateresolver.TemplateLoaderSession; +import org.apache.freemarker.core.templateresolver.TemplateLoadingResult; +import org.apache.freemarker.core.templateresolver.TemplateLoadingResultStatus; +import org.apache.freemarker.core.templateresolver.TemplateLoadingSource; +import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy; +import org.apache.freemarker.core.templateresolver.TemplateNameFormat; +import org.apache.freemarker.core.templateresolver.TemplateResolver; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util.UndeclaredThrowableException; +import org.apache.freemarker.core.util._NullArgumentException; +import org.apache.freemarker.core.util._StringUtil; +import org.slf4j.Logger; + +/** + * Performs caching and on-demand loading of the templates. + * The actual template "file" loading is delegated to a {@link TemplateLoader} that you can specify in the constructor. + * Some aspects of caching is delegated to a {@link CacheStorage} that you can also specify in the constructor. + * + * <p>Typically you don't instantiate or otherwise use this class directly. By default the {@link Configuration} embeds + * an instance of this class, that you access indirectly through {@link Configuration#getTemplate(String)} and other + * {@link Configuration} API-s. When you set the {@link Configuration#getTemplateLoader() templateLoader} or + * {@link Configuration#getCacheStorage() cacheStorage} of the {@link Configuration}, you indirectly configure the + * {@link TemplateResolver}. + */ +public class DefaultTemplateResolver extends TemplateResolver { + + /** + * The default template update delay; see {@link Configuration#getTemplateUpdateDelayMilliseconds()}. + * + * @since 2.3.23 + */ + public static final long DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS = 5000L; + + private static final String ASTERISKSTR = "*"; + private static final char ASTERISK = '*'; + private static final char SLASH = '/'; + private static final String LOCALE_PART_SEPARATOR = "_"; + private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER; + + /** Maybe {@code null}. */ + private final TemplateLoader templateLoader; + + /** Here we keep our cached templates */ + private final CacheStorage cacheStorage; + private final TemplateLookupStrategy templateLookupStrategy; + private final TemplateNameFormat templateNameFormat; + private final TemplateConfigurationFactory templateConfigurations; + private final long templateUpdateDelayMilliseconds; + private final boolean localizedLookup; + + private Configuration config; + + /** + * @param templateLoader + * The {@link TemplateLoader} to use. Can be {@code null}, though then every request will result in + * {@link TemplateNotFoundException}. + * @param cacheStorage + * The {@link CacheStorage} to use. Can't be {@code null}. + * @param templateLookupStrategy + * The {@link TemplateLookupStrategy} to use. Can't be {@code null}. + * @param templateUpdateDelayMilliseconds + * See {@link Configuration#getTemplateUpdateDelayMilliseconds()} + * @param templateNameFormat + * The {@link TemplateNameFormat} to use. Can't be {@code null}. + * @param templateConfigurations + * The {@link TemplateConfigurationFactory} to use. Can be {@code null} (then all templates will use the + * settings coming from the {@link Configuration} as is, except in the very rare case where a + * {@link TemplateLoader} itself specifies a {@link TemplateConfiguration}). + * @param config + * The {@link Configuration} this cache will be used for. Can't be {@code null}. + * + * @since 2.3.24 + */ + public DefaultTemplateResolver( + TemplateLoader templateLoader, + CacheStorage cacheStorage, long templateUpdateDelayMilliseconds, + TemplateLookupStrategy templateLookupStrategy, boolean localizedLookup, + TemplateNameFormat templateNameFormat, + TemplateConfigurationFactory templateConfigurations, + Configuration config) { + super(config); + + this.templateLoader = templateLoader; + + _NullArgumentException.check("cacheStorage", cacheStorage); + this.cacheStorage = cacheStorage; + + this.templateUpdateDelayMilliseconds = templateUpdateDelayMilliseconds; + + this.localizedLookup = localizedLookup; + + _NullArgumentException.check("templateLookupStrategy", templateLookupStrategy); + this.templateLookupStrategy = templateLookupStrategy; + + _NullArgumentException.check("templateNameFormat", templateNameFormat); + this.templateNameFormat = templateNameFormat; + + // Can be null + this.templateConfigurations = templateConfigurations; + + _NullArgumentException.check("config", config); + this.config = config; + } + + /** + * Returns the configuration for internal usage. + */ + @Override + public Configuration getConfiguration() { + return config; + } + + public TemplateLoader getTemplateLoader() { + return templateLoader; + } + + public CacheStorage getCacheStorage() { + return cacheStorage; + } + + /** + * @since 2.3.22 + */ + public TemplateLookupStrategy getTemplateLookupStrategy() { + return templateLookupStrategy; + } + + /** + * @since 2.3.22 + */ + public TemplateNameFormat getTemplateNameFormat() { + return templateNameFormat; + } + + /** + * @since 2.3.24 + */ + public TemplateConfigurationFactory getTemplateConfigurations() { + return templateConfigurations; + } + + /** + * Retrieves the template with the given name (and according the specified further parameters) from the template + * cache, loading it into the cache first if it's missing/staled. + * + * <p> + * All parameters must be non-{@code null}, except {@code customLookupCondition}. For the meaning of the parameters + * see {@link Configuration#getTemplate(String, Locale, Serializable, boolean)}. + * + * @return A {@link GetTemplateResult} object that contains the {@link Template}, or a + * {@link GetTemplateResult} object that contains {@code null} as the {@link Template} and information + * about the missing template. The return value itself is never {@code null}. Note that exceptions occurring + * during template loading will not be classified as a missing template, so they will cause an exception to + * be thrown by this method instead of returning a {@link GetTemplateResult}. The idea is that having a + * missing template is normal (not exceptional), providing that the backing storage mechanism could indeed + * check that it's missing. + * + * @throws MalformedTemplateNameException + * If the {@code name} was malformed according the current {@link TemplateNameFormat}. However, if the + * {@link TemplateNameFormat} is {@link DefaultTemplateNameFormatFM2#INSTANCE} and + * {@link Configuration#getIncompatibleImprovements()} is less than 2.4.0, then instead of throwing this + * exception, a {@link GetTemplateResult} will be returned, similarly as if the template were missing + * (the {@link GetTemplateResult#getMissingTemplateReason()} will describe the real error). + * + * @throws IOException + * If reading the template has failed from a reason other than the template is missing. This method + * should never be a {@link TemplateNotFoundException}, as that condition is indicated in the return + * value. + * + * @since 2.3.22 + */ + @Override + public GetTemplateResult getTemplate(String name, Locale locale, Serializable customLookupCondition) + throws IOException { + _NullArgumentException.check("name", name); + _NullArgumentException.check("locale", locale); + + name = templateNameFormat.normalizeRootBasedName(name); + + if (templateLoader == null) { + return new GetTemplateResult(name, "The TemplateLoader (and TemplateLoader2) was null."); + } + + Template template = getTemplateInternal(name, locale, customLookupCondition); + return template != null ? new GetTemplateResult(template) : new GetTemplateResult(name, (String) null); + } + + @Override + public String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException { + return templateNameFormat.toRootBasedName(baseName, targetName); + } + + @Override + public String normalizeRootBasedName(String name) throws MalformedTemplateNameException { + return templateNameFormat.normalizeRootBasedName(name); + } + + private Template getTemplateInternal( + final String name, final Locale locale, final Serializable customLookupCondition) + throws IOException { + final boolean debug = LOG.isDebugEnabled(); + final String debugPrefix = debug + ? getDebugPrefix("getTemplate", name, locale, customLookupCondition) + : null; + final CachedResultKey cacheKey = new CachedResultKey(name, locale, customLookupCondition); + + CachedResult oldCachedResult = (CachedResult) cacheStorage.get(cacheKey); + + final long now = System.currentTimeMillis(); + + boolean rethrownCachedException = false; + boolean suppressFinallyException = false; + TemplateLoaderBasedTemplateLookupResult newLookupResult = null; + CachedResult newCachedResult = null; + TemplateLoaderSession session = null; + try { + if (oldCachedResult != null) { + // If we're within the refresh delay, return the cached result + if (now - oldCachedResult.lastChecked < templateUpdateDelayMilliseconds) { + if (debug) { + LOG.debug(debugPrefix + "Cached copy not yet stale; using cached."); + } + Object t = oldCachedResult.templateOrException; + // t can be null, indicating a cached negative lookup + if (t instanceof Template || t == null) { + return (Template) t; + } else if (t instanceof RuntimeException) { + rethrowCachedException((RuntimeException) t); + } else if (t instanceof IOException) { + rethrownCachedException = true; + rethrowCachedException((IOException) t); + } + throw new BugException("Unhandled class for t: " + t.getClass().getName()); + } + // The freshness of the cache result must be checked. + + // Clone, as the instance in the cache store must not be modified to ensure proper concurrent behavior. + newCachedResult = oldCachedResult.clone(); + newCachedResult.lastChecked = now; + + session = templateLoader.createSession(); + if (debug && session != null) { + LOG.debug(debugPrefix + "Session created."); + } + + // Find the template source, load it if it doesn't correspond to the cached result. + newLookupResult = lookupAndLoadTemplateIfChanged( + name, locale, customLookupCondition, oldCachedResult.source, oldCachedResult.version, session); + + // Template source was removed (TemplateLoader2ResultStatus.NOT_FOUND, or no TemplateLoader2Result) + if (!newLookupResult.isPositive()) { + if (debug) { + LOG.debug(debugPrefix + "No source found."); + } + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null); + return null; + } + + final TemplateLoadingResult newTemplateLoaderResult = newLookupResult.getTemplateLoaderResult(); + if (newTemplateLoaderResult.getStatus() == TemplateLoadingResultStatus.NOT_MODIFIED) { + // Return the cached version. + if (debug) { + LOG.debug(debugPrefix + ": Using cached template " + + "(source: " + newTemplateLoaderResult.getSource() + ")" + + " as it hasn't been changed on the backing store."); + } + cacheStorage.put(cacheKey, newCachedResult); + return (Template) newCachedResult.templateOrException; + } else { + if (newTemplateLoaderResult.getStatus() != TemplateLoadingResultStatus.OPENED) { + // TemplateLoader2ResultStatus.NOT_FOUND was already handler earlier + throw new BugException("Unxpected status: " + newTemplateLoaderResult.getStatus()); + } + if (debug) { + StringBuilder debugMsg = new StringBuilder(); + debugMsg.append(debugPrefix) + .append("Reloading template instead of using the cached result because "); + if (newCachedResult.templateOrException instanceof Throwable) { + debugMsg.append("it's a cached error (retrying)."); + } else { + Object newSource = newTemplateLoaderResult.getSource(); + if (!nullSafeEquals(newSource, oldCachedResult.source)) { + debugMsg.append("the source has been changed: ") + .append("cached.source=").append(_StringUtil.jQuoteNoXSS(oldCachedResult.source)) + .append(", current.source=").append(_StringUtil.jQuoteNoXSS(newSource)); + } else { + Serializable newVersion = newTemplateLoaderResult.getVersion(); + if (!nullSafeEquals(oldCachedResult.version, newVersion)) { + debugMsg.append("the version has been changed: ") + .append("cached.version=").append(oldCachedResult.version) + .append(", current.version=").append(newVersion); + } else { + debugMsg.append("??? (unknown reason)"); + } + } + } + LOG.debug(debugMsg.toString()); + } + } + } else { // if there was no cached result + if (debug) { + LOG.debug(debugPrefix + "No cached result was found; will try to load template."); + } + + newCachedResult = new CachedResult(); + newCachedResult.lastChecked = now; + + session = templateLoader.createSession(); + if (debug && session != null) { + LOG.debug(debugPrefix + "Session created."); + } + + newLookupResult = lookupAndLoadTemplateIfChanged( + name, locale, customLookupCondition, null, null, session); + + if (!newLookupResult.isPositive()) { + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null); + return null; + } + } + // We have newCachedResult and newLookupResult initialized at this point. + + TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult(); + newCachedResult.source = templateLoaderResult.getSource(); + + // If we get here, then we need to (re)load the template + if (debug) { + LOG.debug(debugPrefix + "Reading template content (source: " + + _StringUtil.jQuoteNoXSS(newCachedResult.source) + ")"); + } + + Template template = loadTemplate( + templateLoaderResult, + name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition); + if (session != null) { + session.close(); + if (debug) { + LOG.debug(debugPrefix + "Session closed."); + } + } + newCachedResult.templateOrException = template; + newCachedResult.version = templateLoaderResult.getVersion(); + cacheStorage.put(cacheKey, newCachedResult); + return template; + } catch (RuntimeException e) { + if (newCachedResult != null) { + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e); + } + suppressFinallyException = true; + throw e; + } catch (IOException e) { + // Rethrown cached exceptions are wrapped into IOException-s, so we only need this condition here. + if (!rethrownCachedException) { + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e); + } + suppressFinallyException = true; + throw e; + } finally { + try { + // Close streams first: + + if (newLookupResult != null && newLookupResult.isPositive()) { + TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult(); + Reader reader = templateLoaderResult.getReader(); + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { // [FM3] Exception e + if (suppressFinallyException) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close template content Reader for: " + name, e); + } + } else { + suppressFinallyException = true; + throw e; + } + } + } else if (templateLoaderResult.getInputStream() != null) { + try { + templateLoaderResult.getInputStream().close(); + } catch (IOException e) { // [FM3] Exception e + if (suppressFinallyException) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close template content InputStream for: " + name, e); + } + } else { + suppressFinallyException = true; + throw e; + } + } + } + } + } finally { + // Then close streams: + + if (session != null && !session.isClosed()) { + try { + session.close(); + if (debug) { + LOG.debug(debugPrefix + "Session closed."); + } + } catch (IOException e) { // [FM3] Exception e + if (suppressFinallyException) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close template loader session for" + name, e); + } + } else { + suppressFinallyException = true; + throw e; + } + } + } + } + } + } + + + + private static final Method INIT_CAUSE = getInitCauseMethod(); + + private static Method getInitCauseMethod() { + try { + return Throwable.class.getMethod("initCause", Throwable.class); + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Creates an {@link IOException} that has a cause exception. + */ + // [Java 6] Remove + private IOException newIOException(String message, Throwable cause) { + if (cause == null) { + return new IOException(message); + } + + IOException ioe; + if (INIT_CAUSE != null) { + ioe = new IOException(message); + try { + INIT_CAUSE.invoke(ioe, cause); + } catch (RuntimeException ex) { + throw ex; + } catch (Exception ex) { + throw new UndeclaredThrowableException(ex); + } + } else { + ioe = new IOException(message + "\nCaused by: " + cause.getClass().getName() + + ": " + cause.getMessage()); + } + return ioe; + } + + private void rethrowCachedException(Throwable e) throws IOException { + throw newIOException("There was an error loading the " + + "template on an earlier attempt; see cause exception.", e); + } + + private void setToNegativeAndPutIntoCache(CachedResultKey cacheKey, CachedResult cachedResult, Exception e) { + cachedResult.templateOrException = e; + cachedResult.source = null; + cachedResult.version = null; + cacheStorage.put(cacheKey, cachedResult); + } + + private Template loadTemplate( + TemplateLoadingResult templateLoaderResult, + final String name, final String sourceName, Locale locale, final Serializable customLookupCondition) + throws IOException { + TemplateConfiguration tc; + { + TemplateConfiguration cfgTC; + try { + cfgTC = templateConfigurations != null + ? templateConfigurations.get(sourceName, templateLoaderResult.getSource()) : null; + } catch (TemplateConfigurationFactoryException e) { + throw newIOException("Error while getting TemplateConfiguration; see cause exception.", e); + } + TemplateConfiguration templateLoaderResultTC = templateLoaderResult.getTemplateConfiguration(); + if (templateLoaderResultTC != null) { + TemplateConfiguration.Builder mergedTCBuilder = new TemplateConfiguration.Builder(); + if (cfgTC != null) { + mergedTCBuilder.merge(cfgTC); + } + mergedTCBuilder.merge(templateLoaderResultTC); + + tc = mergedTCBuilder.build(); + } else { + tc = cfgTC; + } + } + + if (tc != null && tc.isLocaleSet()) { + locale = tc.getLocale(); + } + Charset initialEncoding = tc != null && tc.isSourceEncodingSet() ? tc.getSourceEncoding() + : config.getSourceEncoding(); + TemplateLanguage templateLanguage = tc != null && tc.isTemplateLanguageSet() ? tc.getTemplateLanguage() + : config.getTemplateLanguage(); + + Template template; + { + Reader reader = templateLoaderResult.getReader(); + InputStream inputStream = templateLoaderResult.getInputStream(); + InputStream markedInputStream; + if (reader != null) { + if (inputStream != null) { + throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName() + + ", both getReader() and getInputStream() has returned non-null."); + } + initialEncoding = null; // No charset decoding has happened + markedInputStream = null; + } else if (inputStream != null) { + if (templateLanguage.getCanSpecifyCharsetInContent()) { + // We need mark support, to restart if the charset suggested by <#ftl encoding=...> differs + // from that we use initially. + if (!inputStream.markSupported()) { + inputStream = new BufferedInputStream(inputStream); + } + inputStream.mark(Integer.MAX_VALUE); // Mark is released after the 1st FTL tag + markedInputStream = inputStream; + } else { + markedInputStream = null; + } + // Regarding buffering worries: On the Reader side we should only read in chunks (like through a + // BufferedReader), so there shouldn't be a problem if the InputStream is not buffered. (Also, at least + // on Oracle JDK and OpenJDK 7 the InputStreamReader itself has an internal ~8K buffer.) + reader = new InputStreamReader(inputStream, initialEncoding); + } else { + throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName() + + ", both getReader() and getInputStream() has returned null."); + } + + try { + try { + template = templateLanguage.parse(name, sourceName, reader, config, tc, + initialEncoding, markedInputStream); + } catch (WrongTemplateCharsetException charsetException) { + final Charset templateSpecifiedEncoding = charsetException.getTemplateSpecifiedEncoding(); + + if (inputStream != null) { + // We restart InputStream to re-decode it with the new charset. + inputStream.reset(); + + // Don't close `reader`; it's an InputStreamReader that would close the wrapped InputStream. + reader = new InputStreamReader(inputStream, templateSpecifiedEncoding); + } else { + throw new IllegalStateException( + "TemplateLanguage " + _StringUtil.jQuote(templateLanguage.getName()) + " has thrown " + + WrongTemplateCharsetException.class.getName() + + ", but its canSpecifyCharsetInContent property is false."); + } + + template = templateLanguage.parse(name, sourceName, reader, config, tc, + templateSpecifiedEncoding, markedInputStream); + } + } finally { + reader.close(); + } + } + + template.setLookupLocale(locale); + template.setCustomLookupCondition(customLookupCondition); + return template; + } + + /** + * Gets the delay in milliseconds between checking for newer versions of a + * template source. + * @return the current value of the delay + */ + public long getTemplateUpdateDelayMilliseconds() { + // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not. + synchronized (this) { + return templateUpdateDelayMilliseconds; + } + } + + /** + * Returns if localized template lookup is enabled or not. + */ + public boolean getLocalizedLookup() { + // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not. + synchronized (this) { + return localizedLookup; + } + } + + /** + * Removes all entries from the cache, forcing reloading of templates on subsequent + * {@link #getTemplate(String, Locale, Serializable)} calls. + * + * @param resetTemplateLoader + * Whether to call {@link TemplateLoader#resetState()}. on the template loader. + */ + public void clearTemplateCache(boolean resetTemplateLoader) { + synchronized (cacheStorage) { + cacheStorage.clear(); + if (templateLoader != null && resetTemplateLoader) { + templateLoader.resetState(); + } + } + } + + /** + * Same as {@link #clearTemplateCache(boolean)} with {@code true} {@code resetTemplateLoader} argument. + */ + @Override + public void clearTemplateCache() { + synchronized (cacheStorage) { + cacheStorage.clear(); + if (templateLoader != null) { + templateLoader.resetState(); + } + } + } + + /** + * Removes an entry from the cache, hence forcing the re-loading of it when it's next time requested. (It doesn't + * delete the template file itself.) This is to give the application finer control over cache updating than the + * update delay ({@link #getTemplateUpdateDelayMilliseconds()}) alone does. + * + * For the meaning of the parameters, see + * {@link Configuration#getTemplate(String, Locale, Serializable, boolean)} + */ + @Override + public void removeTemplateFromCache( + String name, Locale locale, Serializable customLookupCondition) + throws IOException { + if (name == null) { + throw new IllegalArgumentException("Argument \"name\" can't be null"); + } + if (locale == null) { + throw new IllegalArgumentException("Argument \"locale\" can't be null"); + } + name = templateNameFormat.normalizeRootBasedName(name); + if (name != null && templateLoader != null) { + boolean debug = LOG.isDebugEnabled(); + String debugPrefix = debug + ? getDebugPrefix("removeTemplate", name, locale, customLookupCondition) + : null; + CachedResultKey tk = new CachedResultKey(name, locale, customLookupCondition); + + cacheStorage.remove(tk); + if (debug) { + LOG.debug(debugPrefix + "Template was removed from the cache, if it was there"); + } + } + } + + private String getDebugPrefix(String operation, String name, Locale locale, Object customLookupCondition) { + return operation + " " + _StringUtil.jQuoteNoXSS(name) + "(" + + _StringUtil.jQuoteNoXSS(locale) + + (customLookupCondition != null ? ", cond=" + _StringUtil.jQuoteNoXSS(customLookupCondition) : "") + + "): "; + } + + /** + * Looks up according the {@link TemplateLookupStrategy} and then starts reading the template, if it was changed + * compared to the cached result, or if there was no cached result yet. + */ + private TemplateLoaderBasedTemplateLookupResult lookupAndLoadTemplateIfChanged( + String name, Locale locale, Object customLookupCondition, + TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion, + TemplateLoaderSession session) throws IOException { + final TemplateLoaderBasedTemplateLookupResult lookupResult = templateLookupStrategy.lookup( + new DefaultTemplateResolverTemplateLookupContext( + name, locale, customLookupCondition, + cachedResultSource, cachedResultVersion, + session)); + if (lookupResult == null) { + throw new NullPointerException("Lookup result shouldn't be null"); + } + return lookupResult; + } + + private String concatPath(List<String> pathSteps, int from, int to) { + StringBuilder buf = new StringBuilder((to - from) * 16); + for (int i = from; i < to; ++i) { + buf.append(pathSteps.get(i)); + if (i < pathSteps.size() - 1) { + buf.append('/'); + } + } + return buf.toString(); + } + + // Replace with Objects.equals in Java 7 + private static boolean nullSafeEquals(Object o1, Object o2) { + if (o1 == o2) return true; + if (o1 == null || o2 == null) return false; + return o1.equals(o2); + } + + /** + * Used as cache key to look up a {@link CachedResult}. + */ + @SuppressWarnings("serial") + private static final class CachedResultKey implements Serializable { + private final String name; + private final Locale locale; + private final Serializable customLookupCondition; + + CachedResultKey(String name, Locale locale, Serializable customLookupCondition) { + this.name = name; + this.locale = locale; + this.customLookupCondition = customLookupCondition; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CachedResultKey)) { + return false; + } + CachedResultKey tk = (CachedResultKey) o; + return + name.equals(tk.name) && + locale.equals(tk.locale) && + nullSafeEquals(customLookupCondition, tk.customLookupCondition); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + locale.hashCode(); + result = 31 * result + (customLookupCondition != null ? customLookupCondition.hashCode() : 0); + return result; + } + + } + + /** + * Hold the a cached {@link #getTemplate(String, Locale, Serializable)} result and the associated + * information needed to check if the cached value is up to date. + * + * <p> + * Note: this class is Serializable to allow custom 3rd party CacheStorage implementations to serialize/replicate + * them; FreeMarker code itself doesn't rely on its serializability. + * + * @see CachedResultKey + */ + private static final class CachedResult implements Cloneable, Serializable { + private static final long serialVersionUID = 1L; + + Object templateOrException; + TemplateLoadingSource source; + Serializable version; + long lastChecked; + + @Override + public CachedResult clone() { + try { + return (CachedResult) super.clone(); + } catch (CloneNotSupportedException e) { + throw new UndeclaredThrowableException(e); + } + } + } + + private class DefaultTemplateResolverTemplateLookupContext extends TemplateLoaderBasedTemplateLookupContext { + + private final TemplateLoaderSession session; + + DefaultTemplateResolverTemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition, + TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion, + TemplateLoaderSession session) { + super(templateName, localizedLookup ? templateLocale : null, customLookupCondition, + cachedResultSource, cachedResultVersion); + this.session = session; + } + + @Override + public TemplateLoaderBasedTemplateLookupResult lookupWithAcquisitionStrategy(String path) throws IOException { + // Only one of the possible ways of making a name non-normalized, but is the easiest mistake to do: + if (path.startsWith("/")) { + throw new IllegalArgumentException("Non-normalized name, starts with \"/\": " + path); + } + + int asterisk = path.indexOf(ASTERISK); + // Shortcut in case there is no acquisition + if (asterisk == -1) { + return createLookupResult( + path, + templateLoader.load(path, getCachedResultSource(), getCachedResultVersion(), session)); + } + StringTokenizer pathTokenizer = new StringTokenizer(path, "/"); + int lastAsterisk = -1; + List<String> pathSteps = new ArrayList<>(); + while (pathTokenizer.hasMoreTokens()) { + String pathStep = pathTokenizer.nextToken(); + if (pathStep.equals(ASTERISKSTR)) { + if (lastAsterisk != -1) { + pathSteps.remove(lastAsterisk); + } + lastAsterisk = pathSteps.size(); + } + pathSteps.add(pathStep); + } + if (lastAsterisk == -1) { // if there was no real "*" step after all + return createLookupResult( + path, + templateLoader.load(path, getCachedResultSource(), getCachedResultVersion(), session)); + } + String basePath = concatPath(pathSteps, 0, lastAsterisk); + String postAsteriskPath = concatPath(pathSteps, lastAsterisk + 1, pathSteps.size()); + StringBuilder buf = new StringBuilder(path.length()).append(basePath); + int basePathLen = basePath.length(); + while (true) { + String fullPath = buf.append(postAsteriskPath).toString(); + TemplateLoadingResult templateLoaderResult = templateLoader.load( + fullPath, getCachedResultSource(), getCachedResultVersion(), session); + if (templateLoaderResult.getStatus() == TemplateLoadingResultStatus.OPENED) { + return createLookupResult(fullPath, templateLoaderResult); + } + if (basePathLen == 0) { + return createNegativeLookupResult(); + } + basePathLen = basePath.lastIndexOf(SLASH, basePathLen - 2) + 1; + buf.setLength(basePathLen); + } + } + + @Override + public TemplateLoaderBasedTemplateLookupResult lookupWithLocalizedThenAcquisitionStrategy(final String templateName, + final Locale templateLocale) throws IOException { + + if (templateLocale == null) { + return lookupWithAcquisitionStrategy(templateName); + } + + int lastDot = templateName.lastIndexOf('.'); + String prefix = lastDot == -1 ? templateName : templateName.substring(0, lastDot); + String suffix = lastDot == -1 ? "" : templateName.substring(lastDot); + String localeName = LOCALE_PART_SEPARATOR + templateLocale.toString(); + StringBuilder buf = new StringBuilder(templateName.length() + localeName.length()); + buf.append(prefix); + tryLocaleNameVariations: while (true) { + buf.setLength(prefix.length()); + String path = buf.append(localeName).append(suffix).toString(); + TemplateLoaderBasedTemplateLookupResult lookupResult = lookupWithAcquisitionStrategy(path); + if (lookupResult.isPositive()) { + return lookupResult; + } + + int lastUnderscore = localeName.lastIndexOf('_'); + if (lastUnderscore == -1) { + break tryLocaleNameVariations; + } + localeName = localeName.substring(0, lastUnderscore); + } + return createNegativeLookupResult(); + } + + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java new file mode 100644 index 0000000..e2437c1 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/FileTemplateLoader.java @@ -0,0 +1,383 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core.templateresolver.impl; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Objects; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.templateresolver.TemplateLoaderSession; +import org.apache.freemarker.core.templateresolver.TemplateLoadingResult; +import org.apache.freemarker.core.templateresolver.TemplateLoadingSource; +import org.apache.freemarker.core.util._SecurityUtil; +import org.apache.freemarker.core.util._StringUtil; +import org.slf4j.Logger; + +/** + * A {@link TemplateLoader} that uses files inside a specified directory as the source of templates. By default it does + * security checks on the <em>canonical</em> path that will prevent it serving templates outside that specified + * directory. If you want symbolic links that point outside the template directory to work, you need to disable this + * feature by using {@link #FileTemplateLoader(File, boolean)} with {@code true} second argument, but before that, + * check the security implications there! + */ +public class FileTemplateLoader implements TemplateLoader { + + /** + * By setting this Java system property to {@code true}, you can change the default of + * {@code #getEmulateCaseSensitiveFileSystem()}. + */ + public static String SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM + = "org.freemarker.emulateCaseSensitiveFileSystem"; + private static final boolean EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT; + static { + final String s = _SecurityUtil.getSystemProperty(SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM, + "false"); + boolean emuCaseSensFS; + try { + emuCaseSensFS = _StringUtil.getYesNo(s); + } catch (Exception e) { + emuCaseSensFS = false; + } + EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT = emuCaseSensFS; + } + + private static final int CASE_CHECH_CACHE_HARD_SIZE = 50; + private static final int CASE_CHECK_CACHE__SOFT_SIZE = 1000; + private static final boolean SEP_IS_SLASH = File.separatorChar == '/'; + + private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER; + + public final File baseDir; + private final String canonicalBasePath; + private boolean emulateCaseSensitiveFileSystem; + private MruCacheStorage correctCasePaths; + + /** + * Creates a new file template loader that will use the specified directory + * as the base directory for loading templates. It will not allow access to + * template files that are accessible through symlinks that point outside + * the base directory. + * @param baseDir the base directory for loading templates + */ + public FileTemplateLoader(final File baseDir) + throws IOException { + this(baseDir, false); + } + + /** + * Creates a new file template loader that will use the specified directory as the base directory for loading + * templates. See the parameters for allowing symlinks that point outside the base directory. + * + * @param baseDir + * the base directory for loading templates + * + * @param disableCanonicalPathCheck + * If {@code true}, it will not check if the file to be loaded is inside the {@code baseDir} or not, + * according the <em>canonical</em> paths of the {@code baseDir} and the file to load. Note that + * {@link Configuration#getTemplate(String)} and (its overloads) already prevents backing out from the + * template directory with paths like {@code /../../../etc/password}, however, that can be circumvented + * with symbolic links or other file system features. If you really want to use symbolic links that point + * outside the {@code baseDir}, set this parameter to {@code true}, but then be very careful with + * template paths that are supplied by the visitor or an external system. + */ + public FileTemplateLoader(final File baseDir, final boolean disableCanonicalPathCheck) + throws IOException { + try { + Object[] retval = AccessController.doPrivileged(new PrivilegedExceptionAction<Object[]>() { + @Override + public Object[] run() throws IOException { + if (!baseDir.exists()) { + throw new FileNotFoundException(baseDir + " does not exist."); + } + if (!baseDir.isDirectory()) { + throw new IOException(baseDir + " is not a directory."); + } + Object[] retval = new Object[2]; + if (disableCanonicalPathCheck) { + retval[0] = baseDir; + retval[1] = null; + } else { + retval[0] = baseDir.getCanonicalFile(); + String basePath = ((File) retval[0]).getPath(); + // Most canonical paths don't end with File.separator, + // but some does. Like, "C:\" VS "C:\templates". + if (!basePath.endsWith(File.separator)) { + basePath += File.separatorChar; + } + retval[1] = basePath; + } + return retval; + } + }); + this.baseDir = (File) retval[0]; + canonicalBasePath = (String) retval[1]; + + setEmulateCaseSensitiveFileSystem(getEmulateCaseSensitiveFileSystemDefault()); + } catch (PrivilegedActionException e) { + throw (IOException) e.getException(); + } + } + + private File getFile(final String name) throws IOException { + try { + return AccessController.doPrivileged(new PrivilegedExceptionAction<File>() { + @Override + public File run() throws IOException { + File source = new File(baseDir, SEP_IS_SLASH ? name : + name.replace('/', File.separatorChar)); + if (!source.isFile()) { + return null; + } + // Security check for inadvertently returning something + // outside the template directory when linking is not + // allowed. + if (canonicalBasePath != null) { + String normalized = source.getCanonicalPath(); + if (!normalized.startsWith(canonicalBasePath)) { + throw new SecurityException(source.getAbsolutePath() + + " resolves to " + normalized + " which " + + " doesn't start with " + canonicalBasePath); + } + } + + if (emulateCaseSensitiveFileSystem && !isNameCaseCorrect(source)) { + return null; + } + + return source; + } + }); + } catch (PrivilegedActionException e) { + throw (IOException) e.getException(); + } + } + + private long getLastModified(final File templateSource) { + return (AccessController.<Long>doPrivileged(new PrivilegedAction<Long>() { + @Override + public Long run() { + return Long.valueOf((templateSource).lastModified()); + } + })).longValue(); + } + + private InputStream getInputStream(final File templateSource) + throws IOException { + try { + return AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>() { + @Override + public InputStream run() throws IOException { + return new FileInputStream(templateSource); + } + }); + } catch (PrivilegedActionException e) { + throw (IOException) e.getException(); + } + } + + /** + * Called by {@link #getFile(String)} when {@link #getEmulateCaseSensitiveFileSystem()} is {@code true}. + */ + private boolean isNameCaseCorrect(File source) throws IOException { + final String sourcePath = source.getPath(); + synchronized (correctCasePaths) { + if (correctCasePaths.get(sourcePath) != null) { + return true; + } + } + + final File parentDir = source.getParentFile(); + if (parentDir != null) { + if (!baseDir.equals(parentDir) && !isNameCaseCorrect(parentDir)) { + return false; + } + + final String[] listing = parentDir.list(); + if (listing != null) { + final String fileName = source.getName(); + + boolean identicalNameFound = false; + for (int i = 0; !identicalNameFound && i < listing.length; i++) { + if (fileName.equals(listing[i])) { + identicalNameFound = true; + } + } + + if (!identicalNameFound) { + // If we find a similarly named file that only differs in case, then this is a file-not-found. + for (final String listingEntry : listing) { + if (fileName.equalsIgnoreCase(listingEntry)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Emulating file-not-found because of letter case differences to the " + + "real file, for: {}", sourcePath); + } + return false; + } + } + } + } + } + + synchronized (correctCasePaths) { + correctCasePaths.put(sourcePath, Boolean.TRUE); + } + return true; + } + + /** + * Returns the base directory in which the templates are searched. This comes from the constructor argument, but + * it's possibly a canonicalized version of that. + * + * @since 2.3.21 + */ + public File getBaseDirectory() { + return baseDir; + } + + /** + * Intended for development only, checks if the template name matches the case (upper VS lower case letters) of the + * actual file name, and if it doesn't, it emulates a file-not-found even if the file system is case insensitive. + * This is useful when developing application on Windows, which will be later installed on Linux, OS X, etc. This + * check can be resource intensive, as to check the file name the directories involved, up to the + * {@link #getBaseDirectory()} directory, must be listed. Positive results (matching case) will be cached without + * expiration time. + * + * <p>The default in {@link FileTemplateLoader} is {@code false}, but subclasses may change they by overriding + * {@link #getEmulateCaseSensitiveFileSystemDefault()}. + * + * @since 2.3.23 + */ + public void setEmulateCaseSensitiveFileSystem(boolean emulateCaseSensitiveFileSystem) { + // Ensure that the cache exists exactly when needed: + if (emulateCaseSensitiveFileSystem) { + if (correctCasePaths == null) { + correctCasePaths = new MruCacheStorage(CASE_CHECH_CACHE_HARD_SIZE, CASE_CHECK_CACHE__SOFT_SIZE); + } + } else { + correctCasePaths = null; + } + + this.emulateCaseSensitiveFileSystem = emulateCaseSensitiveFileSystem; + } + + /** + * Getter pair of {@link #setEmulateCaseSensitiveFileSystem(boolean)}. + * + * @since 2.3.23 + */ + public boolean getEmulateCaseSensitiveFileSystem() { + return emulateCaseSensitiveFileSystem; + } + + /** + * Returns the default of {@link #getEmulateCaseSensitiveFileSystem()}. In {@link FileTemplateLoader} it's + * {@code false}, unless the {@link #SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM} system property was + * set to {@code true}, but this can be overridden here in custom subclasses. For example, if your environment + * defines something like developer mode, you may want to override this to return {@code true} on Windows. + * + * @since 2.3.23 + */ + protected boolean getEmulateCaseSensitiveFileSystemDefault() { + return EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT; + } + + /** + * Show class name and some details that are useful in template-not-found errors. + * + * @since 2.3.21 + */ + @Override + public String toString() { + // We don't _StringUtil.jQuote paths here, because on Windows there will be \\-s then that some may find + // confusing. + return _TemplateLoaderUtils.getClassNameForToString(this) + "(" + + "baseDir=\"" + baseDir + "\"" + + (canonicalBasePath != null ? ", canonicalBasePath=\"" + canonicalBasePath + "\"" : "") + + (emulateCaseSensitiveFileSystem ? ", emulateCaseSensitiveFileSystem=true" : "") + + ")"; + } + + @Override + public TemplateLoaderSession createSession() { + return null; + } + + @Override + public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom, + Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException { + File file = getFile(name); + if (file == null) { + return TemplateLoadingResult.NOT_FOUND; + } + + FileTemplateLoadingSource source = new FileTemplateLoadingSource(file); + + long lmd = getLastModified(file); + Long version = lmd != -1 ? lmd : null; + + if (ifSourceDiffersFrom != null && ifSourceDiffersFrom.equals(source) + && Objects.equals(ifVersionDiffersFrom, version)) { + return TemplateLoadingResult.NOT_MODIFIED; + } + + return new TemplateLoadingResult(source, version, getInputStream(file), null); + } + + @Override + public void resetState() { + // Does nothing + } + + @SuppressWarnings("serial") + private static class FileTemplateLoadingSource implements TemplateLoadingSource { + + private final File file; + + FileTemplateLoadingSource(File file) { + this.file = file; + } + + @Override + public int hashCode() { + return file.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + return file.equals(((FileTemplateLoadingSource) obj).file); + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java new file mode 100644 index 0000000..9f004fe --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MruCacheStorage.java @@ -0,0 +1,330 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core.templateresolver.impl; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.Map; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize; + +/** + * A cache storage that implements a two-level Most Recently Used cache. In the + * first level, items are strongly referenced up to the specified maximum. When + * the maximum is exceeded, the least recently used item is moved into the + * second level cache, where they are softly referenced, up to another + * specified maximum. When the second level maximum is also exceeded, the least + * recently used item is discarded altogether. This cache storage is a + * generalization of both {@link StrongCacheStorage} and + * {@link SoftCacheStorage} - the effect of both of them can be achieved by + * setting one maximum to zero and the other to the largest positive integer. + * On the other hand, if you wish to use this storage in a strong-only mode, or + * in a soft-only mode, you might consider using {@link StrongCacheStorage} or + * {@link SoftCacheStorage} instead, as they can be used by + * {@link DefaultTemplateResolver} concurrently without any synchronization on a 5.0 or + * later JRE. + * + * <p>This class is <em>NOT</em> thread-safe. If it's accessed from multiple + * threads concurrently, proper synchronization must be provided by the callers. + * Note that {@link DefaultTemplateResolver}, the natural user of this class provides the + * necessary synchronizations when it uses the class. + * Also you might consider whether you need this sort of a mixed storage at all + * in your solution, as in most cases SoftCacheStorage can also be sufficient. + * SoftCacheStorage will use Java soft references, and they already use access + * timestamps internally to bias the garbage collector against clearing + * recently used references, so you can get reasonably good (and + * memory-sensitive) most-recently-used caching through + * {@link SoftCacheStorage} as well. + * + * @see Configuration#getCacheStorage() + */ +public class MruCacheStorage implements CacheStorageWithGetSize { + private final MruEntry strongHead = new MruEntry(); + private final MruEntry softHead = new MruEntry(); + { + softHead.linkAfter(strongHead); + } + private final Map map = new HashMap(); + private final ReferenceQueue refQueue = new ReferenceQueue(); + private final int strongSizeLimit; + private final int softSizeLimit; + private int strongSize = 0; + private int softSize = 0; + + /** + * Creates a new MRU cache storage with specified maximum cache sizes. Each + * cache size can vary between 0 and {@link Integer#MAX_VALUE}. + * @param strongSizeLimit the maximum number of strongly referenced templates; when exceeded, the entry used + * the least recently will be moved into the soft cache. + * @param softSizeLimit the maximum number of softly referenced templates; when exceeded, the entry used + * the least recently will be discarded. + */ + public MruCacheStorage(int strongSizeLimit, int softSizeLimit) { + if (strongSizeLimit < 0) throw new IllegalArgumentException("strongSizeLimit < 0"); + if (softSizeLimit < 0) throw new IllegalArgumentException("softSizeLimit < 0"); + this.strongSizeLimit = strongSizeLimit; + this.softSizeLimit = softSizeLimit; + } + + @Override + public Object get(Object key) { + removeClearedReferences(); + MruEntry entry = (MruEntry) map.get(key); + if (entry == null) { + return null; + } + relinkEntryAfterStrongHead(entry, null); + Object value = entry.getValue(); + if (value instanceof MruReference) { + // This can only happen with strongSizeLimit == 0 + return ((MruReference) value).get(); + } + return value; + } + + @Override + public void put(Object key, Object value) { + removeClearedReferences(); + MruEntry entry = (MruEntry) map.get(key); + if (entry == null) { + entry = new MruEntry(key, value); + map.put(key, entry); + linkAfterStrongHead(entry); + } else { + relinkEntryAfterStrongHead(entry, value); + } + + } + + @Override + public void remove(Object key) { + removeClearedReferences(); + removeInternal(key); + } + + private void removeInternal(Object key) { + MruEntry entry = (MruEntry) map.remove(key); + if (entry != null) { + unlinkEntryAndInspectIfSoft(entry); + } + } + + @Override + public void clear() { + strongHead.makeHead(); + softHead.linkAfter(strongHead); + map.clear(); + strongSize = softSize = 0; + // Quick refQueue processing + while (refQueue.poll() != null); + } + + private void relinkEntryAfterStrongHead(MruEntry entry, Object newValue) { + if (unlinkEntryAndInspectIfSoft(entry) && newValue == null) { + // Turn soft reference into strong reference, unless is was cleared + MruReference mref = (MruReference) entry.getValue(); + Object strongValue = mref.get(); + if (strongValue != null) { + entry.setValue(strongValue); + linkAfterStrongHead(entry); + } else { + map.remove(mref.getKey()); + } + } else { + if (newValue != null) { + entry.setValue(newValue); + } + linkAfterStrongHead(entry); + } + } + + private void linkAfterStrongHead(MruEntry entry) { + entry.linkAfter(strongHead); + if (strongSize == strongSizeLimit) { + // softHead.previous is LRU strong entry + MruEntry lruStrong = softHead.getPrevious(); + // Attila: This is equaivalent to strongSizeLimit != 0 + // DD: But entry.linkAfter(strongHead) was just executed above, so + // lruStrong != strongHead is true even if strongSizeLimit == 0. + if (lruStrong != strongHead) { + lruStrong.unlink(); + if (softSizeLimit > 0) { + lruStrong.linkAfter(softHead); + lruStrong.setValue(new MruReference(lruStrong, refQueue)); + if (softSize == softSizeLimit) { + // List is circular, so strongHead.previous is LRU soft entry + MruEntry lruSoft = strongHead.getPrevious(); + lruSoft.unlink(); + map.remove(lruSoft.getKey()); + } else { + ++softSize; + } + } else { + map.remove(lruStrong.getKey()); + } + } + } else { + ++strongSize; + } + } + + private boolean unlinkEntryAndInspectIfSoft(MruEntry entry) { + entry.unlink(); + if (entry.getValue() instanceof MruReference) { + --softSize; + return true; + } else { + --strongSize; + return false; + } + } + + private void removeClearedReferences() { + for (; ; ) { + MruReference ref = (MruReference) refQueue.poll(); + if (ref == null) { + break; + } + removeInternal(ref.getKey()); + } + } + + /** + * Returns the configured upper limit of the number of strong cache entries. + * + * @since 2.3.21 + */ + public int getStrongSizeLimit() { + return strongSizeLimit; + } + + /** + * Returns the configured upper limit of the number of soft cache entries. + * + * @since 2.3.21 + */ + public int getSoftSizeLimit() { + return softSizeLimit; + } + + /** + * Returns the <em>current</em> number of strong cache entries. + * + * @see #getStrongSizeLimit() + * @since 2.3.21 + */ + public int getStrongSize() { + return strongSize; + } + + /** + * Returns a close approximation of the <em>current</em> number of soft cache entries. + * + * @see #getSoftSizeLimit() + * @since 2.3.21 + */ + public int getSoftSize() { + removeClearedReferences(); + return softSize; + } + + /** + * Returns a close approximation of the current number of cache entries. + * + * @see #getStrongSize() + * @see #getSoftSize() + * @since 2.3.21 + */ + @Override + public int getSize() { + return getSoftSize() + getStrongSize(); + } + + private static final class MruEntry { + private MruEntry prev; + private MruEntry next; + private final Object key; + private Object value; + + /** + * Used solely to construct the head element + */ + MruEntry() { + makeHead(); + key = value = null; + } + + MruEntry(Object key, Object value) { + this.key = key; + this.value = value; + } + + Object getKey() { + return key; + } + + Object getValue() { + return value; + } + + void setValue(Object value) { + this.value = value; + } + + MruEntry getPrevious() { + return prev; + } + + void linkAfter(MruEntry entry) { + next = entry.next; + entry.next = this; + prev = entry; + next.prev = this; + } + + void unlink() { + next.prev = prev; + prev.next = next; + prev = null; + next = null; + } + + void makeHead() { + prev = next = this; + } + } + + private static class MruReference extends SoftReference { + private final Object key; + + MruReference(MruEntry entry, ReferenceQueue queue) { + super(entry.getValue(), queue); + key = entry.getKey(); + } + + Object getKey() { + return key; + } + } + + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java new file mode 100644 index 0000000..883ec62 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/MultiTemplateLoader.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core.templateresolver.impl; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.templateresolver.TemplateLoaderSession; +import org.apache.freemarker.core.templateresolver.TemplateLoadingResult; +import org.apache.freemarker.core.templateresolver.TemplateLoadingResultStatus; +import org.apache.freemarker.core.templateresolver.TemplateLoadingSource; +import org.apache.freemarker.core.util._NullArgumentException; + +/** + * A {@link TemplateLoader} that uses a set of other loaders to load the templates. On every request, loaders are + * queried in the order of their appearance in the array of loaders provided to the constructor. Except, when the + * {@linkplain #setSticky(boolean)} sticky} setting is set to {@code true} (default is false {@code false}), if + * a request for some template name was already satisfied in the past by one of the loaders, that loader is queried + * first (stickiness). + * + * <p>This class is thread-safe. + */ +// TODO JUnit test +public class MultiTemplateLoader implements TemplateLoader { + + private final TemplateLoader[] templateLoaders; + private final Map<String, TemplateLoader> lastTemplateLoaderForName = new ConcurrentHashMap<>(); + + private boolean sticky = false; + + /** + * Creates a new instance that will use the specified template loaders. + * + * @param templateLoaders + * the template loaders that are used to load templates, in the order as they will be searched + * (except where {@linkplain #setSticky(boolean) stickiness} says otherwise). + */ + public MultiTemplateLoader(TemplateLoader... templateLoaders) { + _NullArgumentException.check("templateLoaders", templateLoaders); + this.templateLoaders = templateLoaders.clone(); + } + + /** + * Clears the sickiness memory, also resets the state of all enclosed {@link TemplateLoader}-s. + */ + @Override + public void resetState() { + lastTemplateLoaderForName.clear(); + for (TemplateLoader templateLoader : templateLoaders) { + templateLoader.resetState(); + } + } + + /** + * Show class name and some details that are useful in template-not-found errors. + * + * @since 2.3.21 + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MultiTemplateLoader("); + for (int i = 0; i < templateLoaders.length; i++) { + if (i != 0) { + sb.append(", "); + } + sb.append("loader").append(i + 1).append(" = ").append(templateLoaders[i]); + } + sb.append(")"); + return sb.toString(); + } + + /** + * Returns the number of {@link TemplateLoader}-s directly inside this {@link TemplateLoader}. + * + * @since 2.3.23 + */ + public int getTemplateLoaderCount() { + return templateLoaders.length; + } + + /** + * Returns the {@link TemplateLoader} at the given index. + * + * @param index + * Must be below {@link #getTemplateLoaderCount()}. + */ + public TemplateLoader getTemplateLoader(int index) { + return templateLoaders[index]; + } + + /** + * Getter pair of {@link #setSticky(boolean)}. + */ + public boolean isSticky() { + return sticky; + } + + /** + * Sets if for a name that was already loaded earlier the same {@link TemplateLoader} will be tried first, or + * we always try the {@link TemplateLoader}-s strictly in the order as it was specified in the constructor. + * The default is {@code false}. + */ + public void setSticky(boolean sticky) { + this.sticky = sticky; + } + + @Override + public TemplateLoaderSession createSession() { + return null; + } + + @Override + public TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom, + Serializable ifVersionDiffersFrom, TemplateLoaderSession session) throws IOException { + TemplateLoader lastLoader = null; + if (sticky) { + // Use soft affinity - give the loader that last found this + // resource a chance to find it again first. + lastLoader = lastTemplateLoaderForName.get(name); + if (lastLoader != null) { + TemplateLoadingResult result = lastLoader.load(name, ifSourceDiffersFrom, ifVersionDiffersFrom, session); + if (result.getStatus() != TemplateLoadingResultStatus.NOT_FOUND) { + return result; + } + } + } + + // If there is no affine loader, or it could not find the resource + // again, try all loaders in order of appearance. If any manages + // to find the resource, then associate it as the new affine loader + // for this resource. + for (TemplateLoader templateLoader : templateLoaders) { + if (lastLoader != templateLoader) { + TemplateLoadingResult result = templateLoader.load( + name, ifSourceDiffersFrom, ifVersionDiffersFrom, session); + if (result.getStatus() != TemplateLoadingResultStatus.NOT_FOUND) { + if (sticky) { + lastTemplateLoaderForName.put(name, templateLoader); + } + return result; + } + } + } + + if (sticky) { + lastTemplateLoaderForName.remove(name); + } + return TemplateLoadingResult.NOT_FOUND; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java new file mode 100644 index 0000000..c8ff55c --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/NullCacheStorage.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core.templateresolver.impl; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.templateresolver.CacheStorage; +import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize; + +/** + * A cache storage that doesn't store anything. Use this if you + * don't want caching. + * + * @see Configuration#getCacheStorage() + * + * @since 2.3.17 + */ +public class NullCacheStorage implements CacheStorage, CacheStorageWithGetSize { + + /** + * @since 2.3.22 + */ + public static final NullCacheStorage INSTANCE = new NullCacheStorage(); + + @Override + public Object get(Object key) { + return null; + } + + @Override + public void put(Object key, Object value) { + // do nothing + } + + @Override + public void remove(Object key) { + // do nothing + } + + @Override + public void clear() { + // do nothing + } + + /** + * Always returns 0. + * + * @since 2.3.21 + */ + @Override + public int getSize() { + return 0; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java new file mode 100644 index 0000000..3e22c33 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/templateresolver/impl/SoftCacheStorage.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core.templateresolver.impl; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.freemarker.core.Configuration.ExtendableBuilder; +import org.apache.freemarker.core.templateresolver.CacheStorage; +import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize; + +/** + * Soft cache storage is a cache storage that uses {@link SoftReference} objects to hold the objects it was passed, + * therefore allows the garbage collector to purge the cache when it determines that it wants to free up memory. This + * class is thread-safe to the extent that its underlying map is. The parameterless constructor uses a thread-safe map + * since 2.3.24 or Java 5. + * + * @see ExtendableBuilder#setCacheStorage(CacheStorage) + */ +public class SoftCacheStorage implements CacheStorage, CacheStorageWithGetSize { + + private final ReferenceQueue queue = new ReferenceQueue(); + private final ConcurrentMap map; + + /** + * Creates an instance that uses a {@link ConcurrentMap} internally. + */ + public SoftCacheStorage() { + map = new ConcurrentHashMap(); + } + + @Override + public Object get(Object key) { + processQueue(); + Reference ref = (Reference) map.get(key); + return ref == null ? null : ref.get(); + } + + @Override + public void put(Object key, Object value) { + processQueue(); + map.put(key, new SoftValueReference(key, value, queue)); + } + + @Override + public void remove(Object key) { + processQueue(); + map.remove(key); + } + + @Override + public void clear() { + map.clear(); + processQueue(); + } + + /** + * Returns a close approximation of the number of cache entries. + * + * @since 2.3.21 + */ + @Override + public int getSize() { + processQueue(); + return map.size(); + } + + private void processQueue() { + for (; ; ) { + SoftValueReference ref = (SoftValueReference) queue.poll(); + if (ref == null) { + return; + } + Object key = ref.getKey(); + map.remove(key, ref); + } + } + + private static final class SoftValueReference extends SoftReference { + private final Object key; + + SoftValueReference(Object key, Object value, ReferenceQueue queue) { + super(value, queue); + this.key = key; + } + + Object getKey() { + return key; + } + } + +} \ No newline at end of file
