Added early draft of TemplateResolver, renamed TemplateCache to DefaultTemplateResolver. TemplateResolver is not yet directly used in Configuration. This was only added in a hurry, so that it's visible why the o.a.f.core.templateresolver subpackage name makes sense.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/94d39312 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/94d39312 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/94d39312 Branch: refs/heads/3 Commit: 94d39312b09629e81beffb0d9b2029e5829a6d10 Parents: ecb4e23 Author: ddekany <[email protected]> Authored: Thu Feb 16 23:56:05 2017 +0100 Committer: ddekany <[email protected]> Committed: Fri Feb 17 00:07:40 2017 +0100 ---------------------------------------------------------------------- .../apache/freemarker/core/Configuration.java | 168 +-- .../org/apache/freemarker/core/Template.java | 4 +- .../apache/freemarker/core/ast/Environment.java | 7 +- .../core/ast/TemplateConfiguration.java | 4 +- .../core/templateresolver/CacheStorage.java | 2 +- .../DefaultTemplateResolver.java | 1028 ++++++++++++++++ .../templateresolver/GetTemplateResult.java | 88 ++ .../core/templateresolver/MruCacheStorage.java | 4 +- .../core/templateresolver/TemplateCache.java | 1111 ------------------ .../core/templateresolver/TemplateLoader.java | 6 +- .../templateresolver/TemplateLoaderSession.java | 4 +- .../templateresolver/TemplateLoadingResult.java | 4 +- .../templateresolver/TemplateNameFormat.java | 32 +- .../core/templateresolver/TemplateResolver.java | 165 +++ src/manual/en_US/FM3-CHANGE-LOG.txt | 5 +- .../freemarker/core/ConfigurationTest.java | 4 +- ...plateConfigurationWithTemplateCacheTest.java | 326 ----- ...teConfigurationWithTemplateResolverTest.java | 326 +++++ .../DefaultTemplateResolverTest.java | 479 ++++++++ .../templateresolver/TemplateCacheTest.java | 488 -------- 20 files changed, 2196 insertions(+), 2059 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/Configuration.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/Configuration.java b/src/main/java/org/apache/freemarker/core/Configuration.java index 84cbed2..7612820 100644 --- a/src/main/java/org/apache/freemarker/core/Configuration.java +++ b/src/main/java/org/apache/freemarker/core/Configuration.java @@ -88,13 +88,19 @@ import org.apache.freemarker.core.model.impl.beans.BeansWrapper; import org.apache.freemarker.core.model.impl.beans.BeansWrapperBuilder; import org.apache.freemarker.core.templateresolver.CacheStorage; import org.apache.freemarker.core.templateresolver.ClassTemplateLoader; +import org.apache.freemarker.core.templateresolver.DefaultTemplateResolver; import org.apache.freemarker.core.templateresolver.FileTemplateLoader; +import org.apache.freemarker.core.templateresolver.GetTemplateResult; import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; import org.apache.freemarker.core.templateresolver.MruCacheStorage; import org.apache.freemarker.core.templateresolver.MultiTemplateLoader; import org.apache.freemarker.core.templateresolver.SoftCacheStorage; -import org.apache.freemarker.core.templateresolver.TemplateCache; -import org.apache.freemarker.core.templateresolver.TemplateCache.MaybeMissingTemplate; +import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory; +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.templateresolver.TemplateLookupContext; +import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy; +import org.apache.freemarker.core.templateresolver.TemplateNameFormat; +import org.apache.freemarker.core.templateresolver.URLTemplateLoader; import org.apache.freemarker.core.util.CaptureOutput; import org.apache.freemarker.core.util.ClassUtil; import org.apache.freemarker.core.util.Constants; @@ -105,12 +111,6 @@ import org.apache.freemarker.core.util.SecurityUtilities; import org.apache.freemarker.core.util.StandardCompress; import org.apache.freemarker.core.util.StringUtil; import org.apache.freemarker.core.util.XmlEscape; -import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory; -import org.apache.freemarker.core.templateresolver.TemplateLoader; -import org.apache.freemarker.core.templateresolver.TemplateLookupContext; -import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy; -import org.apache.freemarker.core.templateresolver.TemplateNameFormat; -import org.apache.freemarker.core.templateresolver.URLTemplateLoader; /** * <b>The main entry point into the FreeMarker API</b>; encapsulates the configuration settings of FreeMarker, @@ -120,8 +120,8 @@ import org.apache.freemarker.core.templateresolver.URLTemplateLoader; * the application life-cycle, set its {@link #setSetting(String, String) configuration settings} there (either with the * setter methods like {@link #setTemplateLoader(TemplateLoader)} or by loading a {@code .properties} file), and then * use that single instance everywhere in your application. Frequently re-creating {@link Configuration} is a typical - * and grave mistake from performance standpoint, as the {@link Configuration} holds the template cache, and often also - * the class introspection cache, which then will be lost. (Note that, naturally, having multiple long-lived instances, + * and grave mistake from performance standpoint, as the {@link Configuration} holds the template templateResolver, and often also + * the class introspection templateResolver, which then will be lost. (Note that, naturally, having multiple long-lived instances, * like one per component that internally uses FreeMarker is fine.) * * <p>The basic usage pattern is like: @@ -508,7 +508,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf private int namingConvention = AUTO_DETECT_NAMING_CONVENTION; private int tabSize = 8; // Default from JavaCC 3.x - private TemplateCache cache; + private DefaultTemplateResolver templateResolver; private boolean templateLoaderExplicitlySet; private boolean templateLookupStrategyExplicitlySet; @@ -601,8 +601,8 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * won't remain hidden now. As the old default is a singleton too, potentially shared by independently * developed components, most of them expects the out-of-the-box behavior from it (and the others are * necessarily buggy). Also, then concurrency glitches can occur (and even pollute the class introspection - * cache) because the singleton is modified after publishing to other threads.) - * Furthermore the new default object wrapper shares class introspection cache with other + * templateResolver) because the singleton is modified after publishing to other threads.) + * Furthermore the new default object wrapper shares class introspection templateResolver with other * {@link BeansWrapper}-s created with {@link BeansWrapperBuilder}, which has an impact as * {@link BeansWrapper#clearClassIntrospecitonCache()} will be disallowed; see more about it there. * </li> @@ -842,7 +842,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf NullArgumentException.check("incompatibleImprovements", incompatibleImprovements); this.incompatibleImprovements = incompatibleImprovements; - createTemplateCache(); + createTemplateResolver(); loadBuiltInSharedVariables(); } @@ -854,33 +854,33 @@ public class Configuration extends Configurable implements Cloneable, ParserConf } } - private void createTemplateCache() { - cache = new TemplateCache( + private void createTemplateResolver() { + templateResolver = new DefaultTemplateResolver( null, getDefaultCacheStorage(), getDefaultTemplateLookupStrategy(), getDefaultTemplateNameFormat(), null, this); - cache.clear(); // for fully BC behavior - cache.setDelay(5000); + templateResolver.clearTemplateCache(); // for fully BC behavior + templateResolver.setTemplateUpdateDelayMilliseconds(5000); } - private void recreateTemplateCacheWith( + private void recreateTemplateResolverWith( TemplateLoader loader, CacheStorage storage, TemplateLookupStrategy templateLookupStrategy, TemplateNameFormat templateNameFormat, TemplateConfigurationFactory templateConfigurations) { - TemplateCache oldCache = cache; - cache = new TemplateCache( + DefaultTemplateResolver oldCache = templateResolver; + templateResolver = new DefaultTemplateResolver( loader, storage, templateLookupStrategy, templateNameFormat, templateConfigurations, this); - cache.clear(false); - cache.setDelay(oldCache.getDelay()); - cache.setLocalizedLookup(localizedLookup); + templateResolver.clearTemplateCache(false); + templateResolver.setTemplateUpdateDelayMilliseconds(oldCache.getTemplateUpdateDelayMilliseconds()); + templateResolver.setLocalizedLookup(localizedLookup); } - private void recreateTemplateCache() { - recreateTemplateCacheWith(cache.getTemplateLoader(), cache.getCacheStorage(), - cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(), + private void recreateTemplateResolver() { + recreateTemplateResolverWith(templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(), + templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(), getTemplateConfigurations()); } @@ -947,10 +947,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf Configuration copy = (Configuration) super.clone(); copy.sharedVariables = new HashMap(sharedVariables); copy.localeToCharsetMap = new ConcurrentHashMap(localeToCharsetMap); - copy.recreateTemplateCacheWith( - cache.getTemplateLoader(), cache.getCacheStorage(), - cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(), - cache.getTemplateConfigurations()); + copy.recreateTemplateResolverWith( + templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(), + templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(), + templateResolver.getTemplateConfigurations()); return copy; } catch (CloneNotSupportedException e) { throw new BugException("Cloning failed", e); @@ -1117,7 +1117,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf /** * Sets a {@link TemplateLoader} that is used to look up and load templates; - * as a side effect the template cache will be emptied. + * as a side effect the template templateResolver will be emptied. * By providing your own {@link TemplateLoader} implementation, you can load templates from whatever kind of * storages, like from relational databases, NoSQL-storages, etc. * @@ -1137,10 +1137,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf public void setTemplateLoader(TemplateLoader templateLoader) { // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration synchronized (this) { - if (cache.getTemplateLoader() != templateLoader) { - recreateTemplateCacheWith(templateLoader, cache.getCacheStorage(), - cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(), - cache.getTemplateConfigurations()); + if (templateResolver.getTemplateLoader() != templateLoader) { + recreateTemplateResolverWith(templateLoader, templateResolver.getCacheStorage(), + templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(), + templateResolver.getTemplateConfigurations()); } templateLoaderExplicitlySet = true; } @@ -1173,23 +1173,23 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * The getter pair of {@link #setTemplateLoader(TemplateLoader)}. */ public TemplateLoader getTemplateLoader() { - if (cache == null) { + if (templateResolver == null) { return null; } - return cache.getTemplateLoader(); + return templateResolver.getTemplateLoader(); } /** * Sets a {@link TemplateLookupStrategy} that is used to look up templates based on the requested name; as a side - * effect the template cache will be emptied. The default value is {@link TemplateLookupStrategy#DEFAULT_2_3_0}. + * effect the template templateResolver will be emptied. The default value is {@link TemplateLookupStrategy#DEFAULT_2_3_0}. * * @since 2.3.22 */ public void setTemplateLookupStrategy(TemplateLookupStrategy templateLookupStrategy) { - if (cache.getTemplateLookupStrategy() != templateLookupStrategy) { - recreateTemplateCacheWith(cache.getTemplateLoader(), cache.getCacheStorage(), - templateLookupStrategy, cache.getTemplateNameFormat(), - cache.getTemplateConfigurations()); + if (templateResolver.getTemplateLookupStrategy() != templateLookupStrategy) { + recreateTemplateResolverWith(templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(), + templateLookupStrategy, templateResolver.getTemplateNameFormat(), + templateResolver.getTemplateConfigurations()); } templateLookupStrategyExplicitlySet = true; } @@ -1222,10 +1222,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * The getter pair of {@link #setTemplateLookupStrategy(TemplateLookupStrategy)}. */ public TemplateLookupStrategy getTemplateLookupStrategy() { - if (cache == null) { + if (templateResolver == null) { return null; } - return cache.getTemplateLookupStrategy(); + return templateResolver.getTemplateLookupStrategy(); } /** @@ -1235,10 +1235,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * @since 2.3.22 */ public void setTemplateNameFormat(TemplateNameFormat templateNameFormat) { - if (cache.getTemplateNameFormat() != templateNameFormat) { - recreateTemplateCacheWith(cache.getTemplateLoader(), cache.getCacheStorage(), - cache.getTemplateLookupStrategy(), templateNameFormat, - cache.getTemplateConfigurations()); + if (templateResolver.getTemplateNameFormat() != templateNameFormat) { + recreateTemplateResolverWith(templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(), + templateResolver.getTemplateLookupStrategy(), templateNameFormat, + templateResolver.getTemplateConfigurations()); } templateNameFormatExplicitlySet = true; } @@ -1270,10 +1270,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * The getter pair of {@link #setTemplateNameFormat(TemplateNameFormat)}. */ public TemplateNameFormat getTemplateNameFormat() { - if (cache == null) { + if (templateResolver == null) { return null; } - return cache.getTemplateNameFormat(); + return templateResolver.getTemplateNameFormat(); } /** @@ -1291,12 +1291,12 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * @since 2.3.24 */ public void setTemplateConfigurations(TemplateConfigurationFactory templateConfigurations) { - if (cache.getTemplateConfigurations() != templateConfigurations) { + if (templateResolver.getTemplateConfigurations() != templateConfigurations) { if (templateConfigurations != null) { templateConfigurations.setConfiguration(this); } - recreateTemplateCacheWith(cache.getTemplateLoader(), cache.getCacheStorage(), - cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(), + recreateTemplateResolverWith(templateResolver.getTemplateLoader(), templateResolver.getCacheStorage(), + templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(), templateConfigurations); } } @@ -1305,31 +1305,31 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * The getter pair of {@link #setTemplateConfigurations(TemplateConfigurationFactory)}. */ public TemplateConfigurationFactory getTemplateConfigurations() { - if (cache == null) { + if (templateResolver == null) { return null; } - return cache.getTemplateConfigurations(); + return templateResolver.getTemplateConfigurations(); } /** * Sets the {@link CacheStorage} used for caching {@link Template}-s; - * the earlier content of the template cache will be dropt. + * the earlier content of the template templateResolver will be dropt. * * The default is a {@link SoftCacheStorage}. If the total size of the {@link Template} * objects is significant but most templates are used rarely, using a * {@link MruCacheStorage} instead might be advisable. If you don't want caching at * all, use {@link org.apache.freemarker.core.templateresolver.NullCacheStorage} (you can't use {@code null}). * - * <p>Note that setting the cache storage will re-create the template cache, so + * <p>Note that setting the templateResolver storage will re-create the template templateResolver, so * all its content will be lost. */ public void setCacheStorage(CacheStorage cacheStorage) { // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration synchronized (this) { if (getCacheStorage() != cacheStorage) { - recreateTemplateCacheWith(cache.getTemplateLoader(), cacheStorage, - cache.getTemplateLookupStrategy(), cache.getTemplateNameFormat(), - cache.getTemplateConfigurations()); + recreateTemplateResolverWith(templateResolver.getTemplateLoader(), cacheStorage, + templateResolver.getTemplateLookupStrategy(), templateResolver.getTemplateNameFormat(), + templateResolver.getTemplateConfigurations()); } cacheStorageExplicitlySet = true; } @@ -1366,10 +1366,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf public CacheStorage getCacheStorage() { // "synchronized" is removed from the API as it's not safe to set anything after publishing the Configuration synchronized (this) { - if (cache == null) { + if (templateResolver == null) { return null; } - return cache.getCacheStorage(); + return templateResolver.getCacheStorage(); } } @@ -1487,7 +1487,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf */ @Deprecated public void setTemplateUpdateDelay(int seconds) { - cache.setDelay(1000L * seconds); + templateResolver.setTemplateUpdateDelayMilliseconds(1000L * seconds); } /** @@ -1496,16 +1496,16 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * * <p> * When you get a template via {@link #getTemplate(String)} (or some of its overloads). FreeMarker will try to get - * the template from the template cache. If the template is found, and at least this amount of time was elapsed + * the template from the template templateResolver. If the template is found, and at least this amount of time was elapsed * since the template last modification date was checked, FreeMarker will re-check the last modification date (this - * could mean I/O), possibly reloading the template and updating the cache as a consequence (can mean even more + * could mean I/O), possibly reloading the template and updating the templateResolver as a consequence (can mean even more * I/O). The {@link #getTemplate(String)} (or some of its overloads) call will only return after this all is * done, so it will return the fresh template. * * @since 2.3.23 */ public void setTemplateUpdateDelayMilliseconds(long millis) { - cache.setDelay(millis); + templateResolver.setTemplateUpdateDelayMilliseconds(millis); } /** @@ -1514,7 +1514,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * @since 2.3.23 */ public long getTemplateUpdateDelayMilliseconds() { - return cache.getDelay(); + return templateResolver.getTemplateUpdateDelayMilliseconds(); } @Override @@ -1624,7 +1624,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * was never set in this {@link Configuration} object through the public API, its value will be set to the default * value appropriate for the new {@code incompatibleImprovements}. (This adjustment of a setting value doesn't * count as setting that setting, so setting {@code incompatibleImprovements} for multiple times also works as - * expected.) Note that if the {@code template_loader} have to be changed because of this, the template cache will + * expected.) Note that if the {@code template_loader} have to be changed because of this, the template templateResolver will * be emptied. * * @throws IllegalArgumentException @@ -1674,7 +1674,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf unsetObjectWrapper(); } - recreateTemplateCache(); + recreateTemplateResolver(); } } @@ -2228,7 +2228,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf } /** - * Retrieves the template with the given name from the template cache, loading it into the cache first if it's + * Retrieves the template with the given name from the template templateResolver, loading it into the templateResolver first if it's * missing/staled. * * <p> @@ -2292,7 +2292,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf /** * Retrieves the template with the given name (and according the specified further parameters) from the template - * cache, loading it into the cache first if it's missing/staled. + * templateResolver, loading it into the templateResolver first if it's missing/staled. * * <p> * This method is thread-safe. @@ -2303,7 +2303,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * @param name * The name or path of the template, which is not a real path, but interpreted inside the current * {@link TemplateLoader}. Can't be {@code null}. The exact syntax of the name depends on the underlying - * {@link TemplateLoader}, but the cache makes some assumptions. First, the name is expected to be a + * {@link TemplateLoader}, but the templateResolver makes some assumptions. First, the name is expected to be a * hierarchical path, with path components separated by a slash character (not with backslash!). The path * (the name) given here must <em>not</em> begin with slash; it's always interpreted relative to the * "template root directory". Then, the {@code ..} and {@code .} path meta-elements will be resolved. For @@ -2314,7 +2314,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * contain at most one path element whose name is {@code *} (asterisk). This path meta-element triggers * the <i>acquisition mechanism</i>. If the template is not found in the location described by the * concatenation of the path left to the asterisk (called base path) and the part to the right of the - * asterisk (called resource path), the cache will attempt to remove the rightmost path component from + * asterisk (called resource path), the templateResolver will attempt to remove the rightmost path component from * the base path ("go up one directory") and concatenate that with the resource path. The process is * repeated until either a template is found, or the base path is completely exhausted. * @@ -2337,7 +2337,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * @param customLookupCondition * This value can be used by a custom {@link TemplateLookupStrategy}; has no effect with the default one. * Can be {@code null} (though it's up to the custom {@link TemplateLookupStrategy} if it allows that). - * This object will be used as part of the cache key, so it must to have a proper + * This object will be used as part of the templateResolver key, so it must to have a proper * {@link Object#equals(Object)} and {@link Object#hashCode()} method. It also should have reasonable * {@link Object#toString()}, as it's possibly quoted in error messages. The expected type is up to the * custom {@link TemplateLookupStrategy}. See also: @@ -2391,7 +2391,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf encoding = getEncoding(locale); } - final MaybeMissingTemplate maybeTemp = cache.getTemplate(name, locale, customLookupCondition, encoding, parseAsFTL); + final GetTemplateResult maybeTemp = templateResolver.getTemplate(name, locale, customLookupCondition, encoding, parseAsFTL); final Template temp = maybeTemp.getTemplate(); if (temp == null) { if (ignoreMissing) { @@ -2685,13 +2685,13 @@ public class Configuration extends Configurable implements Cloneable, ParserConf } /** - * Removes all entries from the template cache, thus forcing reloading of templates + * Removes all entries from the template templateResolver, thus forcing reloading of templates * on subsequent <code>getTemplate</code> calls. * * <p>This method is thread-safe and can be called while the engine processes templates. */ public void clearTemplateCache() { - cache.clear(); + templateResolver.clearTemplateCache(); } /** @@ -2728,9 +2728,9 @@ public class Configuration extends Configurable implements Cloneable, ParserConf } /** - * Removes a template from the template cache, hence forcing the re-loading + * Removes a template from the template templateResolver, hence forcing the re-loading * of it when it's next time requested. This is to give the application - * finer control over cache updating than {@link #setTemplateUpdateDelay(int)} + * finer control over templateResolver updating than {@link #setTemplateUpdateDelay(int)} * alone does. * * <p>For the meaning of the parameters, see @@ -2743,7 +2743,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf public void removeTemplateFromCache( String name, Locale locale, String encoding, boolean parse) throws IOException { - cache.removeTemplate(name, locale, encoding, parse); + templateResolver.removeTemplateFromCache(name, locale, encoding, parse); } /** @@ -2752,7 +2752,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * <p>This method is thread-safe and can be called while the engine works. */ public boolean getLocalizedLookup() { - return cache.getLocalizedLookup(); + return templateResolver.getLocalizedLookup(); } /** @@ -2767,7 +2767,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * template names, use {@link #setTemplateLookupStrategy(TemplateLookupStrategy)} with your custom * {@link TemplateLookupStrategy}. * - * <p>Note that changing the value of this setting causes the template cache to be emptied so that old lookup + * <p>Note that changing the value of this setting causes the template templateResolver to be emptied so that old lookup * results won't be reused (since 2.3.22). * * <p> @@ -2777,7 +2777,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf */ public void setLocalizedLookup(boolean localizedLookup) { this.localizedLookup = localizedLookup; - cache.setLocalizedLookup(localizedLookup); + templateResolver.setLocalizedLookup(localizedLookup); } @Override http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/Template.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/Template.java b/src/main/java/org/apache/freemarker/core/Template.java index 9f1ded1..c78e66c 100644 --- a/src/main/java/org/apache/freemarker/core/Template.java +++ b/src/main/java/org/apache/freemarker/core/Template.java @@ -57,7 +57,7 @@ import org.apache.freemarker.core.model.TemplateHashModel; import org.apache.freemarker.core.model.TemplateModel; import org.apache.freemarker.core.model.TemplateNodeModel; import org.apache.freemarker.core.model.impl.SimpleHash; -import org.apache.freemarker.core.templateresolver.TemplateCache; +import org.apache.freemarker.core.templateresolver.DefaultTemplateResolver; import org.apache.freemarker.core.templateresolver.TemplateLoader; import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy; import org.apache.freemarker.core.util.NullArgumentException; @@ -652,7 +652,7 @@ public class Template extends Configurable { * Mostly only used internally; setter pair of {@link #getCustomLookupCondition()}. This meant to be called directly * after instantiating the template with its constructor, after a successfull lookup that used this condition. So * this should only be called from code that deals with creating new {@code Template} objects, like from - * {@link TemplateCache}. + * {@link DefaultTemplateResolver}. * * @since 2.3.22 */ http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/ast/Environment.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ast/Environment.java b/src/main/java/org/apache/freemarker/core/ast/Environment.java index 34f2c3c..cb7dcfc 100644 --- a/src/main/java/org/apache/freemarker/core/ast/Environment.java +++ b/src/main/java/org/apache/freemarker/core/ast/Environment.java @@ -67,12 +67,13 @@ import org.apache.freemarker.core.model.impl.SimpleHash; import org.apache.freemarker.core.model.impl.SimpleSequence; import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; import org.apache.freemarker.core.templateresolver.TemplateNameFormat; +import org.apache.freemarker.core.templateresolver.TemplateResolver; import org.apache.freemarker.core.templateresolver._CacheAPI; import org.apache.freemarker.core.util.DateUtil; +import org.apache.freemarker.core.util.DateUtil.DateToISO8601CalendarFactory; import org.apache.freemarker.core.util.NullWriter; import org.apache.freemarker.core.util.StringUtil; import org.apache.freemarker.core.util.UndeclaredThrowableException; -import org.apache.freemarker.core.util.DateUtil.DateToISO8601CalendarFactory; import org.slf4j.Logger; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -2449,7 +2450,7 @@ public final class Environment extends Configurable { * @param name * the name of the template, relatively to the template root directory (not the to the directory of the * currently executing template file). (Note that you can use - * {@link org.apache.freemarker.core.templateresolver.TemplateCache#getFullTemplatePath} to convert paths to template root relative + * {@link TemplateResolver#toRootBasedName(String, String)} to convert paths to template root based * paths.) For more details see the identical parameter of * {@link Configuration#getTemplate(String, Locale, String, boolean, boolean)} * @@ -2592,7 +2593,7 @@ public final class Environment extends Configurable { * @param name * the name of the template, relatively to the template root directory (not the to the directory of the * currently executing template file!). (Note that you can use - * {@link org.apache.freemarker.core.templateresolver.TemplateCache#getFullTemplatePath} to convert paths to template root relative + * {@link TemplateResolver#toRootBasedName(String, String)} to convert paths to template root based * paths.) */ public Template getTemplateForImporting(String name) throws IOException { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java b/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java index 55f125d..f4bb357 100644 --- a/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java +++ b/src/main/java/org/apache/freemarker/core/ast/TemplateConfiguration.java @@ -28,14 +28,14 @@ import org.apache.freemarker.core.Configuration; import org.apache.freemarker.core.Template; import org.apache.freemarker.core.Version; import org.apache.freemarker.core._TemplateAPI; -import org.apache.freemarker.core.templateresolver.TemplateCache; +import org.apache.freemarker.core.templateresolver.DefaultTemplateResolver; import org.apache.freemarker.core.util.NullArgumentException; /** * Used for customizing the configuration settings for individual {@link Template}-s (or rather groups of templates), * relatively to the common setting values coming from the {@link Configuration}. This was designed with the standard * template loading mechanism of FreeMarker in mind ({@link Configuration#getTemplate(String)} - * and {@link TemplateCache}), though can also be reused for custom template loading and caching solutions. + * and {@link DefaultTemplateResolver}), though can also be reused for custom template loading and caching solutions. * * <p> * Note on the {@code locale} setting: When used with the standard template loading/caching mechanism ( http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java b/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java index 5ff11c2..1dfe01a 100644 --- a/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java +++ b/src/main/java/org/apache/freemarker/core/templateresolver/CacheStorage.java @@ -24,7 +24,7 @@ package org.apache.freemarker.core.templateresolver; * an object with a key, retrieval and removal via the key. It is actually a * small subset of the {@link java.util.Map} interface. * The implementations can be coded in a non-threadsafe manner as the natural - * user of the cache storage, {@link TemplateCache} does the necessary + * user of the cache storage, {@link DefaultTemplateResolver} does the necessary * synchronization. * * @see org.apache.freemarker.core.Configuration#setCacheStorage(CacheStorage) http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolver.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolver.java b/src/main/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolver.java new file mode 100644 index 0000000..e6b9ea1 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/templateresolver/DefaultTemplateResolver.java @@ -0,0 +1,1028 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.freemarker.core.templateresolver; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.Template; +import org.apache.freemarker.core.TemplateNotFoundException; +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core._TemplateAPI; +import org.apache.freemarker.core.ast.BugException; +import org.apache.freemarker.core.ast.MarkReleaserTemplateSpecifiedEncodingHandler; +import org.apache.freemarker.core.ast.TemplateConfiguration; +import org.apache.freemarker.core.ast.TemplateSpecifiedEncodingHandler; +import org.apache.freemarker.core.util.NullArgumentException; +import org.apache.freemarker.core.util.StringUtil; +import org.apache.freemarker.core.util.UndeclaredThrowableException; +import org.slf4j.Logger; + +/** + * Performs caching and on-demand loading of the templates. + * The actual template "file" loading is delegated to a {@link TemplateLoader} that you can specify in the constructor. + * Some aspects of caching is delegated to a {@link CacheStorage} that you can also specify in the constructor. + * + * <p>Typically you don't instantiate or otherwise use this class directly. The {@link Configuration} embeds an + * instance of this class, that you access indirectly through {@link Configuration#getTemplate(String)} and other + * {@link Configuration} API-s. Then {@link TemplateLoader} and {@link CacheStorage} can be set with + * {@link Configuration#setTemplateLoader(TemplateLoader)} and + * {@link Configuration#setCacheStorage(CacheStorage)}. + */ +public class DefaultTemplateResolver extends TemplateResolver { + + /** + * The default template update delay; see {@link Configuration#setTemplateUpdateDelayMilliseconds(long)}. + * + * @since 2.3.23 + */ + public static final long DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS = 5000L; + + private static final String ASTERISKSTR = "*"; + private static final char ASTERISK = '*'; + private static final char SLASH = '/'; + private static final String LOCALE_PART_SEPARATOR = "_"; + private static final Logger LOG = _CoreLogs.TEMPLATE_RESOLVER; + + /** Maybe {@code null}. */ + private final TemplateLoader templateLoader; + + /** Here we keep our cached templates */ + private final CacheStorage cacheStorage; + private final TemplateLookupStrategy templateLookupStrategy; + private final TemplateNameFormat templateNameFormat; + private final TemplateConfigurationFactory templateConfigurations; + + private final boolean isCacheStorageConcurrent; + /** {@link Configuration#setTemplateUpdateDelayMilliseconds(long)} */ + private long templateUpdateDelayMilliseconds = DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS; + /** {@link Configuration#setLocalizedLookup(boolean)} */ + private boolean localizedLookup = true; + + private Configuration config; + + /** + * Same as {@link #DefaultTemplateResolver(TemplateLoader, CacheStorage, Configuration)} with a new {@link SoftCacheStorage} + * as the 2nd parameter. + * + * @since 2.3.21 + */ + public DefaultTemplateResolver(TemplateLoader templateLoader, Configuration config) { + this(templateLoader, _TemplateAPI.createDefaultCacheStorage(Configuration.VERSION_2_3_0), config); + } + + /** + * Same as + * {@link #DefaultTemplateResolver(TemplateLoader, CacheStorage, TemplateLookupStrategy, TemplateNameFormat, Configuration)} + * with {@link TemplateLookupStrategy#DEFAULT_2_3_0} and {@link TemplateNameFormat#DEFAULT_2_3_0}. + * + * @since 2.3.21 + */ + public DefaultTemplateResolver(TemplateLoader templateLoader, CacheStorage cacheStorage, Configuration config) { + this(templateLoader, cacheStorage, + _TemplateAPI.getDefaultTemplateLookupStrategy(Configuration.VERSION_2_3_0), + _TemplateAPI.getDefaultTemplateNameFormat(Configuration.VERSION_2_3_0), + config); + } + + /** + * Same as + * {@link #DefaultTemplateResolver(TemplateLoader, CacheStorage, TemplateLookupStrategy, TemplateNameFormat, + * TemplateConfigurationFactory, Configuration)} with {@code null} for {@code templateConfigurations}-s. + * + * @since 2.3.22 + */ + public DefaultTemplateResolver(TemplateLoader templateLoader, CacheStorage cacheStorage, + TemplateLookupStrategy templateLookupStrategy, TemplateNameFormat templateNameFormat, + Configuration config) { + this(templateLoader, cacheStorage, templateLookupStrategy, templateNameFormat, null, config); + } + + /** + * @param templateLoader + * The {@link TemplateLoader} to use. Can be {@code null}, though then every request will result in + * {@link TemplateNotFoundException}. + * @param cacheStorage + * The {@link CacheStorage} to use. Can't be {@code null}. + * @param templateLookupStrategy + * The {@link TemplateLookupStrategy} to use. Can't be {@code null}. + * @param templateNameFormat + * The {@link TemplateNameFormat} to use. Can't be {@code null}. + * @param templateConfigurations + * The {@link TemplateConfigurationFactory} to use. Can be {@code null} (then all templates will use the + * settings coming from the {@link Configuration} as is, except in the very rare case where a + * {@link TemplateLoader} itself specifies a {@link TemplateConfiguration}). + * @param config + * The {@link Configuration} this cache will be used for. Can't be {@code null}. + * + * @since 2.3.24 + */ + public DefaultTemplateResolver(TemplateLoader templateLoader, CacheStorage cacheStorage, + TemplateLookupStrategy templateLookupStrategy, TemplateNameFormat templateNameFormat, + TemplateConfigurationFactory templateConfigurations, + Configuration config) { + super(config); + + this.templateLoader = templateLoader; + + NullArgumentException.check("cacheStorage", cacheStorage); + this.cacheStorage = cacheStorage; + isCacheStorageConcurrent = cacheStorage instanceof ConcurrentCacheStorage && + ((ConcurrentCacheStorage) cacheStorage).isConcurrent(); + + NullArgumentException.check("templateLookupStrategy", templateLookupStrategy); + this.templateLookupStrategy = templateLookupStrategy; + + NullArgumentException.check("templateNameFormat", templateNameFormat); + this.templateNameFormat = templateNameFormat; + + // Can be null + this.templateConfigurations = templateConfigurations; + + NullArgumentException.check("config", config); + this.config = config; + } + + /** + * Returns the configuration for internal usage. + */ + @Override + public Configuration getConfiguration() { + return config; + } + + public TemplateLoader getTemplateLoader() { + return templateLoader; + } + + public CacheStorage getCacheStorage() { + return cacheStorage; + } + + /** + * @since 2.3.22 + */ + public TemplateLookupStrategy getTemplateLookupStrategy() { + return templateLookupStrategy; + } + + /** + * @since 2.3.22 + */ + public TemplateNameFormat getTemplateNameFormat() { + return templateNameFormat; + } + + /** + * @since 2.3.24 + */ + public TemplateConfigurationFactory getTemplateConfigurations() { + return templateConfigurations; + } + + /** + * Retrieves the template with the given name (and according the specified further parameters) from the template + * cache, loading it into the cache first if it's missing/staled. + * + * <p> + * All parameters must be non-{@code null}, except {@code customLookupCondition}. For the meaning of the parameters + * see {@link Configuration#getTemplate(String, Locale, String, boolean)}. + * + * @return A {@link GetTemplateResult} object that contains the {@link Template}, or a + * {@link GetTemplateResult} object that contains {@code null} as the {@link Template} and information + * about the missing template. The return value itself is never {@code null}. Note that exceptions occurring + * during template loading will not be classified as a missing template, so they will cause an exception to + * be thrown by this method instead of returning a {@link GetTemplateResult}. The idea is that having a + * missing template is normal (not exceptional), providing that the backing storage mechanism could indeed + * check that it's missing. + * + * @throws MalformedTemplateNameException + * If the {@code name} was malformed according the current {@link TemplateNameFormat}. However, if the + * {@link TemplateNameFormat} is {@link TemplateNameFormat#DEFAULT_2_3_0} and + * {@link Configuration#getIncompatibleImprovements()} is less than 2.4.0, then instead of throwing this + * exception, a {@link GetTemplateResult} will be returned, similarly as if the template were missing + * (the {@link GetTemplateResult#getMissingTemplateReason()} will describe the real error). + * + * @throws IOException + * If reading the template has failed from a reason other than the template is missing. This method + * should never be a {@link TemplateNotFoundException}, as that condition is indicated in the return + * value. + * + * @since 2.3.22 + */ + @Override + public GetTemplateResult getTemplate(String name, Locale locale, Object customLookupCondition, + String encoding, boolean parseAsFTL) + throws IOException { + NullArgumentException.check("name", name); + NullArgumentException.check("locale", locale); + NullArgumentException.check("encoding", encoding); + + try { + name = templateNameFormat.normalizeRootBasedName(name); + } catch (MalformedTemplateNameException e) { + // If we don't have to emulate backward compatible behavior, then just rethrow it: + if (templateNameFormat != TemplateNameFormat.DEFAULT_2_3_0 + || config.getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_4_0) { + throw e; + } + return new GetTemplateResult(null, e); + } + + if (templateLoader == null) { + return new GetTemplateResult(name, "The TemplateLoader (and TemplateLoader2) was null."); + } + + Template template = getTemplateInternal(name, locale, customLookupCondition, encoding, parseAsFTL); + return template != null ? new GetTemplateResult(template) : new GetTemplateResult(name, (String) null); + } + + 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 debugPrefix = debug + ? getDebugPrefix("getTemplate", name, locale, customLookupCondition, encoding, parseAsFTL) + : null; + final CachedResultKey cacheKey = new CachedResultKey(name, locale, customLookupCondition, encoding, parseAsFTL); + + CachedResult oldCachedResult; + if (isCacheStorageConcurrent) { + oldCachedResult = (CachedResult) cacheStorage.get(cacheKey); + } else { + synchronized (cacheStorage) { + oldCachedResult = (CachedResult) cacheStorage.get(cacheKey); + } + } + + final long now = System.currentTimeMillis(); + + boolean rethrownCachedException = false; + boolean suppressFinallyException = false; + TemplateLookupResult newLookupResult = null; + CachedResult newCachedResult = null; + TemplateLoaderSession session = null; + try { + if (oldCachedResult != null) { + // If we're within the refresh delay, return the cached result + if (now - oldCachedResult.lastChecked < templateUpdateDelayMilliseconds) { + if (debug) { + LOG.debug(debugPrefix + "Cached copy not yet stale; using cached."); + } + Object t = oldCachedResult.templateOrException; + // t can be null, indicating a cached negative lookup + if (t instanceof Template || t == null) { + return (Template) t; + } else if (t instanceof RuntimeException) { + rethrowCachedException((RuntimeException) t); + } else if (t instanceof IOException) { + rethrownCachedException = true; + rethrowCachedException((IOException) t); + } + throw new BugException("Unhandled class for t: " + t.getClass().getName()); + } + // The freshness of the cache result must be checked. + + // Clone, as the instance in the cache store must not be modified to ensure proper concurrent behavior. + newCachedResult = oldCachedResult.clone(); + newCachedResult.lastChecked = now; + + session = templateLoader.createSession(); + if (debug && session != null) { + LOG.debug(debugPrefix + "Session created."); + } + + // Find the template source, load it if it doesn't correspond to the cached result. + newLookupResult = lookupAndLoadTemplateIfChanged( + name, locale, customLookupCondition, oldCachedResult.source, oldCachedResult.version, session); + + // Template source was removed (TemplateLoader2ResultStatus.NOT_FOUND, or no TemplateLoader2Result) + if (!newLookupResult.isPositive()) { + if (debug) { + LOG.debug(debugPrefix + "No source found."); + } + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null); + return null; + } + + final TemplateLoadingResult newTemplateLoaderResult = newLookupResult.getTemplateLoaderResult(); + if (newTemplateLoaderResult.getStatus() == TemplateLoadingResultStatus.NOT_MODIFIED) { + // Return the cached version. + if (debug) { + LOG.debug(debugPrefix + ": Using cached template " + + "(source: " + newTemplateLoaderResult.getSource() + ")" + + " as it hasn't been changed on the backing store."); + } + 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 { // if there was no cached result + if (debug) { + LOG.debug(debugPrefix + "No cached result was found; will try to load template."); + } + + newCachedResult = new CachedResult(); + newCachedResult.lastChecked = now; + + session = templateLoader.createSession(); + if (debug && session != null) { + LOG.debug(debugPrefix + "Session created."); + } + + newLookupResult = lookupAndLoadTemplateIfChanged( + name, locale, customLookupCondition, null, null, session); + + if (!newLookupResult.isPositive()) { + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, null); + return null; + } + } + // We have newCachedResult and newLookupResult initialized at this point. + + TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult(); + newCachedResult.source = templateLoaderResult.getSource(); + + // If we get here, then we need to (re)load the template + if (debug) { + LOG.debug(debugPrefix + "Reading template content (source: " + + StringUtil.jQuoteNoXSS(newCachedResult.source) + ")"); + } + + Template template = loadTemplate( + templateLoaderResult, + name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition, + encoding, parseAsFTL); + 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 (newCachedResult != null) { + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e); + } + suppressFinallyException = true; + throw e; + } catch (IOException e) { + // Rethrown cached exceptions are wrapped into IOException-s, so we only need this condition here. + if (!rethrownCachedException) { + setToNegativeAndPutIntoCache(cacheKey, newCachedResult, e); + } + suppressFinallyException = true; + throw e; + } finally { + try { + // Close streams first: + + if (newLookupResult != null && newLookupResult.isPositive()) { + TemplateLoadingResult templateLoaderResult = newLookupResult.getTemplateLoaderResult(); + Reader reader = templateLoaderResult.getReader(); + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { // [FM3] Exception e + if (suppressFinallyException) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close template content Reader for: " + name, e); + } + } else { + suppressFinallyException = true; + throw e; + } + } + } else if (templateLoaderResult.getInputStream() != null) { + try { + templateLoaderResult.getInputStream().close(); + } catch (IOException e) { // [FM3] Exception e + if (suppressFinallyException) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close template content InputStream for: " + name, e); + } + } else { + suppressFinallyException = true; + throw e; + } + } + } + } + } finally { + // Then close streams: + + if (session != null && !session.isClosed()) { + try { + session.close(); + if (debug) { + LOG.debug(debugPrefix + "Session closed."); + } + } catch (IOException e) { // [FM3] Exception e + if (suppressFinallyException) { + if (LOG.isWarnEnabled()) { + LOG.warn("Failed to close template loader session for" + name, e); + } + } else { + suppressFinallyException = true; + throw e; + } + } + } + } + } + } + + + + private static final Method INIT_CAUSE = getInitCauseMethod(); + + private static final Method getInitCauseMethod() { + try { + return Throwable.class.getMethod("initCause", new Class[] { Throwable.class }); + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Creates an {@link IOException} that has a cause exception. + */ + // [Java 6] Remove + private IOException newIOException(String message, Throwable cause) { + if (cause == null) { + return new IOException(message); + } + + IOException ioe; + if (INIT_CAUSE != null) { + ioe = new IOException(message); + try { + INIT_CAUSE.invoke(ioe, cause); + } catch (RuntimeException ex) { + throw ex; + } catch (Exception ex) { + throw new UndeclaredThrowableException(ex); + } + } else { + ioe = new IOException(message + "\nCaused by: " + cause.getClass().getName() + + ": " + cause.getMessage()); + } + return ioe; + } + + private void rethrowCachedException(Throwable e) throws IOException { + throw newIOException("There was an error loading the " + + "template on an earlier attempt; see cause exception.", e); + } + + private void setToNegativeAndPutIntoCache(CachedResultKey cacheKey, CachedResult cachedResult, Exception e) { + cachedResult.templateOrException = e; + cachedResult.source = null; + cachedResult.version = null; + putIntoCache(cacheKey, cachedResult); + } + + private void putIntoCache(CachedResultKey tk, CachedResult cachedTemplate) { + if (isCacheStorageConcurrent) { + cacheStorage.put(tk, cachedTemplate); + } else { + synchronized (cacheStorage) { + cacheStorage.put(tk, cachedTemplate); + } + } + } + + @SuppressWarnings("deprecation") + private Template loadTemplate( + TemplateLoadingResult templateLoaderResult, + final String name, final String sourceName, Locale locale, final Object customLookupCondition, + String initialEncoding, final boolean parseAsFTL) throws IOException { + TemplateConfiguration tc; + { + TemplateConfiguration cfgTC; + try { + cfgTC = templateConfigurations != null + ? templateConfigurations.get(sourceName, templateLoaderResult.getSource()) : null; + } catch (TemplateConfigurationFactoryException e) { + throw newIOException("Error while getting TemplateConfiguration; see cause exception.", e); + } + TemplateConfiguration 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()) { + initialEncoding = tc.getEncoding(); + } + if (tc.isLocaleSet()) { + locale = tc.getLocale(); + } + } + + Template template; + { + 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); // Mark is released after the 1st FTL tag + templateSpecifiedEncodingHandler = new MarkReleaserTemplateSpecifiedEncodingHandler(inputStream); + } else { + templateSpecifiedEncodingHandler = null; + } + // Regarding buffering worries: On the Reader side we should only read in chunks (like through a + // BufferedReader), so there shouldn't be a problem if the InputStream is not buffered. (Also, at least + // on Oracle JDK and OpenJDK 7 the InputStreamReader itself has an internal ~8K buffer.) + reader = new InputStreamReader(inputStream, initialEncoding); + } else { + throw new IllegalStateException("For a(n) " + templateLoaderResult.getClass().getName() + + ", both getReader() and getInputStream() has returned null."); + } + + try { + 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); + } + template = Template.getPlainTextTemplate(name, sourceName, sb.toString(), config); + template.setEncoding(initialEncoding); + } + } finally { + reader.close(); + } + } + + if (tc != null) { + tc.apply(template); + } + + template.setLocale(locale); + template.setCustomLookupCondition(customLookupCondition); + return template; + } + + /** + * Gets the delay in milliseconds between checking for newer versions of a + * template source. + * @return the current value of the delay + */ + public long getTemplateUpdateDelayMilliseconds() { + // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not. + synchronized (this) { + return templateUpdateDelayMilliseconds; + } + } + + /** + * Sets the delay in milliseconds between checking for newer versions of a + * template sources. + * @param templateUpdateDelayMilliseconds the new value of the delay + */ + public void setTemplateUpdateDelayMilliseconds(long templateUpdateDelayMilliseconds) { + // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not. + synchronized (this) { + this.templateUpdateDelayMilliseconds = templateUpdateDelayMilliseconds; + } + } + + /** + * Returns if localized template lookup is enabled or not. + */ + public boolean getLocalizedLookup() { + // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not. + synchronized (this) { + return localizedLookup; + } + } + + /** + * Setis if localized template lookup is enabled or not. + */ + public void setLocalizedLookup(boolean localizedLookup) { + // synchronized was moved here so that we don't advertise that it's thread-safe, as it's not. + synchronized (this) { + if (this.localizedLookup != localizedLookup) { + this.localizedLookup = localizedLookup; + clearTemplateCache(); + } + } + } + + /** + * Removes all entries from the cache, forcing reloading of templates on subsequent + * {@link #getTemplate(String, Locale, Object, String, boolean)} calls. + * + * @param resetTemplateLoader + * Whether to call {@link TemplateLoader#resetState()}. on the template loader. + */ + public void clearTemplateCache(boolean resetTemplateLoader) { + synchronized (cacheStorage) { + cacheStorage.clear(); + if (templateLoader != null && resetTemplateLoader) { + templateLoader.resetState(); + } + } + } + + /** + * Same as {@link #clearTemplateCache(boolean)} with {@code true} {@code resetTemplateLoader} argument. + */ + @Override + public void clearTemplateCache() { + synchronized (cacheStorage) { + cacheStorage.clear(); + if (templateLoader != null) { + templateLoader.resetState(); + } + } + } + + /** + * Same as {@link #removeTemplateFromCache(String, Locale, Object, String, boolean)} with {@code null} + * {@code customLookupCondition}. + */ + @Override + public void removeTemplateFromCache( + String name, Locale locale, String encoding, boolean parse) throws IOException { + removeTemplateFromCache(name, locale, null, encoding, parse); + } + + /** + * Removes an entry from the cache, hence forcing the re-loading of it when it's next time requested. (It doesn't + * delete the template file itself.) This is to give the application finer control over cache updating than + * {@link #setTemplateUpdateDelayMilliseconds(long)} alone does. + * + * For the meaning of the parameters, see + * {@link Configuration#getTemplate(String, Locale, Object, String, boolean, boolean)} + */ + public void removeTemplateFromCache( + String name, Locale locale, Object customLookupCondition, String encoding, boolean parse) + throws IOException { + if (name == null) { + throw new IllegalArgumentException("Argument \"name\" can't be null"); + } + if (locale == null) { + throw new IllegalArgumentException("Argument \"locale\" can't be null"); + } + if (encoding == null) { + throw new IllegalArgumentException("Argument \"encoding\" can't be null"); + } + name = templateNameFormat.normalizeRootBasedName(name); + if (name != null && templateLoader != null) { + boolean debug = LOG.isDebugEnabled(); + String debugPrefix = debug + ? getDebugPrefix("removeTemplate", name, locale, customLookupCondition, encoding, parse) + : null; + CachedResultKey tk = new CachedResultKey(name, locale, customLookupCondition, encoding, parse); + + if (isCacheStorageConcurrent) { + cacheStorage.remove(tk); + } else { + synchronized (cacheStorage) { + cacheStorage.remove(tk); + } + } + if (debug) { + LOG.debug(debugPrefix + "Template was removed from the cache, if it was there"); + } + } + } + + private String getDebugPrefix(String operation, String name, Locale locale, Object customLookupCondition, String encoding, + boolean parse) { + return operation + " " + StringUtil.jQuoteNoXSS(name) + "(" + + StringUtil.jQuoteNoXSS(locale) + + (customLookupCondition != null ? ", cond=" + StringUtil.jQuoteNoXSS(customLookupCondition) : "") + + ", " + encoding + + (parse ? ", parsed)" : ", unparsed]") + + ": "; + } + + /** + * 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 DefaultTemplateResolverTemplateLookupContext( + name, locale, customLookupCondition, + cachedResultSource, cachedResultVersion, + session)); + if (lookupResult == null) { + throw new NullPointerException("Lookup result shouldn't be null"); + } + return lookupResult; + } + + private String concatPath(List<String> pathSteps, int from, int to) { + StringBuilder buf = new StringBuilder((to - from) * 16); + for (int i = from; i < to; ++i) { + buf.append(pathSteps.get(i)); + if (i < pathSteps.size() - 1) { + buf.append('/'); + } + } + return buf.toString(); + } + + // Replace with Objects.equals in Java 7 + private static boolean nullSafeEquals(Object o1, Object o2) { + if (o1 == o2) return true; + if (o1 == null || o2 == null) return false; + return o1.equals(o2); + } + + /** + * Used as cache key to look up a {@link CachedResult}. + */ + @SuppressWarnings("serial") + private static final class CachedResultKey implements Serializable { + private final String name; + private final Locale locale; + private final Object customLookupCondition; + private final String encoding; + private final boolean parse; + + CachedResultKey(String name, Locale locale, Object customLookupCondition, String encoding, boolean parse) { + this.name = name; + this.locale = locale; + this.customLookupCondition = customLookupCondition; + this.encoding = encoding; + this.parse = parse; + } + + @Override + public boolean equals(Object o) { + if (o instanceof CachedResultKey) { + CachedResultKey tk = (CachedResultKey) o; + return + parse == tk.parse && + name.equals(tk.name) && + locale.equals(tk.locale) && + nullSafeEquals(customLookupCondition, tk.customLookupCondition) && + encoding.equals(tk.encoding); + } + return false; + } + + @Override + public int hashCode() { + return + name.hashCode() ^ + locale.hashCode() ^ + encoding.hashCode() ^ + (customLookupCondition != null ? customLookupCondition.hashCode() : 0) ^ + Boolean.valueOf(!parse).hashCode(); + } + } + + /** + * 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 CachedResult implements Cloneable, Serializable { + private static final long serialVersionUID = 1L; + + Object templateOrException; + TemplateLoadingSource source; + Serializable version; + long lastChecked; + + @Override + public CachedResult clone() { + try { + return (CachedResult) super.clone(); + } catch (CloneNotSupportedException e) { + throw new UndeclaredThrowableException(e); + } + } + } + + private class DefaultTemplateResolverTemplateLookupContext extends TemplateLookupContext { + + private final TemplateLoaderSession session; + + DefaultTemplateResolverTemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition, + TemplateLoadingSource cachedResultSource, Serializable cachedResultVersion, + TemplateLoaderSession session) { + super(templateName, localizedLookup ? templateLocale : null, customLookupCondition, + cachedResultSource, cachedResultVersion); + this.session = session; + } + + @Override + public 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 (path.startsWith("/")) { + throw new IllegalArgumentException("Non-normalized name, starts with \"/\": " + path); + } + + int asterisk = path.indexOf(ASTERISK); + // Shortcut in case there is no acquisition + if (asterisk == -1) { + return 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 + public TemplateLookupResult lookupWithLocalizedThenAcquisitionStrategy(final String templateName, + final Locale templateLocale) throws IOException { + + if (templateLocale == null) { + return lookupWithAcquisitionStrategy(templateName); + } + + int lastDot = templateName.lastIndexOf('.'); + String prefix = lastDot == -1 ? templateName : templateName.substring(0, lastDot); + String suffix = lastDot == -1 ? "" : templateName.substring(lastDot); + String localeName = LOCALE_PART_SEPARATOR + templateLocale.toString(); + StringBuilder buf = new StringBuilder(templateName.length() + localeName.length()); + buf.append(prefix); + tryLocaleNameVariations: while (true) { + buf.setLength(prefix.length()); + String path = buf.append(localeName).append(suffix).toString(); + TemplateLookupResult lookupResult = lookupWithAcquisitionStrategy(path); + if (lookupResult.isPositive()) { + return lookupResult; + } + + int lastUnderscore = localeName.lastIndexOf('_'); + if (lastUnderscore == -1) { + break tryLocaleNameVariations; + } + localeName = localeName.substring(0, lastUnderscore); + } + return createNegativeLookupResult(); + } + + } + + @Override + public String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException { + return templateNameFormat.toRootBasedName(baseName, targetName); + } + + @Override + public String normalizeRootBasedName(String name) throws MalformedTemplateNameException { + return templateNameFormat.normalizeRootBasedName(name); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/94d39312/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java b/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java new file mode 100644 index 0000000..845c2a2 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/templateresolver/GetTemplateResult.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.freemarker.core.templateresolver; + +import java.util.Locale; + +import org.apache.freemarker.core.Template; + +/** + * Used for the return value of {@link TemplateResolver#getTemplate(String, Locale, Object, String, boolean)}. + * + * @since 3.0.0 + */ +//TODO DRAFT only [FM3] +public final class GetTemplateResult { + + private final Template template; + private final String missingTemplateNormalizedName; + private final String missingTemplateReason; + private final Exception missingTemplateCauseException; + + public GetTemplateResult(Template template) { + this.template = template; + this.missingTemplateNormalizedName = null; + this.missingTemplateReason = null; + this.missingTemplateCauseException = null; + } + + public GetTemplateResult(String normalizedName, Exception missingTemplateCauseException) { + this.template = null; + this.missingTemplateNormalizedName = normalizedName; + this.missingTemplateReason = null; + this.missingTemplateCauseException = missingTemplateCauseException; + } + + public GetTemplateResult(String normalizedName, String missingTemplateReason) { + this.template = null; + this.missingTemplateNormalizedName = normalizedName; + this.missingTemplateReason = missingTemplateReason; + this.missingTemplateCauseException = null; + } + + /** + * The {@link Template} if it wasn't missing, otherwise {@code null}. + */ + public Template getTemplate() { + return template; + } + + /** + * When the template was missing, this <em>possibly</em> contains the explanation, or {@code null}. If the + * template wasn't missing (i.e., when {@link #getTemplate()} return non-{@code null}) this is always + * {@code null}. + */ + public String getMissingTemplateReason() { + return missingTemplateReason != null + ? missingTemplateReason + : (missingTemplateCauseException != null + ? missingTemplateCauseException.getMessage() + : null); + } + + /** + * When the template was missing, this <em>possibly</em> contains its normalized name. If the template wasn't + * missing (i.e., when {@link #getTemplate()} return non-{@code null}) this is always {@code null}. When the + * template is missing, it will be {@code null} for example if the normalization itself was unsuccessful. + */ + public String getMissingTemplateNormalizedName() { + return missingTemplateNormalizedName; + } + +} \ No newline at end of file
