DEV Community

Salad Lam
Salad Lam

Posted on • Updated on

How thymeleaf processes value of "th:text" attribute

Notice

I wrote this article and was originally published on Qiita on 3 September 2019.


Assumption

In this passage Thymeleaf template engine which used in Spring MVC is discussed. Code shown is extracted from Thymeleaf 3.0.11-RELEASE and Spring MVC 5.1.9-RELEASE. Please notes that behavior of Thymeleaf used in other environment may be different.

How?

First look into following code.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Example 1</title>
  <meta charset="utf-8">
</head>
<body>
<div>
  <div th:text="a == b"></div>
</div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

After process, result is

<!DOCTYPE html>
<html>
<head>
  <title>Example 1</title>
  <meta charset="utf-8">
</head>
<body>
<div>
  <div>false</div>
</div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

How thymeleaf processes attribute "th:text" of tag "<div th:text="a == b"></div>"? Attribute is handle by class org.thymeleaf.standard.processor.StandardTextTagProcessor.

public final class StandardTextTagProcessor extends AbstractStandardExpressionAttributeTagProcessor {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Look into its parent class org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor, in function doProcess(ITemplateContext, IProcessableElementTag, AttributeName, String, IElementTagStructureHandler), parameter "attributeValue" is "a == b", which is value of attribute "th:text".

public abstract class AbstractStandardExpressionAttributeTagProcessor extends AbstractAttributeTagProcessor {

