All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.thymeleaf.TemplateEngine Maven / Gradle / Ivy

Go to download

Modern server-side Java template engine for both web and standalone environments

There is a newer version: 3.1.3.RELEASE
Show newest version
/*
 * =============================================================================
 * 
 *   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 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 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 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); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy