How thymeleaf processes value of "th:text" attribute
Tags: spring, thymeleaf, springmvc
Published: 2024-01-31 21:21:44

## Notice

I wrote this article and was originally published on [Qiita](https://qiita.com/saladlam/items/5d7bd0ded5d88c900fba) 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.

```html
<!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>
```

After process, result is

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

How thymeleaf processes attribute "th:text" of tag "&lt;div th:text=&quot;a == b&quot;&gt;&lt;/div&gt;"? Attribute is handle by class org.thymeleaf.standard.processor.StandardTextTagProcessor.

```java
public final class StandardTextTagProcessor extends AbstractStandardExpressionAttributeTagProcessor {
    // ...
}
```

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".

```java
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);

    }
    // ...
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/576s31qmgobz3blzi2gz.png)

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

![IStandardExpression.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qih0sbk66f8uj6ajwukc.png)

And then value of expression is obtained from function

```java
expressionResult = expression.execute(context, this.expressionExecutionContext);
```

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

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

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.

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

Values defined in resources\messages.properties.

```properties
applicationName=Example 2
```

After process, result is

```html
<!DOCTYPE html>
<html>
<head>
  <title>Example 2</title>
  <meta charset="utf-8">
</head>
<body></body>
</html>
```

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

```java
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);
        
    }
    // ...
}
```

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.

```java
Object messageKey = baseExpression.execute(templateContext, expContext);
```

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

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

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.

```java
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;
    }
    // ...
}
```

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.

```html
<!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>
```

And model attributes are added in controller

```java
public String index(Model model) {
  model.addAttribute("message", "Model attribute");
  return "index";
}
```

After process, result is

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

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).

```java
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;
        }

        // ...
    }
    // ...
}
```

Actual value of expression is evaluated by

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

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)

```java
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);
        }
        
    }
    // ...
}
```

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

```java
final ComputedSpelExpression exp = obtainComputedSpelExpression(configuration, expression, spelExpression);
```

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<String>, Map<String, ?>, HttpServletRequest, HttpServletResponse). And extracted on following

```java
EvaluationContext evaluationContext =
                    (EvaluationContext) context.
                            getVariable(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME);
```

SpelExpression is finally evaluated on following

```java
return exp.expression.getValue(thymeleafEvaluationContext, evaluationRoot);
```

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 |