You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/02/10 23:47:08 UTC
[4/5] incubator-freemarker git commit: - Totally redesigned
TemplateLoader interface. The FM2 TemplateLoader can't be adapted (wrapped)
to it, but usually
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
+}