    @Override
    protected final void doProcess(
            final ITemplateContext context,
            final IProcessableElementTag tag,
            final AttributeName attributeName,
            final String attributeValue,
            final IElementTagStructureHandler structureHandler) {

        final Object expressionResult;
        if (attributeValue != null) {

            final IStandardExpression expression = EngineEventUtils.computeAttributeExpression(context, tag, attributeName, attributeValue);

            if (expression != null && expression instanceof FragmentExpression) {

                // ...

            } else {

                /*
                 * Some attributes will require the execution of the expressions contained in them in RESTRICTED
                 * mode, so that e.g. access to request parameters is forbidden.
                 */
                expressionResult = expression.execute(context, this.expressionExecutionContext);

            }

        } else {
            expressionResult = null;
        }

        // If the result of this expression is NO-OP, there is nothing to execute
        if (expressionResult == NoOpToken.VALUE) {
            if (this.removeIfNoop) {
                structureHandler.removeAttribute(attributeName);
            }
            return;
        }

        doProcess(
                context, tag,
                attributeName, attributeValue,
                expressionResult, structureHandler);

    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

In function org.thymeleaf.engine.EngineEventUtils.computeAttributeExpression(ITemplateContext, IProcessableElementTag, AttributeName, String), "a == b" will be changed to express object org.thymeleaf.standard.expression.EqualsExpression, with interface org.thymeleaf.standard.expression.IStandardExpression.

EqualsExpression.png

Following is class implements org.thymeleaf.standard.expression.IStandardExpression

IStandardExpression.png

And then value of expression is obtained from function

expressionResult = expression.execute(context, this.expressionExecutionContext);
Enter fullscreen mode Exit fullscreen mode

java.lang.Boolean object which represent "false" value is assigned to variable expressionResult. And then text representation is obtain from function

doProcess(context, tag, attributeName, attributeValue, expressionResult, structureHandler);
Enter fullscreen mode Exit fullscreen mode

Processing is finished.

Expressions in Thymeleaf

From Thymeleaf document, there is 5 type of expressions.

Name Symbol Expression class
Variable Expressions ${...} org.thymeleaf.standard.expression.VariableExpression
Selection Variable Expressions *{...} org.thymeleaf.standard.expression.SelectionVariableExpression
Message Expressions #{...} org.thymeleaf.standard.expression.MessageExpression
Link URL Expressions @{...} org.thymeleaf.standard.expression.LinkExpression
Fragment Expressions ~{...} org.thymeleaf.standard.expression.FragmentExpression

I will discuss message expressions and variable expressions. For remaining may lookup source code of class states above.

Message expressions

First look into following code.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title th:text="#{applicationName}"></title>
  <meta charset="utf-8">
</head>
<body></body>
</html>
Enter fullscreen mode Exit fullscreen mode

Values defined in resources\messages.properties.

applicationName=Example 2
Enter fullscreen mode Exit fullscreen mode

After process, result is

<!DOCTYPE html>
<html>
<head>
  <title>Example 2</title>
  <meta charset="utf-8">
</head>
<body></body>
</html>
Enter fullscreen mode Exit fullscreen mode

Property value "#{applicationName}" is represent by expression object org.thymeleaf.standard.expression.MessageExpression. And value is evaluated by static function executeMessageExpression(IExpressionContext, MessageExpression, StandardExpressionExecutionContext).

public final class MessageExpression extends SimpleExpression {

    static Object executeMessageExpression(
            final IExpressionContext context,
            final MessageExpression expression, final StandardExpressionExecutionContext expContext) {

        // ...

        final ITemplateContext templateContext = (ITemplateContext)context;

        final IStandardExpression baseExpression = expression.getBase();
        Object messageKey = baseExpression.execute(templateContext, expContext);
        messageKey = LiteralValue.unwrap(messageKey);
        if (messageKey != null && !(messageKey instanceof String)) {
            messageKey = messageKey.toString();
        }
        if (StringUtils.isEmptyOrWhitespace((String) messageKey)) {
            throw new TemplateProcessingException(
                    "Message key for message resolution must be a non-null and non-empty String");
        }


        final Object[] messageParameters;
        if (expression.hasParameters()) {

            // ...

        } else {
            messageParameters = NO_PARAMETERS;
        }

        return templateContext.getMessage(null, (String)messageKey, messageParameters, true);

    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

In some case key for lookup message is from other expression such as variable expression, so code below is for evaluate the expression inside #{...} bracket.

Object messageKey = baseExpression.execute(templateContext, expContext);
Enter fullscreen mode Exit fullscreen mode

In this example, "applicationName" is return which is the same as input. Now using this key to lookup the value by following function.

return templateContext.getMessage(null, (String)messageKey, messageParameters, true);
Enter fullscreen mode Exit fullscreen mode

templateContext is instance of class org.thymeleaf.context.WebEngineContext. Function getMessage(Class<?>, String, Object[], boolean) is defined in its parent class org.thymeleaf.context.AbstractEngineContext.

public abstract class AbstractEngineContext implements IEngineContext {

    public final String getMessage(
            final Class<?> origin, final String key, final Object[] messageParameters, final boolean useAbsentMessageRepresentation) {

        // ...

        // only have one messageResolvers: org.thymeleaf.spring5.messageresolver.SpringMessageResolver
        final Set<IMessageResolver> messageResolvers = this.configuration.getMessageResolvers();

        // Try to resolve the message
        for (final IMessageResolver messageResolver : messageResolvers) {
            final String resolvedMessage =
                    messageResolver.resolveMessage(this, origin, key, messageParameters);
            if (resolvedMessage != null) {
                return resolvedMessage;
            }
        }

        if (useAbsentMessageRepresentation) {
            // ...
        }

        return null;
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

In class org.thymeleaf.spring5.messageresolver.SpringMessageResolver, resolveMessage(ITemplateContext, Class<?>, String, Object[]) actually call org.springframework.context.support.AbstractApplicationContext.getMessage(String, Object[], Locale), facility provided by Spring's application context.

Variable expressions

First look into following code.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Example 3</title>
  <meta charset="utf-8">
</head>
<body>
  <div th:text="${message}"></div>
  <div th:text="${#locale.language}"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

And model attributes are added in controller

public String index(Model model) {
  model.addAttribute("message", "Model attribute");
  return "index";
}
Enter fullscreen mode Exit fullscreen mode

After process, result is

<!DOCTYPE html>
<html>
<head>
  <title>Example 3</title>
  <meta charset="utf-8">
</head>
<body>
  <div>Model attribute</div>
  <div>en</div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Property value "${message}" and "${#locale.language}" are represent by expression object org.thymeleaf.standard.expression.VariableExpression. And value of expression is evaluated by static function executeVariableExpression(IExpressionContext, VariableExpression, IStandardVariableExpressionEvaluator, StandardExpressionExecutionContext).

public final class VariableExpression extends SimpleExpression implements IStandardVariableExpression {

    static Object executeVariableExpression(
            final IExpressionContext context,
            final VariableExpression expression, final IStandardVariableExpressionEvaluator expressionEvaluator,
            final StandardExpressionExecutionContext expContext) {

        if (logger.isTraceEnabled()) {
            logger.trace("[THYMELEAF][{}] Evaluating variable expression: \"{}\"", TemplateEngine.threadIndex(), expression.getStringRepresentation());
        }

        final StandardExpressionExecutionContext evalExpContext =
            (expression.getConvertToString()? expContext.withTypeConversion() : expContext.withoutTypeConversion());

        final Object result = expressionEvaluator.evaluate(context, expression, evalExpContext);

        if (!expContext.getForbidUnsafeExpressionResults()) {
            return result;
        }

        // ...
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Actual value of expression is evaluated by

final Object result = expressionEvaluator.evaluate(context, expression, evalExpContext);
Enter fullscreen mode Exit fullscreen mode

Variable "expressionEvaluator" is instance of class org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator. Variable "context" is instance of class org.thymeleaf.context.WebEngineContext. Variable "expression" is instance of class org.thymeleaf.standard.expression.VariableExpression which represent "message" and "#locale.language". Variable "evalExpContext" is instance of org.thymeleaf.standard.expression.StandardExpressionExecutionContext. On function org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(IExpressionContext, IStandardVariableExpression, StandardExpressionExecutionContext)

public class SPELVariableExpressionEvaluator
        implements IStandardVariableExpressionEvaluator {

    public final Object evaluate(
            final IExpressionContext context,
            final IStandardVariableExpression expression,
            final StandardExpressionExecutionContext expContext) {

        if (logger.isTraceEnabled()) {
            logger.trace("[THYMELEAF][{}] SpringEL expression: evaluating expression \"{}\" on target", TemplateEngine.threadIndex(), expression.getExpression());
        }

        try {

            final String spelExpression = expression.getExpression();
            final boolean useSelectionAsRoot = expression.getUseSelectionAsRoot();

            if (spelExpression == null) {
                throw new TemplateProcessingException("Expression content is null, which is not allowed");
            }

            /*
             * TRY TO DELEGATE EVALUATION TO SPRING IF EXPRESSION IS ON A BOUND OBJECT
             */
            if (expContext.getPerformTypeConversion()) {
                // ...
            }

            final IEngineConfiguration configuration = context.getConfiguration();


            /*
             * OBTAIN THE EXPRESSION (SpelExpression OBJECT) FROM THE CACHE, OR PARSE IT
             */
            final ComputedSpelExpression exp = obtainComputedSpelExpression(configuration, expression, spelExpression);


            /*
             * COMPUTE EXPRESSION OBJECTS AND ADDITIONAL CONTEXT VARIABLES MAP
             * The IExpressionObjects implementation returned by processing contexts that include the Standard
             * Dialects will be lazy in the creation of expression objects (i.e. they won't be created until really
             * needed).
             */
            final IExpressionObjects expressionObjects =
                    (exp.mightNeedExpressionObjects? context.getExpressionObjects() : null);


            /*
             * CREATE/OBTAIN THE SPEL EVALUATION CONTEXT OBJECT
             */
            EvaluationContext evaluationContext =
                    (EvaluationContext) context.
                            getVariable(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME);

            if (evaluationContext == null) {
                // ...
            } else if (!(evaluationContext instanceof IThymeleafEvaluationContext)) {
                // ...
            }


            /*
             * AT THIS POINT, WE ARE SURE IT IS AN IThymeleafEvaluationContext
             *
             * This is needed in order to be sure we can modify the 'requestParametersRestricted' flag and also the
             * expression objects.
             */
            final IThymeleafEvaluationContext thymeleafEvaluationContext = (IThymeleafEvaluationContext) evaluationContext;


            /*
             * CONFIGURE THE IThymeleafEvaluationContext INSTANCE: expression objects and restrictions
             *
             * NOTE this is possible even if the evaluation context object is shared for the whole template execution
             * because evaluation contexts are not thread-safe and are only used in a single template execution
             */
            thymeleafEvaluationContext.setExpressionObjects(expressionObjects);
            thymeleafEvaluationContext.setVariableAccessRestricted(expContext.getRestrictVariableAccess());


            /*
             * RESOLVE THE EVALUATION ROOT
             */
            final ITemplateContext templateContext = (context instanceof ITemplateContext ? (ITemplateContext) context : null);
            final Object evaluationRoot =
                    (useSelectionAsRoot && templateContext != null && templateContext.hasSelectionTarget()?
                            templateContext.getSelectionTarget() : new SPELContextMapWrapper(context, thymeleafEvaluationContext));


            /*
             * If no conversion is to be made, JUST RETURN
             */
            if (!expContext.getPerformTypeConversion()) {
                return exp.expression.getValue(thymeleafEvaluationContext, evaluationRoot);
            }

            // ...

        } catch (final TemplateProcessingException e) {
            throw e;
        } catch(final Exception e) {
            throw new TemplateProcessingException(
                    "Exception evaluating SpringEL expression: \"" + expression.getExpression() + "\"", e);
        }

    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

"message", "#locale.language" read from VariableExpression and SpelExpression instance is built (or get from cache if available) on following

final ComputedSpelExpression exp = obtainComputedSpelExpression(configuration, expression, spelExpression);
Enter fullscreen mode Exit fullscreen mode

EvaluationContext which used by SpelExpression is first saved into model variable with key "thymeleaf::EvaluationContext" (defined in org.thymeleaf.spring5.expression.ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME) in function org.thymeleaf.spring5.view.ThymeleafView.renderFragment(Set, Map, HttpServletRequest, HttpServletResponse). And extracted on following

EvaluationContext evaluationContext =
                    (EvaluationContext) context.
                            getVariable(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME);
Enter fullscreen mode Exit fullscreen mode

SpelExpression is finally evaluated on following

return exp.expression.getValue(thymeleafEvaluationContext, evaluationRoot);
Enter fullscreen mode Exit fullscreen mode

SpEL variable can be obtained by function
org.thymeleaf.spring5.expression.ThymeleafEvaluationContext.lookupVariable(String). Model variable can be obtained from evaluationRoot variable, which is instance of org.thymeleaf.spring5.expression.SPELContextMapWrapper. SPELContextMapWrapper is a wrapper of org.thymeleaf.context.WebEngineContext. Model variable can be obtained from WebEngineContext by getVariable(String) function.

SpEL variable which can be used in SpEL when writing variable expression

In SpEL, variable can be referenced in the expression using the syntax #variableName. following pre-defined variable can be used on writing expression in Thymeleaf template.

org.thymeleaf.spring5.expression.SpringStandardExpressionObjectFactory

Name Class of instance
#fields org.thymeleaf.spring5.expression.Mvc
#themes org.thymeleaf.spring5.expression.Themes
#mvc org.thymeleaf.spring5.expression.Fields
#requestdatavalues org.thymeleaf.spring5.expression.RequestDataValues

org.thymeleaf.standard.expression.StandardExpressionObjectFactory

Name Class of instance
#ctx class implements org.thymeleaf.context.IExpressionContext
#root class implements org.thymeleaf.context.IExpressionContext
#vars class implements org.thymeleaf.context.IExpressionContext
#object java.lang.Object or class implements org.thymeleaf.context.IExpressionContext
#locale java.util.Locale
#request class implements javax.servlet.http.HttpServletRequest
#response class implements javax.servlet.http.HttpServletResponse
#session class implements javax.servlet.http.HttpSession
#servletContext class implements javax.servlet.ServletContext
#conversions org.thymeleaf.expression.Conversions
#uris org.thymeleaf.expression.Uris
#calendars org.thymeleaf.expression.Calendars
#dates org.thymeleaf.expression.Dates
#bools org.thymeleaf.expression.Bools
#numbers org.thymeleaf.expression.Numbers
#objects org.thymeleaf.expression.Objects
#strings org.thymeleaf.expression.Strings
#arrays org.thymeleaf.expression.Arrays
#lists org.thymeleaf.expression.Lists
#sets org.thymeleaf.expression.Sets
#maps org.thymeleaf.expression.Maps
#aggregates org.thymeleaf.expression.Aggregates
#messages org.thymeleaf.expression.Messages
#ids org.thymeleaf.expression.Ids
#execInfo org.thymeleaf.expression.ExecutionInfo
#httpServletRequest (Deprecated) class implements javax.servlet.http.HttpServletRequest
#httpSession (Deprecated) class implements javax.servlet.http.HttpSession

org.thymeleaf.extras.java8time.dialect.Java8TimeExpressionFactory

Name Class of instance
#temporals org.thymeleaf.extras.java8time.expression.Temporals

org.thymeleaf.extras.springsecurity5.dialect.expression.SpringSecurityExpressionObjectFactory

Name Class of instance
#authentication class implements org.springframework.security.core.Authentication
#authorization org.thymeleaf.extras.springsecurity5.auth.Authorization

Model variable which can be used in SpEL when writing variable expression

Name Class of instance
session org.thymeleaf.context.WebEngineContext$SessionAttributesMap
param org.thymeleaf.context.WebEngineContext$RequestParametersMap
application org.thymeleaf.context.WebEngineContext$ServletContextAttributesMap
thymeleaf::EvaluationContext org.thymeleaf.spring5.expression.ThymeleafEvaluationContext
thymeleafRequestContext org.thymeleaf.spring5.context.webmvc.SpringWebMvcThymeleafRequestContext
springMacroRequestContext org.thymeleaf.spring5.context.webmvc.SpringWebMvcThymeleafRequestContext
springRequestContext org.thymeleaf.spring5.context.webmvc.SpringWebMvcThymeleafRequestContext

Top comments (0)