org.thymeleaf.TemplateEngine Maven / Gradle / Ivy
Show all versions of thymeleaf Show documentation
/*
* =============================================================================
*
* Copyright (c) 2011, The THYMELEAF team (http://www.thymeleaf.org)
*
* Licensed 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.thymeleaf;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.xml.transform.TransformerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.context.IContext;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.doctype.DocTypeIdentifier;
import org.thymeleaf.doctype.translation.IDocTypeTranslation;
import org.thymeleaf.exceptions.OutputCreationException;
import org.thymeleaf.exceptions.TemplateEngineException;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.messageresolver.IMessageResolver;
import org.thymeleaf.messageresolver.StandardMessageResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.thymeleaf.templateresolver.TemplateResolution;
import org.thymeleaf.util.Validate;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
/**
*
* Main class for the execution of templates.
*
*
* In order to execute Thymeleaf templates, an instance of this class (or one of
* its subclasses) must be created.
*
*
* Creating an instance of TemplateEngine
*
* An instance of this class can be created at any time by calling its constructor:
*
*
* final TemplateEngine templateEngine = new TemplateEngine();
*
*
* Creation and configuration of TemplateEngine instances is expensive, so it is
* recommended to create only one instance of this class (or at least one instance per
* dialect/configuration) and use it to process multiple templates.
*
*
* Configuring the TemplateEngine
*
* Once created, an instance of TemplateEngine has to be configured by
* setting the following required parameters:
*
*
* - One or more Template Resolvers (instances of {@link ITemplateResolver}), in
* charge of reading or obtaining the templates so that the engine is able to process them. If
* only one template resolver is set (the most common case), the {@link #setTemplateResolver(ITemplateResolver)}
* method can be used for this. If more resolvers are to be set, both the
* {@link #setTemplateResolvers(Set)} and {@link #addTemplateResolver(ITemplateResolver)} methods
* can be used.
*
*
* Also, the following parameters can be optionally set:
*
*
* - One or more Dialects (instances of {@link IDialect}), defining the way in which templates
* will be processed: attributes, tags, value and expression processors, etc. If no
* dialect is explicitly set, a unique instance of {@link org.thymeleaf.standard.StandardDialect}
* (the Standard Dialect) will be used.
*
* - Dialects define a default prefix, which will be used for them if not otherwise specified.
* - When setting/adding dialects, a non-default prefix can be specified for each of them.
* - Several dialects can use the same prefix, effectively acting as an aggregate dialect.
* - All specified dialects will be validated to ensure no conflicts with DOCTYPE translations or resolution entries
* exist. Dialects defining a DOCTYPE translation or resolution entry equal to another one in a
* different dialect are not considered to be in conflict.
* - Dialect leniency will be computed per-prefix, so that a prefix will be considered to be lenient
* if at least one of the dialects configured for it is lenient.
* - Note that defining a non-default prefix for a dialect might affect its validation features
* if this dialect includes DTD files for such purpose (e.g. the Standard Dialect).
*
*
* - One or more Message Resolvers (instances of {@link IMessageResolver}), in
* charge of resolving externalized messages. If no message resolver is explicitly set, the default
* setting specified by {@link #setDefaultMessageResolvers(Set)} will be applied (this
* default setting defaults itself to a single instance of {@link StandardMessageResolver}).
* If only one message resolver is set, the {@link #setMessageResolver(IMessageResolver)} method
* can be used for this. If more resolvers are to be set, both the
* {@link #setMessageResolvers(Set)} and {@link #addMessageResolver(IMessageResolver)} methods
* can be used.
* - The size of the template caché, set by means of the
* {@link #setParsedTemplateCacheSize(int)} method. This caché will be used to store the
* parsed DOM trees of templates in order to reduce the amount of input/output operations
* needed. It uses the LRU (Least Recently Used) algorithm. If not set, this parameter
* will default to {@link Configuration#DEFAULT_PARSED_TEMPLATE_CACHE_SIZE}.
*
*
* Template Execution
* 1. Creating a context
*
* All template executions require a context. A context is an object that
* implements the {@link IContext} interface, and that contains at least the following
* data:
*
*
* - The locale to be used for message externalization (internationalization).
* - The context variables. A map of variables that will be available for
* use from expressions in the executed template.
*
*
* Two {@link IContext} implementations are provided out-of-the-box:
*
*
* - {@link org.thymeleaf.context.Context}, a standard implementation containing only
* the required data.
* - {@link org.thymeleaf.context.WebContext}, a web-specific implementation
* extending the {@link org.thymeleaf.context.IWebContext} subinterface, offering
* request, session and servletcontext (application) attributes in special variables
* inside the context variables map. Using an implementation of
* {@link org.thymeleaf.context.IWebContext} is required when using Thymeleaf for
* generating HTML/XHTML interfaces in web applications.
*
*
* Creating a {@link org.thymeleaf.context.Context} instance is very simple:
*
*
* final IContext ctx = new Context();
* ctx.setVariable("allItems", items);
*
*
* A {@link org.thymeleaf.context.WebContext} will also need an
* {@link javax.servlet.http.HttpServletRequest} object as a constructor argument:
*
*
* final IContext ctx = new WebContext(request);
* ctx.setVariable("allItems", items);
*
*
* See the documentation for these specific implementations for more details.
*
*
* 2. Template Processing
*
* In order to execute templates, the {@link #process(String, IContext)} method
* will be used:
*
*
* final String result = templateEngine.process("mytemplate", ctx);
*
*
* The "mytemplate" String argument is the template name, and it
* will relate to the physical/logical location of the template itself in a way
* configured at the template resolver/s.
*
*
* @author Daniel Fernández
*
* @since 1.0
*
*/
public class TemplateEngine {
/**
*
* Name of the TIMER logger. This logger will output the time required
* for executing each template processing operation.
*
*
* The value of this constant is org.thymeleaf.TemplateEngine.TIMER. This
* allows you to set a specific configuration and/or appenders for timing info at your logging
* system configuration.
*
*/
public static final String TIMER_LOGGER_NAME = TemplateEngine.class.getName() + ".TIMER";
/**
*
* Flag set into Text nodes that are created during template processing. The presence of
* this flag disallows the execution of text inlining on nodes already
* generated from template code, effectively avoiding code injection.
*
*
* This flag is set as Node user data using the
* ({@link org.w3c.dom.Node#setUserData(String, Object, org.w3c.dom.UserDataHandler)})
* method.
*
*
* This constant should only be used directly when creating a custom dialect or
* custom processors for it.
*
*/
public static final String THYMELEAF_NON_EXECUTABLE_NODE = "THYMELEAF_NON_EXECUTABLE_NODE";
private static final Logger logger = LoggerFactory.getLogger(TemplateEngine.class);
private static final Logger timerLogger = LoggerFactory.getLogger(TIMER_LOGGER_NAME);
private static long processIndex = 0L;
private static ThreadLocal currentProcessIndex = new ThreadLocal();
private static ThreadLocal currentProcessLocale = new ThreadLocal();
private final Configuration configuration;
private TemplateParser templateParser;
private boolean initialized;
/**
*
* Constructor for TemplateEngine objects.
*
*
* This is the only way to create a TemplateEngine instance (which
* should be configured after creation).
*
*/
public TemplateEngine() {
super();
this.configuration = new Configuration();
this.initialized = false;
setDefaultMessageResolvers(Collections.singleton(new StandardMessageResolver()));
}
/**
*
* Checks whether the TemplateEngine has already been initialized
* or not. A TemplateEngine is initialized when the {@link #initialize()}
* method is called the first time a template is processed.
*
*
* @return true if the template engine has already been initialized,
* false if not.
*/
protected final boolean isInitialized() {
return this.initialized;
}
/**
*
* Returns the configuration object. Meant to be used only by subclasses of TemplateEngine.
*
*
* @return the current configuration
*/
protected Configuration getConfiguration() {
return this.configuration;
}
/**
*
* Returns the configured dialect.
*
*
* @deprecated TemplateEngine can now be multi-dialect, so use
* {@link #getDialects()} instead. This method will be removed
* in 1.0.0-beta5.
* @return the {@link IDialect} instance being used.
*/
@Deprecated
public final IDialect getDialect() {
return this.configuration.getDialect();
}
/**
*
* Returns the configured dialects, referenced by their prefixes.
*
*
* @return the {@link IDialect} instances currently configured.
*/
public final Map getDialectsByPrefix() {
return this.configuration.getDialects();
}
/**
*
* Returns the configured dialects.
*
*
* @return the {@link IDialect} instances currently configured.
*/
public final Set getDialects() {
return Collections.unmodifiableSet(
new HashSet(this.configuration.getDialects().values()));
}
/**
*
* Sets a new unique dialect for this template engine.
*
*
* This operation is equivalent to removing all the currently configured dialects and then
* adding this one.
*
*
* This operation can only be executed before processing templates for the first
* time. Once a template is processed, the template engine is considered to be
* initialized, and from then on any attempt to change its configuration
* will result in an exception.
*
*
* @param dialect the new unique {@link IDialect} to be used.
*/
public void setDialect(final IDialect dialect) {
this.configuration.setDialect(dialect);
}
/**
*
* Adds a new dialect for this template engine, using the specified prefix.
*
*
* This dialect will be added to the set of currently configured ones.
*
*
* This operation can only be executed before processing templates for the first
* time. Once a template is processed, the template engine is considered to be
* initialized, and from then on any attempt to change its configuration
* will result in an exception.
*
*
* @param prefix the prefix that will be used for this dialect
* @param dialect the new {@link IDialect} to be added to the existing ones.
*/
public void addDialect(final String prefix, final IDialect dialect) {
this.configuration.addDialect(prefix, dialect);
}
/**
*
* Adds a new dialect for this template engine, using the dialect's specified
* default dialect.
*
*
* This dialect will be added to the set of currently configured ones.
*
*
* This operation can only be executed before processing templates for the first
* time. Once a template is processed, the template engine is considered to be
* initialized, and from then on any attempt to change its configuration
* will result in an exception.
*
*
* @param dialect the new {@link IDialect} to be added to the existing ones.
*/
public void addDialect(final IDialect dialect) {
this.configuration.addDialect(dialect.getPrefix(), dialect);
}
/**
*
* Sets a new set of dialects for this template engine, referenced
* by the prefixes they will be using.
*
*
* This operation can only be executed before processing templates for the first
* time. Once a template is processed, the template engine is considered to be
* initialized, and from then on any attempt to change its configuration
* will result in an exception.
*
*
* @param dialects the new map of {@link IDialect} objects to be used, referenced
* by their prefixes.
*/
public void setDialectsByPrefix(final Map dialects) {
this.configuration.setDialects(dialects);
}
/**
*
* Sets a new set of dialects for this template engine, all of them using
* their default prefixes.
*
*
* This operation can only be executed before processing templates for the first
* time. Once a template is processed, the template engine is considered to be
* initialized, and from then on any attempt to change its configuration
* will result in an exception.
*
*
* @param dialects the new set of {@link IDialect} objects to be used.
*/
public void setDialects(final Set dialects) {
Validate.notNull(dialects, "Dialect set cannot be null");
final Map dialectMap = new LinkedHashMap();
for (final IDialect dialect : dialects) {
dialectMap.put(dialect.getPrefix(), dialect);
}
this.configuration.setDialects(dialectMap);
}
/**
*
* Removes all the currently configured dialects.
*
*
* This operation can only be executed before processing templates for the first
* time. Once a template is processed, the template engine is considered to be
* initialized, and from then on any attempt to change its configuration
* will result in an exception.
*
*/
public void clearDialects() {
this.configuration.clearDialects();
}
/**
*
* Returns the Set of template resolvers currently configured.
*
*
* @return the template resolvers.
*/
public final Set getTemplateResolvers() {
return this.configuration.getTemplateResolvers();
}
/**
*
* Sets the entire set of template resolvers.
*
*
* @param templateResolvers the new template resolvers.
*/
public void setTemplateResolvers(final Set extends ITemplateResolver> templateResolvers) {
this.configuration.setTemplateResolvers(templateResolvers);
}
/**
*
* Adds a new template resolver to the current set.
*
*
* @param templateResolver the new template resolver.
*/
public void addTemplateResolver(final ITemplateResolver templateResolver) {
this.configuration.addTemplateResolver(templateResolver);
}
/**
*
* Sets a single template resolver for this template engine.
*
*
* Calling this method is equivalent to calling {@link #setTemplateResolvers(Set)}
* passing a Set with only one template resolver.
*
*
* @param templateResolver the template resolver to be set.
*/
public void setTemplateResolver(final ITemplateResolver templateResolver) {
this.configuration.setTemplateResolver(templateResolver);
}
/**
*
* Returns the set of Message Resolvers configured for this Template Engine.
*
*
* @return the set of message resolvers.
*/
public final Set getMessageResolvers() {
return this.configuration.getMessageResolvers();
}
/**
*
* Sets the message resolvers to be used by this template engine.
*
*
* @param messageResolvers the Set of template resolvers.
*/
public void setMessageResolvers(final Set extends IMessageResolver> messageResolvers) {
this.configuration.setMessageResolvers(messageResolvers);
}
/**
*
* Adds a message resolver to the set of message resolvers to be used
* by the template engine.
*
*
* @param messageResolver the new message resolver to be added.
*/
public void addMessageResolver(final IMessageResolver messageResolver) {
this.configuration.addMessageResolver(messageResolver);
}
/**
*
* Sets a single messae resolver for this template engine.
*
*
* Calling this method is equivalent to calling {@link #setMessageResolvers(Set)}
* passing a Set with only one message resolver.
*
*
* @param messageResolver the message resolver to be set.
*/
public void setMessageResolver(final IMessageResolver messageResolver) {
this.configuration.setMessageResolver(messageResolver);
}
/**
*
* Sets the default message resolvers. These are used when no message resolvers
* are set via the {@link #setMessageResolver(IMessageResolver)},
* {@link #setMessageResolvers(Set)} or {@link #addMessageResolver(IMessageResolver)}
* methods.
*
*
* This method is useful for creating subclasses of TemplateEngine that
* establish default configurations for message resolvers.
*
*
* @param defaultMessageResolvers the default message resolvers.
*/
public void setDefaultMessageResolvers(final Set extends IMessageResolver> defaultMessageResolvers) {
this.configuration.setDefaultMessageResolvers(defaultMessageResolvers);
}
/**
*
* Returns the size of the LRU caché of parsed templates.
*
*
* @return the current size of the caché
*/
public final int getParsedTemplateCacheSize() {
return this.configuration.getParsedTemplateCacheSize();
}
/**
*
* Sets the new size for the template caché.
*
*
* This operation can only be executed before processing templates for the first
* time. Once a template is processed, the template engine is considered to be
* initialized, and from then on any attempt to change its configuration
* will result in an exception.
*
*
* @param parsedTemplateCacheSize the new size for the caché
*/
public void setParsedTemplateCacheSize(final int parsedTemplateCacheSize) {
this.configuration.setParsedTemplateCacheSize(parsedTemplateCacheSize);
}
/**
*
* Completely clears the Parsed Template Cache.
*
*
* If this method is called before the TemplateEngine has been initialized,
* it causes its initialization.
*
*/
public void clearParsedTemplateCache() {
if (!isInitialized()) {
initialize();
}
this.templateParser.clearParsedTemplateCache();
}
/**
*
* Clears the entry in the Parsed Template Cache for the specified
* template, if it is currently cached.
*
*
* If this method is called before the TemplateEngine has been initialized,
* it causes its initialization.
*
*
* @param templateName the name of the template to be cleared from cache.
*/
public void clearParsedTemplateCacheFor(final String templateName) {
Validate.notNull(templateName, "Template name cannot be null");
if (!isInitialized()) {
initialize();
}
this.templateParser.clearParsedTemplateCacheFor(templateName);
}
/**
*
* Internal method that initializes the Template Engine instance. This method
* is called before the first execution of {@link #process(String, IContext)}
* in order to create all the structures required for a quick execution of
* templates.
*
*
* THIS METHOD IS INTERNAL AND SHOULD NEVER BE CALLED DIRECTLY.
*
*
* If a subclass of TemplateEngine needs additional steps for
* initialization, the {@link #initializeSpecific()} method should
* be overridden.
*
*/
protected final synchronized void initialize() {
if (!isInitialized()) {
logger.info("[THYMELEAF] INITIALIZING TEMPLATE ENGINE");
this.configuration.initialize();
this.templateParser = new TemplateParser(this.configuration);
initializeSpecific();
this.initialized = true;
// Log configuration details
this.configuration.printConfiguration();
logger.info("[THYMELEAF] TEMPLATE ENGINE INITIALIZED");
}
}
/**
*
* This method performs additional initializations required for a
* TemplateEngine. It is called by {@link #initialize()}.
*
*
* The implementation of this method does nothing, and it is designed
* for being overridden by subclasses of TemplateEngine.
*
*/
protected void initializeSpecific() {
// Nothing to be executed here. Meant for extension
}
/**
*
* Internal method that retrieves the thread-local index for the
* current template execution.
*
*
* THIS METHOD IS INTERNAL AND SHOULD NEVER BE CALLED DIRECTLY.
*
*
* @return the index of the current execution.
*/
public static Long threadIndex() {
return currentProcessIndex.get();
}
/**
*
* Internal method that retrieves the thread-local locale for the
* current template execution.
*
*
* THIS METHOD IS INTERNAL AND SHOULD NEVER BE CALLED DIRECTLY.
*
*
* @return the locale of the current template execution.
*/
public static Locale threadLocale() {
return currentProcessLocale.get();
}
private synchronized static void newThreadIndex() {
currentProcessIndex.set(Long.valueOf(processIndex++));
}
private synchronized static void setThreadLocale(final Locale locale) {
currentProcessLocale.set(locale);
}
/**
*
* Process a template. This method receives both a template name and a context.
*
*
* The template name will be used as input for the template resolvers, queried in chain
* until one of them resolves the template, which will then be executed.
*
*
* The context will contain the variables that will be available for the execution of
* expressions inside the template.
*
*
* @param templateName the name of the template.
* @param context the context.
* @return a String containing the result of evaluating the specified template
* with the provided context.
*/
public final String process(final String templateName, final IContext context) {
if (!isInitialized()) {
initialize();
}
try {
Validate.notNull(templateName, "Template name cannot be null");
Validate.notNull(context, "Context cannot be null");
final long startMs = System.currentTimeMillis();
newThreadIndex();
setThreadLocale(context.getLocale());
if (logger.isDebugEnabled()) {
logger.debug("[THYMELEAF][{}] STARTING PROCESS OF TEMPLATE \"{}\" WITH LOCALE {}", new Object[] {TemplateEngine.threadIndex(), templateName, context.getLocale()});
}
// Add context execution info
context.addContextExecutionInfo(templateName);
final Arguments arguments =
new Arguments(this.configuration, this.templateParser, templateName, context);
final String result = process(arguments);
final long endMs = System.currentTimeMillis();
if (logger.isDebugEnabled()) {
logger.debug("[THYMELEAF][{}] FINISHED PROCESS OF TEMPLATE \"{}\" WITH LOCALE {}", new Object[] {TemplateEngine.threadIndex(), templateName, context.getLocale()});
}
if (timerLogger.isDebugEnabled()) {
final Long elapsed = Long.valueOf(endMs - startMs);
timerLogger.debug(
"[THYMELEAF][{}][{}][{}][{}] TEMPLATE \"{}\" WITH LOCALE {} PROCESSED IN {}ms",
new Object[] {TemplateEngine.threadIndex(),
templateName, context.getLocale(), elapsed,
templateName, context.getLocale(), elapsed});
}
return result;
} catch (final OutputCreationException e) {
final Throwable cause = e.getCause();
if (cause != null && cause instanceof TransformerException) {
processOutputTransformerException((TransformerException) cause, templateName);
}
logger.error("[THYMELEAF][{}] Exception processing template \"{}\": {}", new Object[] {TemplateEngine.threadIndex(), templateName, e.getMessage()});
throw e;
} catch (final TemplateEngineException e) {
logger.error("[THYMELEAF][{}] Exception processing template \"{}\": {}", new Object[] {TemplateEngine.threadIndex(), templateName, e.getMessage()});
throw e;
} catch (final RuntimeException e) {
logger.error("[THYMELEAF][{}] Exception processing template \"{}\": {}", new Object[] {TemplateEngine.threadIndex(), templateName, e.getMessage()});
throw new TemplateProcessingException("Exception processing template", e);
}
}
private final String process(final Arguments arguments) {
final String templateName = arguments.getTemplateName();
final ParsedTemplate parsedTemplate = this.templateParser.parseDocument(arguments);
final TemplateResolution templateResolution = parsedTemplate.getTemplateResolution();
final Document document = parsedTemplate.getDocument();
if (logger.isDebugEnabled()) {
logger.debug("[THYMELEAF][{}] Starting DOM transformations on template \"{}\"", TemplateEngine.threadIndex(), templateName);
}
final DocumentType documentType =
DOMDocumentProcessor.transform(arguments, templateResolution, document);
if (logger.isDebugEnabled()) {
logger.debug("[THYMELEAF][{}] Finished DOM transformations on template \"{}\"", TemplateEngine.threadIndex(), templateName);
}
boolean outputDocType = false;
DocTypeIdentifier outputPublicId = null;
DocTypeIdentifier outputSystemId = null;
final TemplateMode templateMode = templateResolution.getTemplateMode();
if (documentType != null) {
final String publicId = documentType.getPublicId();
final String systemId = documentType.getSystemId();
if (logger.isDebugEnabled()) {
if (publicId == null || publicId.trim().equals("")) {
logger.debug("[THYMELEAF][{}] Original DOCTYPE is: SYSTEM \"{}\"", TemplateEngine.threadIndex(), systemId);
} else {
logger.debug("[THYMELEAF][{}] Original DOCTYPE is: PUBLIC \"{}\" \"{}\"", new Object[] {TemplateEngine.threadIndex(), publicId, systemId});
}
}
final IDocTypeTranslation translation =
this.configuration.getDocTypeTranslationBySource(publicId, systemId);
outputDocType = true;
if (translation != null) {
outputPublicId = translation.getTargetPublicID();
outputSystemId = translation.getTargetSystemID();
if (logger.isDebugEnabled()) {
if (outputPublicId.isNone()) {
logger.debug("[THYMELEAF][{}] Translated DOCTYPE is: SYSTEM \"{}\"", TemplateEngine.threadIndex(), outputSystemId);
} else {
logger.debug("[THYMELEAF][{}] Translated DOCTYPE is: PUBLIC \"{}\" \"{}\"", new Object[] {TemplateEngine.threadIndex(), outputPublicId, outputSystemId});
}
}
} else {
outputPublicId = DocTypeIdentifier.forValue(documentType.getPublicId());
outputSystemId = DocTypeIdentifier.forValue(documentType.getSystemId());
if (logger.isDebugEnabled()) {
logger.debug("[THYMELEAF][{}] DOCTYPE will not be translated", TemplateEngine.threadIndex());
}
}
} else {
if (templateMode.isHTML5()) {
outputDocType = true;
outputPublicId = DocTypeIdentifier.NONE;
outputSystemId = DocTypeIdentifier.NONE;
}
}
final String output =
OutputHandler.output(arguments, templateResolution, document, outputDocType, outputPublicId, outputSystemId);
return output;
}
private static void processOutputTransformerException(final TransformerException e, final String templateName) {
final Throwable cause = e.getCause();
if (cause != null && cause instanceof RuntimeException) {
final String msg = cause.getMessage();
if (msg != null && msg.contains("Namespace for prefix") && msg.contains("has not been declared")) {
final String explanation =
msg + " This can happen if your output still contains tags/attributes with a prefix for which " +
"an xmlns:* attribute has not been declared at the document root (maybe an unprocessed tag/attribute - check your dialects' leniency)";
logger.error("[THYMELEAF][{}] Exception processing template \"{}\": {}", new Object[] {TemplateEngine.threadIndex(), templateName, explanation});
throw new TemplateProcessingException(explanation);
}
}
}
}