| /* |
| * 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( |
| templateLoader, 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 TemplateLoader templateLoader, 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); |
| } |
| } |
| 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, and the last-checked and last-modified timestamps). |
| * It is used as the value in the cached templates map. Note: this class |
| * is Serializable to allow custom 3rd party CacheStorage implementations |
| * to serialize/replicate them (see tracker issue #1926150); FreeMarker |
| * code itself doesn't rely on its serializability. |
| */ |
| 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; |
| } |
| |
| } |
| |
| } |