http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateCache.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateCache.java b/src/main/java/freemarker/cache/TemplateCache.java index fa8df50..d545986 100644 --- a/src/main/java/freemarker/cache/TemplateCache.java +++ b/src/main/java/freemarker/cache/TemplateCache.java @@ -19,12 +19,13 @@ package freemarker.cache; +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.io.StringWriter; import java.lang.reflect.Method; -import java.net.URLConnection; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -33,10 +34,11 @@ import java.util.StringTokenizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import freemarker.cache.MultiTemplateLoader.MultiSource; import freemarker.core.BugException; import freemarker.core.Environment; +import freemarker.core.MarkReleaserTemplateSpecifiedEncodingHandler; import freemarker.core.TemplateConfiguration; +import freemarker.core.TemplateSpecifiedEncodingHandler; import freemarker.template.Configuration; import freemarker.template.MalformedTemplateNameException; import freemarker.template.Template; @@ -76,12 +78,12 @@ public class TemplateCache { private final TemplateLoader templateLoader; /** Here we keep our cached templates */ - private final CacheStorage storage; + private final CacheStorage cacheStorage; private final TemplateLookupStrategy templateLookupStrategy; private final TemplateNameFormat templateNameFormat; private final TemplateConfigurationFactory templateConfigurations; - private final boolean isStorageConcurrent; + private final boolean isCacheStorageConcurrent; /** {@link Configuration#setTemplateUpdateDelayMilliseconds(long)} */ private long updateDelay = DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS; /** {@link Configuration#setLocalizedLookup(boolean)} */ @@ -90,35 +92,6 @@ public class TemplateCache { 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. - */ - @Deprecated - public TemplateCache() { - this(_TemplateAPI.createDefaultTemplateLoader(Configuration.VERSION_2_3_0)); - } - - /** - * @deprecated Use {@link #TemplateCache(TemplateLoader, CacheStorage, Configuration)} instead. - */ - @Deprecated - public TemplateCache(TemplateLoader templateLoader) { - this(templateLoader, (Configuration) null); - } - - /** - * @deprecated Use {@link #TemplateCache(TemplateLoader, CacheStorage, Configuration)} instead. - */ - @Deprecated - public TemplateCache(TemplateLoader templateLoader, CacheStorage cacheStorage) { - this(templateLoader, cacheStorage, null); - } - - /** * Same as {@link #TemplateCache(TemplateLoader, CacheStorage, Configuration)} with a new {@link SoftCacheStorage} * as the 2nd parameter. * @@ -157,7 +130,8 @@ public class TemplateCache { /** * @param templateLoader - * The {@link TemplateLoader} to use. Can't be {@code null}. + * The {@link TemplateLoader} to use. Can be {@code null}, though than you won't be able to load + * anything. * @param cacheStorage * The {@link CacheStorage} to use. Can't be {@code null}. * @param templateLookupStrategy @@ -166,10 +140,10 @@ public class TemplateCache { * 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). + * 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 be {@code null} for backward compatibility, - * as it can be set with {@link #setConfiguration(Configuration)} later. + * The {@link Configuration} this cache will be used for. Can't be {@code null}. * * @since 2.3.24 */ @@ -180,8 +154,8 @@ public class TemplateCache { this.templateLoader = templateLoader; NullArgumentException.check("cacheStorage", cacheStorage); - this.storage = cacheStorage; - isStorageConcurrent = cacheStorage instanceof ConcurrentCacheStorage && + this.cacheStorage = cacheStorage; + isCacheStorageConcurrent = cacheStorage instanceof ConcurrentCacheStorage && ((ConcurrentCacheStorage) cacheStorage).isConcurrent(); NullArgumentException.check("templateLookupStrategy", templateLookupStrategy); @@ -193,28 +167,24 @@ public class TemplateCache { // Can be null this.templateConfigurations = templateConfigurations; + NullArgumentException.check("config", config); this.config = config; } /** - * 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. - * - * @deprecated Use the {@link #TemplateCache(TemplateLoader, CacheStorage, Configuration)} constructor. + * Returns the configuration for internal usage. */ - @Deprecated - public void setConfiguration(Configuration config) { - this.config = config; - clear(); + // [FM3] After setConfiguration was removed, this can be too. + Configuration getConfiguration() { + return config; } public TemplateLoader getTemplateLoader() { return templateLoader; } - + public CacheStorage getCacheStorage() { - return storage; + return cacheStorage; } /** @@ -287,12 +257,12 @@ public class TemplateCache { } if (templateLoader == null) { - return new MaybeMissingTemplate(name, "The TemplateLoader was null."); + return new MaybeMissingTemplate(name, "The TemplateLoader (and TemplateLoader2) was null."); } Template template = getTemplateInternal(name, locale, customLookupCondition, encoding, parseAsFTL); return template != null ? new MaybeMissingTemplate(template) : new MaybeMissingTemplate(name, (String) null); - } + } /** * Similar to {@link #getTemplate(String, Locale, Object, String, boolean)} with {@code null} @@ -310,158 +280,236 @@ public class TemplateCache { return getTemplate(name, locale, null, encoding, parseAsFTL).getTemplate(); } - /** - * Returns the deprecated default template loader of FreeMarker 2.3.0. - * - * @deprecated The {@link TemplateLoader} should be always specified by the constructor caller. - */ - @Deprecated - protected static TemplateLoader createLegacyDefaultTemplateLoader() { - return _TemplateAPI.createDefaultTemplateLoader(Configuration.VERSION_2_3_0); - } - private Template getTemplateInternal( final String name, final Locale locale, final Object customLookupCondition, final String encoding, final boolean parseAsFTL) throws IOException { final boolean debug = LOG.isDebugEnabled(); - final String debugName = debug - ? buildDebugName(name, locale, customLookupCondition, encoding, parseAsFTL) + final String debugPrefix = debug + ? getDebugPrefix("getTemplate", name, locale, customLookupCondition, encoding, parseAsFTL) : null; - final TemplateKey tk = new TemplateKey(name, locale, customLookupCondition, encoding, parseAsFTL); + final CachedResultKey cacheKey = new CachedResultKey(name, locale, customLookupCondition, encoding, parseAsFTL); - CachedTemplate cachedTemplate; - if (isStorageConcurrent) { - cachedTemplate = (CachedTemplate) storage.get(tk); + CachedResult oldCachedResult; + if (isCacheStorageConcurrent) { + oldCachedResult = (CachedResult) cacheStorage.get(cacheKey); } else { - synchronized (storage) { - cachedTemplate = (CachedTemplate) storage.get(tk); + synchronized (cacheStorage) { + oldCachedResult = (CachedResult) cacheStorage.get(cacheKey); } } final long now = System.currentTimeMillis(); - long lastModified = -1L; - boolean rethrown = false; + boolean rethrownCachedException = false; + boolean suppressFinallyException = false; TemplateLookupResult newLookupResult = null; + CachedResult newCachedResult = null; + TemplateLoaderSession session = null; try { - if (cachedTemplate != null) { - // If we're within the refresh delay, return the cached copy - if (now - cachedTemplate.lastChecked < updateDelay) { + if (oldCachedResult != null) { + // If we're within the refresh delay, return the cached result + if (now - oldCachedResult.lastChecked < updateDelay) { if (debug) { - LOG.debug(debugName + " cached copy not yet stale; using cached."); + LOG.debug(debugPrefix + "Cached copy not yet stale; using cached."); } - // Can be null, indicating a cached negative lookup - Object t = cachedTemplate.templateOrException; + 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) { - throwLoadFailedException((RuntimeException) t); + rethrowCachedException((RuntimeException) t); } else if (t instanceof IOException) { - rethrown = true; - throwLoadFailedException((IOException) t); + rethrownCachedException = true; + rethrowCachedException((IOException) t); } - throw new BugException("t is " + t.getClass().getName()); + throw new BugException("Unhandled class for t: " + t.getClass().getName()); } + // The freshness of the cache result must be checked. - // 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; + // Clone, as the instance in the cache store must not be modified to ensure proper concurrent behavior. + newCachedResult = oldCachedResult.clone(); + newCachedResult.lastChecked = now; - // Find the template source - newLookupResult = lookupTemplate(name, locale, customLookupCondition); + 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 - if (!newLookupResult.isPositive()) { + // Template source was removed (TemplateLoader2ResultStatus.NOT_FOUND, or no TemplateLoader2Result) + if (!newLookupResult.isPositive()) { if (debug) { - LOG.debug(debugName + " no source found."); + LOG.debug(debugPrefix + "No source found."); } - storeNegativeLookup(tk, cachedTemplate, null); + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null); return null; } - // If the source didn't change and its last modified date - // also didn't change, return the cached version. - final Object newLookupResultSource = newLookupResult.getTemplateSource(); - lastModified = templateLoader.getLastModified(newLookupResultSource); - boolean lastModifiedNotChanged = lastModified == cachedTemplate.lastModified; - boolean sourceEquals = newLookupResultSource.equals(cachedTemplate.source); - if (lastModifiedNotChanged && sourceEquals) { + final TemplateLoadingResult newTemplateLoaderResult = newLookupResult.getTemplateLoaderResult(); + if (newTemplateLoaderResult.getStatus() == TemplateLoadingResultStatus.NOT_MODIFIED) { + // Return the cached version. if (debug) { - LOG.debug(debugName + ": using cached since " + newLookupResultSource + " hasn't changed."); + LOG.debug(debugPrefix + ": Using cached template " + + "(source: " + newTemplateLoaderResult.getSource() + ")" + + " as it hasn't been changed on the backing store."); } - storeCached(tk, cachedTemplate); - return (Template) cachedTemplate.templateOrException; - } else if (debug) { - if (!sourceEquals) { - LOG.debug("Updating source because: " + - "sourceEquals=" + sourceEquals + - ", newlyFoundSource=" + StringUtil.jQuoteNoXSS(newLookupResultSource) + - ", cached.source=" + StringUtil.jQuoteNoXSS(cachedTemplate.source)); - } else if (!lastModifiedNotChanged) { - LOG.debug("Updating source because: " + - "lastModifiedNotChanged=" + lastModifiedNotChanged + - ", cached.lastModified=" + cachedTemplate.lastModified + - " != source.lastModified=" + lastModified); + putIntoCache(cacheKey, newCachedResult); + return (Template) newCachedResult.templateOrException; + } else { + if (newTemplateLoaderResult.getStatus() != TemplateLoadingResultStatus.OPENED) { + // TemplateLoader2ResultStatus.NOT_FOUND was already handler earlier + throw new BugException("Unxpected status: " + newTemplateLoaderResult.getStatus()); + } + if (debug) { + StringBuilder debugMsg = new StringBuilder(); + debugMsg.append(debugPrefix) + .append("Reloading template instead of using the cached result because "); + if (newCachedResult.templateOrException instanceof Throwable) { + debugMsg.append("it's a cached error (retrying)."); + } else { + Object newSource = newTemplateLoaderResult.getSource(); + if (!nullSafeEquals(newSource, oldCachedResult.source)) { + debugMsg.append("the source has been changed: ") + .append("cached.source=").append(StringUtil.jQuoteNoXSS(oldCachedResult.source)) + .append(", current.source=").append(StringUtil.jQuoteNoXSS(newSource)); + } else { + Serializable newVersion = newTemplateLoaderResult.getVersion(); + if (!nullSafeEquals(oldCachedResult.version, newVersion)) { + debugMsg.append("the version has been changed: ") + .append("cached.version=").append(oldCachedResult.version) + .append(", current.version=").append(newVersion); + } else { + debugMsg.append("??? (unknown reason)"); + } + } + } + LOG.debug(debugMsg.toString()); } } - } else { + } else { // if there was no cached result if (debug) { - LOG.debug("Couldn't find template in cache for " + debugName + "; will try to load it."); + LOG.debug(debugPrefix + "No cached result was found; will try to load template."); } - // 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; + newCachedResult = new CachedResult(); + newCachedResult.lastChecked = now; + + session = templateLoader.createSession(); + if (debug && session != null) { + LOG.debug(debugPrefix + "Session created."); + } - newLookupResult = lookupTemplate(name, locale, customLookupCondition); + newLookupResult = lookupAndLoadTemplateIfChanged( + name, locale, customLookupCondition, null, null, session); if (!newLookupResult.isPositive()) { - storeNegativeLookup(tk, cachedTemplate, null); + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null); return null; } - - cachedTemplate.lastModified = lastModified = Long.MIN_VALUE; } + // We have newCachedResult and newLookupResult initialized at this point. - Object source = newLookupResult.getTemplateSource(); - cachedTemplate.source = source; + TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult(); + newCachedResult.source = templateLoaderResult.getSource(); // If we get here, then we need to (re)load the template if (debug) { - LOG.debug("Loading template for " + debugName + " from " + StringUtil.jQuoteNoXSS(source)); + LOG.debug(debugPrefix + "Reading template content (source: " + + StringUtil.jQuoteNoXSS(newCachedResult.source) + ")"); } - lastModified = lastModified == Long.MIN_VALUE ? templateLoader.getLastModified(source) : lastModified; Template template = loadTemplate( - templateLoader, source, + templateLoaderResult, name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition, encoding, parseAsFTL); - cachedTemplate.templateOrException = template; - cachedTemplate.lastModified = lastModified; - storeCached(tk, cachedTemplate); + if (session != null) { + session.close(); + if (debug) { + LOG.debug(debugPrefix + "Session closed."); + } + } + newCachedResult.templateOrException = template; + newCachedResult.version = templateLoaderResult.getVersion(); + putIntoCache(cacheKey, newCachedResult); return template; } catch (RuntimeException e) { - if (cachedTemplate != null) { - storeNegativeLookup(tk, cachedTemplate, e); + if (newCachedResult != null) { + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e); } + suppressFinallyException = true; throw e; } catch (IOException e) { - if (!rethrown) { - storeNegativeLookup(tk, cachedTemplate, 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 { - if (newLookupResult != null && newLookupResult.isPositive()) { - templateLoader.closeTemplateSource(newLookupResult.getTemplateSource()); + try { + // Close streams first: + + if (newLookupResult != null && newLookupResult.isPositive()) { + TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult(); + Reader reader = templateLoaderResult.getReader(); + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { // [FM3] Exception e + if (suppressFinallyException) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close template content Reader for: " + name, e); + } + } else { + suppressFinallyException = true; + throw e; + } + } + } else if (templateLoaderResult.getInputStream() != null) { + try { + templateLoaderResult.getInputStream().close(); + } catch (IOException e) { // [FM3] Exception e + if (suppressFinallyException) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close template content InputStream for: " + name, e); + } + } else { + suppressFinallyException = true; + throw e; + } + } + } + } + } finally { + // Then close streams: + + if (session != null && !session.isClosed()) { + try { + session.close(); + if (debug) { + LOG.debug(debugPrefix + "Session closed."); + } + } catch (IOException e) { // [FM3] Exception e + if (suppressFinallyException) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close template loader session for" + name, e); + } + } else { + suppressFinallyException = true; + throw e; + } + } + } } } } + + private static final Method INIT_CAUSE = getInitCauseMethod(); private static final Method getInitCauseMethod() { @@ -498,39 +546,59 @@ public class TemplateCache { return ioe; } - private void throwLoadFailedException(Throwable e) throws IOException { + 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 storeNegativeLookup(TemplateKey tk, - CachedTemplate cachedTemplate, Exception e) { - cachedTemplate.templateOrException = e; - cachedTemplate.source = null; - cachedTemplate.lastModified = 0L; - storeCached(tk, cachedTemplate); + private void setToNegativeAndPutIntoCache(CachedResultKey cacheKey, CachedResult cachedResult, Exception e) { + cachedResult.templateOrException = e; + cachedResult.source = null; + cachedResult.version = null; + putIntoCache(cacheKey, cachedResult); } - private void storeCached(TemplateKey tk, CachedTemplate cachedTemplate) { - if (isStorageConcurrent) { - storage.put(tk, cachedTemplate); + private void putIntoCache(CachedResultKey tk, CachedResult cachedTemplate) { + if (isCacheStorageConcurrent) { + cacheStorage.put(tk, cachedTemplate); } else { - synchronized (storage) { - storage.put(tk, cachedTemplate); + synchronized (cacheStorage) { + cacheStorage.put(tk, cachedTemplate); } } } + @SuppressWarnings("deprecation") private Template loadTemplate( - final TemplateLoader templateLoader, final Object source, + TemplateLoadingResult templateLoaderResult, final String name, final String sourceName, Locale locale, final Object customLookupCondition, String initialEncoding, final boolean parseAsFTL) throws IOException { - final TemplateConfiguration tc; - try { - tc = templateConfigurations != null ? templateConfigurations.get(sourceName, source) : null; - } catch (TemplateConfigurationFactoryException e) { - throw newIOException("Error while getting TemplateConfiguration; see cause exception.", e); + TemplateConfiguration tc; + { + TemplateConfiguration cfgTC; + try { + cfgTC = templateConfigurations != null + ? templateConfigurations.get(sourceName, templateLoaderResult.getSource()) : null; + } catch (TemplateConfigurationFactoryException e) { + throw newIOException("Error while getting TemplateConfiguration; see cause exception.", e); + } + TemplateConfiguration resultTC = templateLoaderResult.getTemplateConfiguration(); + if (resultTC != null) { + TemplateConfiguration mergedTC = new TemplateConfiguration(); + if (cfgTC != null) { + mergedTC.merge(cfgTC); + } + if (resultTC != null) { + mergedTC.merge(resultTC); + } + mergedTC.setParentConfiguration(config); + + tc = mergedTC; + } else { + tc = cfgTC; + } } + if (tc != null) { // TC.{encoding,locale} is stronger than the cfg.getTemplate arguments by design. if (tc.isEncodingSet()) { @@ -543,47 +611,70 @@ public class TemplateCache { Template template; { - if (parseAsFTL) { - try { - final Reader reader = templateLoader.getReader(source, initialEncoding); - try { - template = new Template(name, sourceName, reader, config, tc, initialEncoding); - } finally { - reader.close(); - } - } catch (Template.WrongEncodingException wee) { - String actualEncoding = wee.getTemplateSpecifiedEncoding(); - if (LOG.isDebugEnabled()) { - LOG.debug("Initial encoding \"" + initialEncoding + "\" was incorrect, re-reading with \"" - + actualEncoding + "\". Template: " + sourceName); - } - - final Reader reader = templateLoader.getReader(source, actualEncoding); - try { - template = new Template(name, sourceName, reader, config, tc, actualEncoding); - } finally { - reader.close(); + Reader reader = templateLoaderResult.getReader(); + InputStream inputStream = templateLoaderResult.getInputStream(); + TemplateSpecifiedEncodingHandler templateSpecifiedEncodingHandler; + if (reader != null) { + if (inputStream != null) { + throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName() + + ", both getReader() and getInputStream() has returned non-null."); + } + initialEncoding = null; // No charset decoding has happened + templateSpecifiedEncodingHandler = TemplateSpecifiedEncodingHandler.DEFAULT; + } else if (inputStream != null) { + if (parseAsFTL) { + // We need mark support, to restart if the charset suggested by <#ftl encoding=...> differs + // from that we use initially. + if (!inputStream.markSupported()) { + inputStream = new BufferedInputStream(inputStream); } + inputStream.mark(Integer.MAX_VALUE); // [FM3] Mark should be released after the 1st FTL tag + templateSpecifiedEncodingHandler = new MarkReleaserTemplateSpecifiedEncodingHandler( + inputStream); + } else { + templateSpecifiedEncodingHandler = null; } + reader = new InputStreamReader(inputStream, initialEncoding); } else { - // Read the contents into a StringWriter, then construct a single-text-block template from it. - final StringWriter sw = new StringWriter(); - final char[] buf = new char[4096]; - final Reader reader = templateLoader.getReader(source, initialEncoding); - try { - fetchChars: while (true) { - int charsRead = reader.read(buf); - if (charsRead > 0) { - sw.write(buf, 0, charsRead); - } else if (charsRead < 0) { - break fetchChars; + throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName() + + ", both getReader() and getInputStream() has returned null."); + } + + try { + if (parseAsFTL) { + try { + template = new Template(name, sourceName, reader, config, tc, + initialEncoding, templateSpecifiedEncodingHandler); + } catch (Template.WrongEncodingException wee) { + final String templateSpecifiedEncoding = wee.getTemplateSpecifiedEncoding(); + + if (inputStream != null) { + // We restart InputStream to re-decode it with the new charset. + inputStream.reset(); + + // Don't close `reader`; it's an InputStreamReader that would close the wrapped InputStream. + reader = new InputStreamReader(inputStream, templateSpecifiedEncoding); + } else { + // Should be impossible to get here + throw new BugException(); } + + template = new Template(name, sourceName, reader, config, tc, + templateSpecifiedEncoding, templateSpecifiedEncodingHandler); + } + } else { + // Read the contents into a StringWriter, then construct a single-text-block template from it. + final StringBuilder sb = new StringBuilder(); + final char[] buf = new char[4096]; + int charsRead; + while ((charsRead = reader.read(buf)) > 0) { + sb.append(buf, 0, charsRead); } - } finally { - reader.close(); + template = Template.getPlainTextTemplate(name, sourceName, sb.toString(), config); + template.setEncoding(initialEncoding); } - template = Template.getPlainTextTemplate(name, sourceName, sw.toString(), config); - template.setEncoding(initialEncoding); + } finally { + reader.close(); } } @@ -644,17 +735,29 @@ public class TemplateCache { } /** - * 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. + * Removes all entries from the cache, forcing reloading of templates on subsequent + * {@link #getTemplate(String, Locale, String, boolean)} calls. + * + * @param resetTemplateLoader + * Whether to call {@link TemplateLoader#resetState()}. on the template loader. + */ + public void clear(boolean resetTemplateLoader) { + synchronized (cacheStorage) { + cacheStorage.clear(); + if (templateLoader != null && resetTemplateLoader) { + templateLoader.resetState(); + } + } + } + + /** + * Same as {@link #clear(boolean)} with {@code true} {@code resetTemplateLoader} argument. */ public void clear() { - synchronized (storage) { - storage.clear(); - if (templateLoader instanceof StatefulTemplateLoader) { - ((StatefulTemplateLoader) templateLoader).resetState(); + synchronized (cacheStorage) { + cacheStorage.clear(); + if (templateLoader != null) { + templateLoader.resetState(); } } } @@ -691,31 +794,32 @@ public class TemplateCache { name = templateNameFormat.normalizeAbsoluteName(name); if (name != null && templateLoader != null) { boolean debug = LOG.isDebugEnabled(); - String debugName = debug - ? buildDebugName(name, locale, customLookupCondition, encoding, parse) + String debugPrefix = debug + ? getDebugPrefix("removeTemplate", name, locale, customLookupCondition, encoding, parse) : null; - TemplateKey tk = new TemplateKey(name, locale, customLookupCondition, encoding, parse); + CachedResultKey tk = new CachedResultKey(name, locale, customLookupCondition, encoding, parse); - if (isStorageConcurrent) { - storage.remove(tk); + if (isCacheStorageConcurrent) { + cacheStorage.remove(tk); } else { - synchronized (storage) { - storage.remove(tk); + synchronized (cacheStorage) { + cacheStorage.remove(tk); } } if (debug) { - LOG.debug(debugName + " was removed from the cache, if it was there"); + LOG.debug(debugPrefix + "Template was removed from the cache, if it was there"); } } } - private String buildDebugName(String name, Locale locale, Object customLookupCondition, String encoding, + private String getDebugPrefix(String operation, String name, Locale locale, Object customLookupCondition, String encoding, boolean parse) { - return StringUtil.jQuoteNoXSS(name) + "(" + return operation + " " + StringUtil.jQuoteNoXSS(name) + "(" + StringUtil.jQuoteNoXSS(locale) + (customLookupCondition != null ? ", cond=" + StringUtil.jQuoteNoXSS(customLookupCondition) : "") + ", " + encoding - + (parse ? ", parsed)" : ", unparsed]"); + + (parse ? ", parsed)" : ", unparsed]") + + ": "; } /** @@ -735,110 +839,55 @@ public class TemplateCache { } } - private TemplateLookupResult lookupTemplate(String name, Locale locale, Object customLookupCondition) - throws IOException { + /** + * 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 TemplateLookupResult lookupAndLoadTemplateIfChanged( + String name, Locale locale, Object customLookupCondition, + TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion, + TemplateLoaderSession session) throws IOException { final TemplateLookupResult lookupResult = templateLookupStrategy.lookup( - new TemplateCacheTemplateLookupContext(name, locale, customLookupCondition)); + new TemplateCacheTemplateLookupContext( + name, locale, customLookupCondition, + cachedResultSource, cachedResultVersion, + session)); if (lookupResult == null) { throw new NullPointerException("Lookup result shouldn't be null"); } return lookupResult; } - private TemplateLookupResult lookupTemplateWithAcquisitionStrategy(String path) throws IOException { - int asterisk = path.indexOf(ASTERISK); - // Shortcut in case there is no acquisition - if (asterisk == -1) { - return TemplateLookupResult.from(path, 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 TemplateLookupResult.from(path, 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); - } - StringBuilder buf = new StringBuilder(path.length()).append(basePath); - int l = basePath.length(); - for (; ; ) { - String fullPath = buf.append(resourcePath).toString(); - Object templateSource = findTemplateSource(fullPath); - if (templateSource != null) { - return TemplateLookupResult.from(fullPath, templateSource); - } - if (l == 0) { - return TemplateLookupResult.createNegativeResult(); - } - l = basePath.lastIndexOf(SLASH, l - 2) + 1; - buf.setLength(l); - } - } - - private Object findTemplateSource(String path) throws IOException { - final Object result = templateLoader.findTemplateSource(path); - if (LOG.isDebugEnabled()) { - LOG.debug("TemplateLoader.findTemplateSource(" + StringUtil.jQuote(path) + "): " - + (result == null ? "Not found" : "Found")); - } - return modifyForConfIcI(result); - } - - /** - * If IcI >= 2.3.21, sets {@link URLTemplateSource#setUseCaches(boolean)} to {@code false} for sources that come - * from a {@link TemplateLoader} where {@link URLConnection} cache usage wasn't set explicitly. - */ - private Object modifyForConfIcI(Object templateSource) { - if (templateSource == null) return null; - - if (config.getIncompatibleImprovements().intValue() < _TemplateAPI.VERSION_INT_2_3_21) { - return templateSource; - } - - if (templateSource instanceof URLTemplateSource) { - URLTemplateSource urlTemplateSource = (URLTemplateSource) templateSource; - if (urlTemplateSource.getUseCaches() == null) { // It was left unset - urlTemplateSource.setUseCaches(false); - } - } else if (templateSource instanceof MultiSource) { - modifyForConfIcI(((MultiSource) templateSource).getWrappedSource()); - } - return templateSource; - } - - private String concatPath(List path, int from, int to) { + 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(path.get(i)).append('/'); + 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); + } + /** - * This class holds a (name, locale) pair and is used as the key in - * the cached templates map. + * Used as cache key to look up a {@link CachedResult}. */ - private static final class TemplateKey { + @SuppressWarnings("serial") + private static final class CachedResultKey implements Serializable { private final String name; private final Locale locale; private final Object customLookupCondition; private final String encoding; private final boolean parse; - TemplateKey(String name, Locale locale, Object customLookupCondition, String encoding, boolean parse) { + CachedResultKey(String name, Locale locale, Object customLookupCondition, String encoding, boolean parse) { this.name = name; this.locale = locale; this.customLookupCondition = customLookupCondition; @@ -848,8 +897,8 @@ public class TemplateCache { @Override public boolean equals(Object o) { - if (o instanceof TemplateKey) { - TemplateKey tk = (TemplateKey) o; + if (o instanceof CachedResultKey) { + CachedResultKey tk = (CachedResultKey) o; return parse == tk.parse && name.equals(tk.name) && @@ -860,12 +909,6 @@ public class TemplateCache { return false; } - private boolean nullSafeEquals(Object o1, Object o2) { - return o1 != null - ? (o2 != null ? o1.equals(o2) : false) - : o2 == null; - } - @Override public int hashCode() { return @@ -878,24 +921,27 @@ public class TemplateCache { } /** - * 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. + * Hold the a cached {@link #getTemplate(String, Locale, Object, String, boolean)} result and the associated + * information needed to check if the cached value is up to date. + * + * <p> + * Note: this class is Serializable to allow custom 3rd party CacheStorage implementations to serialize/replicate + * them; FreeMarker code itself doesn't rely on its serializability. + * + * @see CachedResultKey */ - private static final class CachedTemplate implements Cloneable, Serializable { + private static final class CachedResult implements Cloneable, Serializable { private static final long serialVersionUID = 1L; Object templateOrException; - Object source; + TemplateLoadingSource source; + Serializable version; long lastChecked; - long lastModified; - public CachedTemplate cloneCachedTemplate() { + @Override + public CachedResult clone() { try { - return (CachedTemplate) super.clone(); + return (CachedResult) super.clone(); } catch (CloneNotSupportedException e) { throw new UndeclaredThrowableException(e); } @@ -904,18 +950,65 @@ public class TemplateCache { private class TemplateCacheTemplateLookupContext extends TemplateLookupContext { - TemplateCacheTemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition) { - super(templateName, localizedLookup ? templateLocale : null, customLookupCondition); + private final TemplateLoaderSession session; + + TemplateCacheTemplateLookupContext(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 TemplateLookupResult lookupWithAcquisitionStrategy(String name) throws IOException { + public TemplateLookupResult 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 (name.startsWith("/")) { - throw new IllegalArgumentException("Non-normalized name, starts with \"/\": " + name); + if (path.startsWith("/")) { + throw new IllegalArgumentException("Non-normalized name, starts with \"/\": " + path); } - return TemplateCache.this.lookupTemplateWithAcquisitionStrategy(name); + int asterisk = path.indexOf(ASTERISK); + // Shortcut in case there is no acquisition + if (asterisk == -1) { + return TemplateLookupResult.from( + path, + templateLoader.load(path, getCachedResultSource(), getCachedResultVersion(), session)); + } + StringTokenizer pathTokenizer = new StringTokenizer(path, "/"); + int lastAsterisk = -1; + List<String> pathSteps = new ArrayList<String>(); + while (pathTokenizer.hasMoreTokens()) { + String pathStep = pathTokenizer.nextToken(); + if (pathStep.equals(ASTERISKSTR)) { + if (lastAsterisk != -1) { + pathSteps.remove(lastAsterisk); + } + lastAsterisk = pathSteps.size(); + } + pathSteps.add(pathStep); + } + if (lastAsterisk == -1) { // if there was no real "*" step after all + return TemplateLookupResult.from( + 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 TemplateLookupResult.from(fullPath, templateLoaderResult); + } + if (basePathLen == 0) { + return TemplateLookupResult.createNegativeResult(); + } + basePathLen = basePath.lastIndexOf(SLASH, basePathLen - 2) + 1; + buf.setLength(basePathLen); + } } @Override
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateConfigurationFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateConfigurationFactory.java b/src/main/java/freemarker/cache/TemplateConfigurationFactory.java index ec8d2ae..b2561e9 100644 --- a/src/main/java/freemarker/cache/TemplateConfigurationFactory.java +++ b/src/main/java/freemarker/cache/TemplateConfigurationFactory.java @@ -37,10 +37,10 @@ public abstract class TemplateConfigurationFactory { * Returns (maybe creates) the {@link TemplateConfiguration} for the given template source. * * @param sourceName - * The name (path) that was used for {@link TemplateLoader#findTemplateSource(String)}. See + * The name (path) that was used for {@link TemplateLoader#load}. See * {@link Template#getSourceName()} for details. - * @param templateSource - * The object returned by {@link TemplateLoader#findTemplateSource(String)}. + * @param templateLoadingSource + * The object returned by {@link TemplateLoadingResult#getSource()}. * * @return The {@link TemplateConfiguration} to apply, or {@code null} if the there's no {@link TemplateConfiguration} for * this template source. @@ -51,7 +51,7 @@ public abstract class TemplateConfigurationFactory { * @throws TemplateConfigurationFactoryException * If there's a problem that's specific to the factory logic. */ - public abstract TemplateConfiguration get(String sourceName, Object templateSource) + public abstract TemplateConfiguration get(String sourceName, TemplateLoadingSource templateLoadingSource) throws IOException, TemplateConfigurationFactoryException; /** http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateLoader.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateLoader.java b/src/main/java/freemarker/cache/TemplateLoader.java index bab7cae..ae82c71 100644 --- a/src/main/java/freemarker/cache/TemplateLoader.java +++ b/src/main/java/freemarker/cache/TemplateLoader.java @@ -16,50 +16,56 @@ * specific language governing permissions and limitations * under the License. */ - package freemarker.cache; import java.io.IOException; -import java.io.Reader; +import java.io.Serializable; import freemarker.template.Configuration; import freemarker.template.TemplateNotFoundException; /** - * FreeMarker loads template "files" through objects that implement this interface, - * thus the templates need not be real files, and can come from any kind of data source - * (like classpath, servlet context, database, etc). While FreeMarker provides a few - * {@link TemplateLoader} implementations out-of-the-box, it's normal for embedding - * frameworks to use their own implementations. + * FreeMarker loads template "files" through objects that implement this interface, thus the templates need not be real + * files, and can come from any kind of data source (like classpath, servlet context, database, etc). While FreeMarker + * provides a few template loader implementations out-of-the-box, it's normal for embedding frameworks to use their own + * implementations. * - * <p>To set the {@link TemplateLoader} used by FreeMaker, use - * {@link Configuration#setTemplateLoader(TemplateLoader)}. + * <p> + * To set the {@link TemplateLoader} used by FreeMaker, use {@link Configuration#setTemplateLoader(TemplateLoader)}. * - * <p>Implementations of this interface should be thread-safe. + * <p> + * Implementations of this interface should be thread-safe. * - * <p>Implementations should override {@link Object#toString()} to show information about from where the + * <p> + * Implementations should override {@link Object#toString()} to show information about from where the * {@link TemplateLoader} loads the templates. For example, for a template loader that loads template from database * table {@code toString} could return something like * {@code "MyDatabaseTemplateLoader(user=\"cms\", table=\"mail_templates\")"}. This string will be shown in * {@link TemplateNotFoundException} exception messages, next to the template name. * - * <p>For those who has to dig deeper, note that the {@link TemplateLoader} is actually stored inside - * the {@link TemplateCache} of the {@link Configuration}, and is normally only accessed directly - * by the {@link TemplateCache}, and templates are get via the {@link TemplateCache} API-s. + * <p> + * For those who has to dig deeper, note that the {@link TemplateLoader} is actually stored inside the + * {@link TemplateCache} of the {@link Configuration}, and is normally only accessed directly by the + * {@link TemplateCache}, and templates are get via the {@link TemplateCache} API-s. */ public interface TemplateLoader { - + + /** + * Creates a new session, or returns {@code null} if the template loader implementation doesn't support sessions. + * See {@link TemplateLoaderSession} for more information about sessions. + */ + TemplateLoaderSession createSession(); + /** - * Finds the template in the backing storage and returns an object that identifies the storage location where the - * template can be loaded from. See the return value for more information. - * + * Loads the template content together with meta-data such as the version (usually the last modification time). + * * @param name - * The name of the template, already localized and normalized by the - * {@link freemarker.cache.TemplateCache cache}. It is completely up to the loader implementation to + * The name (template root directory relative path) of the template, already localized and normalized by + * the {@link freemarker.cache.TemplateCache cache}. It is completely up to the loader implementation to * interpret the name, however it should expect to receive hierarchical paths where path components are * separated by a slash (not backslash). Backslashes (or any other OS specific separator character) are * not considered as separators by FreeMarker, and thus they will not be replaced with slash before - * passing to this method, so it's up to the template loader to handle them (say, be throwing and + * passing to this method, so it's up to the template loader to handle them (say, by throwing an * exception that tells the user that the path (s)he has entered is invalid, as (s)he must use slash -- * typical mistake of Windows users). The passed names are always considered relative to some * loader-defined root location (often referred as the "template root directory"), and will never start @@ -70,81 +76,27 @@ public interface TemplateLoader { * uses backslash as path separator instead of slash as (the template loader should not accept that), the * normalization will not properly happen, as FreeMarker (the cache) recognizes only the slashes as * separators. - * - * @return An object representing the template source, which can be supplied in subsequent calls to - * {@link #getLastModified(Object)} and {@link #getReader(Object, String)}, when those are called on the - * same {@link TemplateLoader}. {@code null} must be returned if the source for the template doesn't exist; - * don't throw exception then! The exact type of this object is up to the {@link TemplateLoader} - * implementation. As this object is possibly used as hash key in caches, and is surly compared with another - * template source for equality, <b>it must have a proper {@link Object#equals(Object)} and - * {@link Object#hashCode()}) implementation</b>. Especially, template sources that refer to the same - * physical source must be equivalent, otherwise template caching can become inefficient. This is only - * expected from {@link Object#equals(Object)} when the compared template sources came from the same - * {@link TemplateLoader} instance. Also, it must not influence the equality if the source is open or - * closed ({@link #closeTemplateSource(Object)}). - * - * @throws IOException - * When an error occurs that makes it impossible to find out if the template exists, or to access the - * existing template. Don't throw exception if the template doesn't exist, instead return with - * {@code null} then! - */ - public Object findTemplateSource(String name) - throws IOException; - - /** - * Returns the time of last modification of the specified template source. This method is called after - * <code>findTemplateSource()</code>. + * @param ifSourceDiffersFrom + * If we only want to load the template if its source differs from this. {@code null} if you want the + * template to be loaded unconditionally. If this is {@code null} then the + * {@code ifVersionDiffersFrom} parameter must be {@code null} too. See + * {@link TemplateLoadingResult#getSource()} for more about sources. + * @param ifVersionDiffersFrom + * If we only want to load the template if its version (which is usually the last modification time) + * differs from this. {@code null} if {@code ifSourceDiffersFrom} is {@code null}, or if the backing + * storage from which the {@code ifSourceDiffersFrom} template source comes from doesn't store a version. + * See {@link TemplateLoadingResult#getVersion()} for more about versions. * - * @param templateSource - * an object representing a template source, obtained through a prior call to - * {@link #findTemplateSource(String)}. This must be an object on which - * {@link TemplateLoader#closeTemplateSource(Object)} wasn't applied yet. - * @return the time of last modification of the specified template source, or -1 if the time is not known. + * @return Not {@code null}. */ - public long getLastModified(Object templateSource); + TemplateLoadingResult load(String name, TemplateLoadingSource ifSourceDiffersFrom, Serializable ifVersionDiffersFrom, + TemplateLoaderSession session) throws IOException; /** - * Returns the character stream of a template represented by the specified template source. This method is possibly - * called for multiple times for the same template source object, and it must always return a {@link Reader} that - * reads the template from its beginning. Before this method is called for the second time (or later), its caller - * must close the previously returned {@link Reader}, and it must not use it anymore. That is, this method is not - * required to support multiple concurrent readers for the same source {@code templateSource} object. - * - * <p> - * Typically, this method is called if the template is missing from the cache, or if after calling - * {@link #findTemplateSource(String)} and {@link #getLastModified(Object)} it was determined that the cached copy - * of the template is stale. Then, if it turns out that the {@code encoding} parameter used doesn't match the actual - * template content (based on the {@code #ftl encoding=...} header), this method will be called for a second time - * with the correct {@code encoding} parameter value. - * - * @param templateSource - * an object representing a template source, obtained through a prior call to - * {@link #findTemplateSource(String)}. This must be an object on which - * {@link TemplateLoader#closeTemplateSource(Object)} wasn't applied yet. - * @param encoding - * the character encoding used to translate source bytes to characters. Some loaders may not have access - * to the byte representation of the template stream, and instead directly obtain a character stream. - * These loaders should ignore the encoding parameter. - * - * @return A {@link Reader} representing the template character stream. It's the responsibility of the caller (which - * is {@link TemplateCache} usually) to {@code close()} it. The {@link Reader} is not required to work after - * the {@code templateSource} was closed ({@link #closeTemplateSource(Object)}). - * - * @throws IOException - * if an I/O error occurs while accessing the stream. + * Invoked by {@link Configuration#clearTemplateCache()} to instruct this template loader to throw away its current + * state (some kind of cache usually) and start afresh. For most {@link TemplateLoader} implementations this does + * nothing. */ - public Reader getReader(Object templateSource, String encoding) - throws IOException; - - /** - * Closes the template source, releasing any resources held that are only required for reading the template and/or - * its metadata. This is the last method that is called by the {@link TemplateCache} for a template source, except - * that {@link Object#equals(Object)} is might called later too. {@link TemplateCache} ensures that this method will - * be called on every object that is returned from {@link #findTemplateSource(String)}. - * - * @param templateSource - * the template source that should be closed. - */ - public void closeTemplateSource(Object templateSource) - throws IOException; + void resetState(); + } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateLoaderSession.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateLoaderSession.java b/src/main/java/freemarker/cache/TemplateLoaderSession.java new file mode 100644 index 0000000..318fe11 --- /dev/null +++ b/src/main/java/freemarker/cache/TemplateLoaderSession.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package freemarker.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +/** + * Stores shared state between {@link TemplateLoader} operations that are executed close to each other in the same + * thread. For example, a {@link TemplateLoader} that reads from a database might wants to store the database + * connection in it for reuse. The goal of sessions is mostly to increase performance. However, because a + * {@link TemplateCache#getTemplate(String, java.util.Locale, Object, String, boolean)} call is executed inside a single + * session, sessions can be also be utilized to ensure that the template lookup (see {@link TemplateLookupStrategy}) + * happens on a consistent view (a snapshot) of the backing storage, if the backing storage mechanism supports such + * thing. + * + * <p> + * The {@link TemplateLoaderSession} implementation is (usually) specific to the {@link TemplateLoader} + * implementation. If your {@link TemplateLoader} implementation can't take advantage of sessions, you don't have to + * implement this interface, just return {@code null} for {@link TemplateLoader#createSession()}. + * + * <p> + * {@link TemplateLoaderSession}-s should be lazy, that is, creating an instance should be very fast and should not + * cause I/O. Only when (and if ever) the shared resource stored in the session is needed for the first time should the + * shared resource be initialized. + * + * <p> + * {@link TemplateLoaderSession}-s need not be thread safe. + */ +public interface TemplateLoaderSession { + + /** + * Closes this session, freeing any resources it holds. Further operations involving this session should fail, with + * the exception of {@link #close()} itself, which should be silently ignored. + * + * <p> + * The {@link Reader} or {@link InputStream} contained in the {@link TemplateLoadingResult} must be closed before + * {@link #close()} is called on the session in which the {@link TemplateLoadingResult} was created. Except, if + * closing the {@link Reader} or {@link InputStream} has thrown an exception, the caller should just proceed with + * closing the session regardless. After {@link #close()} was called on the session, the methods of the + * {@link Reader} or {@link InputStream} is allowed to throw an exception, or behave in any other erratic way. + * (Because the caller of this interface is usually FreeMarker (the {@link TemplateCache}), normally you don't have + * to deal with these rules, but it's useful to know the expectations if you implement + * {@link TemplateLoaderSession}.) + * + * <p>The caller of {@link TemplateLoader#createSession()} has to guarantee that {@link #close()} will be called on + * the created session. + */ + public void close() throws IOException; + + /** + * Tells if this session is closed. + */ + public boolean isClosed(); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateLoaderUtils.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateLoaderUtils.java b/src/main/java/freemarker/cache/TemplateLoaderUtils.java index e229e76..2c98053 100644 --- a/src/main/java/freemarker/cache/TemplateLoaderUtils.java +++ b/src/main/java/freemarker/cache/TemplateLoaderUtils.java @@ -28,11 +28,10 @@ final class TemplateLoaderUtils { } public static String getClassNameForToString(TemplateLoader templateLoader) { - final Class tlClass = templateLoader.getClass(); + final Class<? extends TemplateLoader> tlClass = templateLoader.getClass(); final Package tlPackage = tlClass.getPackage(); return tlPackage == Configuration.class.getPackage() || tlPackage == TemplateLoader.class.getPackage() ? tlClass.getSimpleName() : tlClass.getName(); } - } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateLoadingResult.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateLoadingResult.java b/src/main/java/freemarker/cache/TemplateLoadingResult.java new file mode 100644 index 0000000..87dcac9 --- /dev/null +++ b/src/main/java/freemarker/cache/TemplateLoadingResult.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package freemarker.cache; + +import java.io.InputStream; +import java.io.Reader; +import java.io.Serializable; +import java.util.Date; + +import freemarker.core.TemplateConfiguration; +import freemarker.template.Configuration; +import freemarker.template.utility.NullArgumentException; + +/** + * Return value of {@link TemplateLoader#load(String, TemplateLoadingSource, Serializable, TemplateLoaderSession)} + */ +public final class TemplateLoadingResult { + private final TemplateLoadingResultStatus status; + private final TemplateLoadingSource source; + private final Serializable version; + private final Reader reader; + private final InputStream inputStream; + private final TemplateConfiguration templateConfiguration; + + public static final TemplateLoadingResult NOT_FOUND = new TemplateLoadingResult( + TemplateLoadingResultStatus.NOT_FOUND); + public static final TemplateLoadingResult NOT_MODIFIED = new TemplateLoadingResult( + TemplateLoadingResultStatus.NOT_MODIFIED); + + /** + * Creates an instance with status {@link TemplateLoadingResultStatus#OPENED}, for a storage mechanism that + * naturally returns the template content as sequence of {@code char}-s as opposed to a sequence of {@code byte}-s. + * This is the case for example when you store the template in a database in a varchar or CLOB. Do <em>not</em> use + * this constructor for stores that naturally return binary data instead (like files, class loader resources, + * BLOB-s, etc.), because using this constructor will disable FreeMarker's charset selection mechanism. + * + * @param source + * See {@link #getSource()} + * @param version + * See {@link #getVersion()} for the meaning of this. Can be {@code null}, but use that only if the + * backing storage mechanism doesn't know this information. + * @param reader + * Gives the content of the template + * @param templateConfiguration + * Usually {@code null}, as usually the backing storage mechanism doesn't store such information; + * see {@link #getTemplateConfiguration()}. + */ + public TemplateLoadingResult(TemplateLoadingSource source, Serializable version, Reader reader, + TemplateConfiguration templateConfiguration) { + NullArgumentException.check("templateSource", source); + NullArgumentException.check("reader", reader); + this.status = TemplateLoadingResultStatus.OPENED; + this.source = source; + this.version = version; + this.reader = reader; + this.inputStream = null; + this.templateConfiguration = templateConfiguration; + } + + /** + * Creates an instance with status {@link TemplateLoadingResultStatus#OPENED}, for a storage mechanism that + * naturally returns the template content as sequence of {@code byte}-s as opposed to a sequence of {@code char}-s. + * This is the case for example when you store the template in a file, classpath resource, or BLOB. Do <em>not</em> + * use this constructor for stores that naturally return text instead (like database varchar and CLOB columns). + * + * @param source + * See {@link #getSource()} + * @param version + * See {@link #getVersion()} for the meaning of this. Can be {@code null}, but use that only if the + * backing storage mechanism doesn't know this information. + * @param inputStream + * Gives the content of the template + * @param templateConfiguration + * Usually {@code null}, as usually the backing storage mechanism doesn't store such information; see + * {@link #getTemplateConfiguration()}. The most probable application is supplying the charset (encoding) + * used by the {@link InputStream} (via {@link TemplateConfiguration#setEncoding(String)}), but only + * do that if the storage mechanism really knows what the charset is. + */ + public TemplateLoadingResult(TemplateLoadingSource source, Serializable version, InputStream inputStream, + TemplateConfiguration templateConfiguration) { + NullArgumentException.check("templateSource", source); + NullArgumentException.check("inputStream", inputStream); + this.status = TemplateLoadingResultStatus.OPENED; + this.source = source; + this.version = version; + this.reader = null; + this.inputStream = inputStream; + this.templateConfiguration = templateConfiguration; + } + + /** + * Used internally for creating the singleton instances which has a state where all other fields are {@code null}. + */ + private TemplateLoadingResult(TemplateLoadingResultStatus status) { + this.status = status; + this.source = null; + this.version = null; + this.reader = null; + this.inputStream = null; + this.templateConfiguration = null; + } + + /** + * Returns non-{@code null} exactly if {@link #getStatus()} is {@link TemplateLoadingResultStatus#OPENED} and the + * backing store mechanism returns content as {@code byte}-s, as opposed to as {@code chars}-s. See also + * {@link #TemplateLoadingResult(TemplateLoadingSource, Serializable, InputStream, TemplateConfiguration)}. It's the + * responsibility of the caller (which is {@link TemplateCache} usually) to {@code close()} the {@link InputStream}. + * The return value is always the same instance, no mater when and how many times this method is called. + * + * <p> + * The return {@code InputStream} should use buffering if that's useful considering the backing storage mechanism. + * TODO Is it really needed? + * + * @return {@code null} or a {@code InputStream} to read the template content; see method description for more. + */ + public InputStream getInputStream() { + return inputStream; + } + + /** + * Tells what kind of result this is; see the documentation of {@link TemplateLoadingResultStatus}. + */ + public TemplateLoadingResultStatus getStatus() { + return status; + } + + /** + * Identifies the source on the level of the storage mechanism; stored in the cache together with the version + * ({@link #getVersion()}). When checking if a cache entry is up to date, the sources are compared, and only if they + * are equal are the versions compared. See more at {@link TemplateLoadingSource}. + */ + public TemplateLoadingSource getSource() { + return source; + } + + /** + * If the result status is {@link TemplateLoadingResultStatus#OPENED} and the backing storage stores such + * information, the version (usually the last modification time) of the loaded template, otherwise {@code null}. The + * version is some kind of value which changes when the template in the backing storage is updated. Usually, it's + * the last modification time (a {@link Date} or {@link Long}), though that can be problematic if the template can + * change twice within the granularity of the clock used by the storage. Thus some storages may use a revision + * number instead that's always increased when the template is updated, or the cryptographic hash of the template + * content as the version. Version objects are compared with each other with their {@link Object#equals(Object)} + * method, to see if a cache entry is outdated (though only when the source objects ({@link #getSource()}) are + * equal). Thus, the version object must have proper {@link Object#equals(Object)} and {@link Object#hashCode()} + * methods. + */ + public Serializable getVersion() { + return version; + } + + /** + * Returns non-{@code null} exactly if {@link #getStatus()} is {@link TemplateLoadingResultStatus#OPENED} and the + * backing store mechanism returns content as {@code char}-s, as opposed to as {@code byte}-s. See also + * {@link #TemplateLoadingResult(TemplateLoadingSource, Serializable, Reader, TemplateConfiguration)}. It's the + * responsibility of the caller (which is {@link TemplateCache} usually) to {@code close()} the {@link Reader}. The + * return value is always the same instance, no mater when and how many times this method is called. + * + * @return {@code null} or a {@code Reader} to read the template content; see method description for more. + */ + public Reader getReader() { + return reader; + } + + /** + * If {@link #getStatus()} is {@link TemplateLoadingResultStatus#OPENED}, and the template loader stores such + * information (which is rare) then it returns the {@link TemplateConfiguration} applicable to the template, + * otherwise it returns {@code null}. If {@link #getStatus()} is not {@link TemplateLoadingResultStatus#OPENED}, + * then this should always return {@code null}. If there are {@link TemplateConfiguration}-s coming from other + * sources, such as from {@link Configuration#getTemplateConfigurations()}, this won't replace them, but will be + * merged with them, with properties coming from the returned {@link TemplateConfiguration} having the highest + * priority. + * + * @return {@code null}, or a {@link TemplateConfiguration}. The parent configuration of the + * {@link TemplateConfiguration} need not be set. The returned {@link TemplateConfiguration} won't be + * modified. (If the caller needs to modify it, such as to call + * {@link TemplateConfiguration#setParentConfiguration(Configuration)}, it has to copy it first.) + */ + public TemplateConfiguration getTemplateConfiguration() { + return templateConfiguration; + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateLoadingResultStatus.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateLoadingResultStatus.java b/src/main/java/freemarker/cache/TemplateLoadingResultStatus.java new file mode 100644 index 0000000..6ea5e50 --- /dev/null +++ b/src/main/java/freemarker/cache/TemplateLoadingResultStatus.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package freemarker.cache; + +import java.io.Serializable; + +/** + * Used for the value of {@link TemplateLoadingResult#getStatus()}. + */ +public enum TemplateLoadingResultStatus { + + /** + * The template with the requested name doesn't exist (not to be confused with "wasn't accessible due to error"). If + * there was and error because of which we can't know for sure if the template is there or not (for example we + * couldn't access the backing store due to a network connection error or other unexpected I/O error or + * authorization problem), this value must not be used, instead an exception should be thrown by + * {@link TemplateLoader#load(String, TemplateLoadingSource, Serializable, TemplateLoaderSession)}. + */ + NOT_FOUND, + + /** + * If the template was found, but its source and version is the same as that which was provided to + * {@link TemplateLoader#load(String, TemplateLoadingSource, Serializable, TemplateLoaderSession)} (from a cache + * presumably), so its content wasn't opened for reading. + */ + NOT_MODIFIED, + + /** + * If the template was found and its content is ready for reading. + */ + OPENED + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3d0fdc8c/src/main/java/freemarker/cache/TemplateLoadingSource.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/cache/TemplateLoadingSource.java b/src/main/java/freemarker/cache/TemplateLoadingSource.java new file mode 100644 index 0000000..ce84d24 --- /dev/null +++ b/src/main/java/freemarker/cache/TemplateLoadingSource.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package freemarker.cache; + +import java.io.Serializable; +import java.util.HashMap; + +/** + * The point of {@link TemplateLoadingSource} is that with their {@link Object#equals(Object)} method we can tell if two + * cache entries were generated from the same physical resource or not. Comparing the template names isn't enough, + * because a {@link TemplateLoader} may uses some kind of fallback mechanism, such as delegating to other + * {@link TemplateLoader}-s until the template is found. Like if we have two {@link FileTemplateLoader}-s with different + * physical root directories, both can contain {@code "foo/bar.ftl"}, but obviously the two files aren't the same. + * + * <p> + * When implementing this interface, check these: + * + * <ul> + * <li>{@link Object#equals(Object)} must not be based on object identity, because two instances of + * {@link TemplateLoadingSource} that describe the same resource must be equivalent. + * + * <li>Each {@link TemplateLoader} implementation should have its own {@link TemplateLoadingSource} implementation, so + * that {@link TemplateLoadingSource}-s coming from different {@link TemplateLoader} implementations can't be + * accidentally equal (according to {@link Object#equals(Object)}). + * + * <li>{@link Object#equals(Object)} must still work properly if there are multiple instances of the same + * {@link TemplateLoader} implementation. Like if you have an implementation that loads from a database table, the + * {@link TemplateLoadingSource} should certain contain the JDBC connection string, the table name and the row ID, not + * just the row ID. + * + * <li>Together with {@link Object#equals(Object)}, {@link Object#hashCode()} must be also overridden. The template + * source may be used as a {@link HashMap} key. + * + * <li>Because {@link TemplateLoadingSource}-s are {@link Serializable}, they can't contain non-{@link Serializable} + * fields. Most notably, a reference to the creator {@link TemplateLoader}, so if it's an inner class of the + * {@link TemplateLoader}, it should be static. + * + * <li>Consider that cache entries, in which the source is stored, may move between JVM-s (because of clustering with a + * distributed cache). Thus they should remain meaningful for the purpose of {@link Object#equals(Object)} even in + * another JVM. + * + * <li>A {@link TemplateLoader} may chose not to support distributed caches, like {@link ByteArrayTemplateLoader} + * doesn't support that for example. In that case the serialization related points above can be ignored, but the + * {@link TemplateLoadingSource} implementation should define the {@code writeObject} method (a Java serialization + * feature) and throw an exception there to block serialization attempts. + * </ul> + */ +public interface TemplateLoadingSource extends Serializable { + // Empty +}
