blob: a35750038196b1e0a64329752c7d51530db98c95 [file] [log] [blame]
/*
* 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.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;
import java.util.StringTokenizer;
import freemarker.cache.MultiTemplateLoader.MultiSource;
import freemarker.core.BugException;
import freemarker.core.Environment;
import freemarker.core.TemplateConfiguration;
import freemarker.log.Logger;
import freemarker.template.Configuration;
import freemarker.template.MalformedTemplateNameException;
import freemarker.template.Template;
import freemarker.template.TemplateNotFoundException;
import freemarker.template._TemplateAPI;
import freemarker.template.utility.NullArgumentException;
import freemarker.template.utility.StringUtil;
import freemarker.template.utility.UndeclaredThrowableException;
/**
* Performs caching and on-demand loading of the templates.
* The actual template "file" loading is delegated to a {@link TemplateLoader} that you can specify in the constructor.
* Some aspects of caching is delegated to a {@link CacheStorage} that you can also specify in the constructor.
*
* <p>Typically you don't instantiate or otherwise use this class directly. The {@link Configuration} embeds an
* instance of this class, that you access indirectly through {@link Configuration#getTemplate(String)} and other
* {@link Configuration} API-s. Then {@link TemplateLoader} and {@link CacheStorage} can be set with
* {@link Configuration#setTemplateLoader(TemplateLoader)} and
* {@link Configuration#setCacheStorage(CacheStorage)}.
*/
public class TemplateCache {
/**
* 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 = Logger.getLogger("freemarker.cache");
/** Maybe {@code null}. */
private final TemplateLoader templateLoader;
/** Here we keep our cached templates */
private final CacheStorage storage;
private final TemplateLookupStrategy templateLookupStrategy;
private final TemplateNameFormat templateNameFormat;
private final TemplateConfigurationFactory templateConfigurations;
private final boolean isStorageConcurrent;
/** {@link Configuration#setTemplateUpdateDelayMilliseconds(long)} */
private long updateDelay = DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS;
/** {@link Configuration#setLocalizedLookup(boolean)} */
private boolean localizedLookup = true;
private Configuration config;
/**
* Returns a template cache that will first try to load a template from
* the file system relative to the current user directory (i.e. the value
* of the system property <code>user.dir</code>), then from the classpath.
*
* @deprecated Use {@link #TemplateCache(TemplateLoader)} instead. The default loader is useless in most
* applications, also it can mean a security risk.
*/
@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.
*
* @since 2.3.21
*/
public TemplateCache(TemplateLoader templateLoader, Configuration config) {
this(templateLoader, _TemplateAPI.createDefaultCacheStorage(Configuration.VERSION_2_3_0), config);
}
/**
* Same as
* {@link #TemplateCache(TemplateLoader, CacheStorage, TemplateLookupStrategy, TemplateNameFormat, Configuration)}
* with {@link TemplateLookupStrategy#DEFAULT_2_3_0} and {@link TemplateNameFormat#DEFAULT_2_3_0}.
*
* @since 2.3.21
*/
public TemplateCache(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 TemplateCache#TemplateCache(TemplateLoader, CacheStorage, TemplateLookupStrategy, TemplateNameFormat,
* TemplateConfigurationFactory, Configuration)} with {@code null} for {@code templateConfigurations}-s.
*
* @since 2.3.22
*/
public TemplateCache(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't be {@code null}.
* @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).
* @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.
*
* @since 2.3.24
*/
public TemplateCache(TemplateLoader templateLoader, CacheStorage cacheStorage,
TemplateLookupStrategy templateLookupStrategy, TemplateNameFormat templateNameFormat,
TemplateConfigurationFactory templateConfigurations,
Configuration config) {
this.templateLoader = templateLoader;
NullArgumentException.check("cacheStorage", cacheStorage);
this.storage = cacheStorage;
isStorageConcurrent = 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;
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.
*/
@Deprecated
public void setConfiguration(Configuration config) {
this.config = config;
clear();
}
public TemplateLoader getTemplateLoader() {
return templateLoader;
}
public CacheStorage getCacheStorage() {
return storage;
}
/**
* @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 MaybeMissingTemplate} object that contains the {@link Template}, or a
* {@link MaybeMissingTemplate} 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 MaybeMissingTemplate}. 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 MaybeMissingTemplate} will be returned, similarly as if the template were missing
* (the {@link MaybeMissingTemplate#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
*/
public MaybeMissingTemplate 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.normalizeAbsoluteName(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 MaybeMissingTemplate(null, e);
}
if (templateLoader == null) {
return new MaybeMissingTemplate(name, "The TemplateLoader 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}
* {@code customLookupCondition}.
*
* @return {@link MaybeMissingTemplate#getTemplate()} of the
* {@link #getTemplate(String, Locale, Object, String, boolean)} return value.
*
* @deprecated Use {@link #getTemplate(String, Locale, Object, String, boolean)}, which can return more detailed
* result when the template is missing.
*/
@Deprecated
public Template getTemplate(String name, Locale locale, String encoding, boolean parseAsFTL)
throws IOException {
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)
: null;
final TemplateKey tk = new TemplateKey(name, locale, customLookupCondition, encoding, parseAsFTL);
CachedTemplate cachedTemplate;
if (isStorageConcurrent) {
cachedTemplate = (CachedTemplate) storage.get(tk);
} else {
synchronized (storage) {
cachedTemplate = (CachedTemplate) storage.get(tk);
}
}
final long now = System.currentTimeMillis();
long lastModified = -1L;
boolean rethrown = false;
TemplateLookupResult newLookupResult = null;
try {
if (cachedTemplate != null) {
// If we're within the refresh delay, return the cached copy
if (now - cachedTemplate.lastChecked < updateDelay) {
if (debug) {
LOG.debug(debugName + " cached copy not yet stale; using cached.");
}
// Can be null, indicating a cached negative lookup
Object t = cachedTemplate.templateOrException;
if (t instanceof Template || t == null) {
return (Template) t;
} else if (t instanceof RuntimeException) {
throwLoadFailedException((RuntimeException) t);
} else if (t instanceof IOException) {
rethrown = true;
throwLoadFailedException((IOException) t);
}
throw new BugException("t is " + t.getClass().getName());
}
// Clone as the instance bound to the map should be treated as
// immutable to ensure proper concurrent semantics
cachedTemplate = cachedTemplate.cloneCachedTemplate();
// Update the last-checked flag
cachedTemplate.lastChecked = now;
// Find the template source
newLookupResult = lookupTemplate(name, locale, customLookupCondition);
// Template source was removed
if (!newLookupResult.isPositive()) {
if (debug) {
LOG.debug(debugName + " no source found.");
}
storeNegativeLookup(tk, cachedTemplate, null);
return null;
}
// If the source didn't change and its last modified date
// also didn't change, return the cached version.
final Object newLookupResultSource = newLookupResult.getTemplateSource();
lastModified = templateLoader.getLastModified(newLookupResultSource);
boolean lastModifiedNotChanged = lastModified == cachedTemplate.lastModified;
boolean sourceEquals = newLookupResultSource.equals(cachedTemplate.source);
if (lastModifiedNotChanged && sourceEquals) {
if (debug) {
LOG.debug(debugName + ": using cached since " + newLookupResultSource + " hasn't changed.");
}
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);
}
}
} else {
if (debug) {
LOG.debug("Couldn't find template in cache for " + debugName + "; will try to load it.");
}
// 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;
newLookupResult = lookupTemplate(name, locale, customLookupCondition);
if (!newLookupResult.isPositive()) {
storeNegativeLookup(tk, cachedTemplate, null);
return null;
}
cachedTemplate.lastModified = lastModified = Long.MIN_VALUE;
}
Object source = newLookupResult.getTemplateSource();
cachedTemplate.source = source;
// If we get here, then we need to (re)load the template
if (debug) {
LOG.debug("Loading template for " + debugName + " from " + StringUtil.jQuoteNoXSS(source));
}
lastModified = lastModified == Long.MIN_VALUE ? templateLoader.getLastModified(source) : lastModified;
Template template = loadTemplate(
source,
name, newLookupResult.getTemplateSourceName(), locale, customLookupCondition,
encoding, parseAsFTL);
cachedTemplate.templateOrException = template;
cachedTemplate.lastModified = lastModified;
storeCached(tk, cachedTemplate);
return template;
} catch (RuntimeException e) {
if (cachedTemplate != null) {
storeNegativeLookup(tk, cachedTemplate, e);
}
throw e;
} catch (IOException e) {
if (!rethrown) {
storeNegativeLookup(tk, cachedTemplate, e);
}
throw e;
} finally {
if (newLookupResult != null && newLookupResult.isPositive()) {
templateLoader.closeTemplateSource(newLookupResult.getTemplateSource());
}
}
}
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 throwLoadFailedException(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 storeCached(TemplateKey tk, CachedTemplate cachedTemplate) {
if (isStorageConcurrent) {
storage.put(tk, cachedTemplate);
} else {
synchronized (storage) {
storage.put(tk, cachedTemplate);
}
}
}
private Template loadTemplate(
final Object source,
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);
}
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;
{
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();
}
}
} 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;
}
}
} finally {
reader.close();
}
template = Template.getPlainTextTemplate(name, sourceName, sw.toString(), config);
template.setEncoding(initialEncoding);
}
}
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 getDelay() {
// synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
synchronized (this) {
return updateDelay;
}
}
/**
* Sets the delay in milliseconds between checking for newer versions of a
* template sources.
* @param delay the new value of the delay
*/
public void setDelay(long delay) {
// synchronized was moved here so that we don't advertise that it's thread-safe, as it's not.
synchronized (this) {
this.updateDelay = delay;
}
}
/**
* 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;
clear();
}
}
}
/**
* Removes all entries from the cache, forcing reloading of templates
* on subsequent {@link #getTemplate(String, Locale, String, boolean)}
* calls. If the configured template loader is
* {@link StatefulTemplateLoader stateful}, then its
* {@link StatefulTemplateLoader#resetState()} method is invoked as well.
*/
public void clear() {
synchronized (storage) {
storage.clear();
if (templateLoader instanceof StatefulTemplateLoader) {
((StatefulTemplateLoader) templateLoader).resetState();
}
}
}
/**
* Same as {@link #removeTemplate(String, Locale, Object, String, boolean)} with {@code null}
* {@code customLookupCondition}.
*/
public void removeTemplate(
String name, Locale locale, String encoding, boolean parse) throws IOException {
removeTemplate(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 #setDelay(long)} alone does.
*
* For the meaning of the parameters, see
* {@link Configuration#getTemplate(String, Locale, Object, String, boolean, boolean)}
*/
public void removeTemplate(
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.normalizeAbsoluteName(name);
if (name != null && templateLoader != null) {
boolean debug = LOG.isDebugEnabled();
String debugName = debug
? buildDebugName(name, locale, customLookupCondition, encoding, parse)
: null;
TemplateKey tk = new TemplateKey(name, locale, customLookupCondition, encoding, parse);
if (isStorageConcurrent) {
storage.remove(tk);
} else {
synchronized (storage) {
storage.remove(tk);
}
}
if (debug) {
LOG.debug(debugName + " was removed from the cache, if it was there");
}
}
}
private String buildDebugName(String name, Locale locale, Object customLookupCondition, String encoding,
boolean parse) {
return StringUtil.jQuoteNoXSS(name) + "("
+ StringUtil.jQuoteNoXSS(locale)
+ (customLookupCondition != null ? ", cond=" + StringUtil.jQuoteNoXSS(customLookupCondition) : "")
+ ", " + encoding
+ (parse ? ", parsed)" : ", unparsed]");
}
/**
* @deprecated Use {@link Environment#toFullTemplateName(String, String)} instead, as that can throw
* {@link MalformedTemplateNameException}, and is on a more logical place anyway.
*
* @throws IllegalArgumentException
* If the {@code baseName} or {@code targetName} is malformed according the {@link TemplateNameFormat}
* in use.
*/
@Deprecated
public static String getFullTemplatePath(Environment env, String baseName, String targetName) {
try {
return env.toFullTemplateName(baseName, targetName);
} catch (MalformedTemplateNameException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
private TemplateLookupResult lookupTemplate(String name, Locale locale, Object customLookupCondition)
throws IOException {
final TemplateLookupResult lookupResult = templateLookupStrategy.lookup(
new TemplateCacheTemplateLookupContext(name, locale, customLookupCondition));
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) {
StringBuilder buf = new StringBuilder((to - from) * 16);
for (int i = from; i < to; ++i) {
buf.append(path.get(i)).append('/');
}
return buf.toString();
}
/**
* This class holds a (name, locale) pair and is used as the key in
* the cached templates map.
*/
private static final class TemplateKey {
private final String name;
private final Locale locale;
private final Object customLookupCondition;
private final String encoding;
private final boolean parse;
TemplateKey(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 TemplateKey) {
TemplateKey tk = (TemplateKey) o;
return
parse == tk.parse &&
name.equals(tk.name) &&
locale.equals(tk.locale) &&
nullSafeEquals(customLookupCondition, tk.customLookupCondition) &&
encoding.equals(tk.encoding);
}
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
name.hashCode() ^
locale.hashCode() ^
encoding.hashCode() ^
(customLookupCondition != null ? customLookupCondition.hashCode() : 0) ^
Boolean.valueOf(!parse).hashCode();
}
}
/**
* This class holds the cached template and associated information
* (the source object (already closed), and the last-checked and last-modified time stamps).
* It is used as the value in the cached templates map. Note: this class
* is Serializable to allow custom 3rd party CacheStorage implementations
* to serialize/replicate them (see tracker issue #1926150); FreeMarker
* code itself doesn't rely on its serializability.
*/
private static final class CachedTemplate implements Cloneable, Serializable {
private static final long serialVersionUID = 1L;
Object templateOrException;
Object source;
long lastChecked;
long lastModified;
public CachedTemplate cloneCachedTemplate() {
try {
return (CachedTemplate) super.clone();
} catch (CloneNotSupportedException e) {
throw new UndeclaredThrowableException(e);
}
}
}
private class TemplateCacheTemplateLookupContext extends TemplateLookupContext {
TemplateCacheTemplateLookupContext(String templateName, Locale templateLocale, Object customLookupCondition) {
super(templateName, localizedLookup ? templateLocale : null, customLookupCondition);
}
@Override
public TemplateLookupResult lookupWithAcquisitionStrategy(String name) 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);
}
return TemplateCache.this.lookupTemplateWithAcquisitionStrategy(name);
}
@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();
}
}
/**
* Used for the return value of {@link TemplateCache#getTemplate(String, Locale, Object, String, boolean)}.
*
* @since 2.3.22
*/
public final static class MaybeMissingTemplate {
private final Template template;
private final String missingTemplateNormalizedName;
private final String missingTemplateReason;
private final MalformedTemplateNameException missingTemplateCauseException;
private MaybeMissingTemplate(Template template) {
this.template = template;
this.missingTemplateNormalizedName = null;
this.missingTemplateReason = null;
this.missingTemplateCauseException = null;
}
private MaybeMissingTemplate(String normalizedName, MalformedTemplateNameException missingTemplateCauseException) {
this.template = null;
this.missingTemplateNormalizedName = normalizedName;
this.missingTemplateReason = null;
this.missingTemplateCauseException = missingTemplateCauseException;
}
private MaybeMissingTemplate(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.getMalformednessDescription()
: 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;
}
}
}