Templating with Thymeleaf: Custom Dialects and More (Part 3)

 · 11 min

In the previous articles, we explored the fundamentals of Thymeleaf and focused on reusability with fragments and the Layout Dialect.

Now, in this final installment, we’ll dive into the more advanced features, like creating a custom dialect, processors, and more.


Custom Dialects and Processors

Custom dialects allow you to extend Thymeleaf’s capabilities by defining new processors (tags, attributes) and functionality that can be reused throughout your templates.

What’s a Dialect?

A Dialect is a collection of custom processors and utility methods that provide extended functionality. With a custom Dialect, you can introduce new attributes or elements that simplify complex tasks, encapsulate common logic, or align your templates more closely with your business requirements.

Creating a Custom Dialect

As our example, we’re going to implement a custom Dialect for formatting dates.

The general idea is to specify a date format and render the current date with it:

Formatting the current date
<span dateformat:format="yyyy-MM-dd">2024-10-18</span>

To create our custom Dialect, you need to:

  • Define processor(s) (i.e., custom tags or attributes).
  • Register the dialect to make it available in your templates.

Let’s start by creating a processor!

Creating a Processor

Processors are the units that, well, process different parts of the template. While they’re often used with tags/elements, they are not limited to them.

Thymeleaf offers different types to handle attributes, elements, and even the template structure itself.

For our intended use-case, we want an Element Processor. To not start from scratch, we extend AbstractElementTagProcessor. That way, we only need to call the super constructor to set up the processor and implement doProcess(...) ourselves:

Custom Processor
public class DateFormatProcessor extends AbstractAttributeTagProcessor {

  public DateFormatProcessor(final String dialectPrefix) {
    super(
      TemplateMode.HTML, // TemplateMode: Processor works in HTML mode
      dialectPrefix,     // Dialect Prefix
      null,              // Element name to match (null -> any)
      false,             // Does Element name should be prefixed?
      "format",          // Name of the attribute to match
      true,              // Apply dialect prefix to attribute name
      10_000,            // Precedence (inside dialect's precedence)
      true);             // Remove attribute
  }

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

    // STEP 1: VALIDATE

    if (attributeValue == null || attributeValue.isBlank()) {
      throw new TemplateProcessingException("Attribute value can't be blank");
    }

    // STEP 2: CREATE FORMATTER AND FORMAT DATA

    var formatter = DateTimeFormatter.ofPattern(attributeValue);
    var formattedDate = LocalDateTime.now().format(formatter);

    // STEP 3: SET BODY OF ELEMENT

    structureHandler.setBody(formattedDate, false);
  }
}

The super call configures the Processor with all the necessities, like which TemplateMode to work in, how to match, etc.

In our case the Processor looks for an attribute named format prepended by the dialect’s prefix.

Step 1 is doing some validation. Instead of throwing a TemplateProcessingException, we could output an error message or a sensible fallback directly into the output.

Step 2 does the actual work of formatting the current date.

And finally, step 3 is replacing the element’s body content with the formatted date. The structureHandler gives us access to the currently processed element and its context. The second parameter set to false determines if the new text is processable by other processors.

With a Processor at hand, we can create a Dialect with it.

Creating a Dialect

Let’s create a simple custom Dialect that adds a new attribute for inserting and formatting the current date:

Custom Dialect
public class DateFormatDialect extends AbstractProcessorDialect {

  public DateFormatDialect() {
    super(
      "Date Format Dialect", // Dialect name
      "dateformat",          // Dialect prefix
      1_000);                // Precendence
    }

    @Override
    public Set<IProcessor> getProcessors(String dialectPrefix) {
      return Set.of(new DateFormatProcessor(dialectPrefix));
    }
}

As usual, we won’t implement IDialect directly but extend AbstractProcessorDialect instead.

The super class sets the dialect’s name, prefix, and overall precedence, and getProcessors(...) returns a Set<IProcessor> that belong to the dialect.

The Precedence defines in which order processors do their jobs. If a Processor generates content that needs to be processed further, it’s important to let it do its job before the other.

Now, we can add the Dialect to the template engine:

Reigstering a Dialect
TemplateEngine templateEngine = new TemplateEngine();
templateEngine.addDialect(new DateFormatDialect());

That’s it!

These were all the parts involved in creating a custom Processor, wrapping it into a Dialect, and making it available to our templates.

Our example was quite simplistic; you should definitely check out the other processor types which working on different aspects:

  • Elements: Operate on HTML elements or tags (which we’ve seen here)
  • Attributes: Triggered by custom or standard attributes on tags
  • Text: Process text content inside elements
  • Template: Transform an entire template or sections of it
  • Document: Modify the entire document before or after processing
  • Inline: Manipulate inline expressions or text.

Check out the documentation for more detailed information:


Expression Utility Objects

Expression utility objects are utility objects that provide additional functionality. They simplify common tasks, like String operations or formatting values.

These objects are accessed using the # (number sign) syntax, like #numbers. Thymeleaf has 17 built-in expression objects:

  • #execInfo: Access template/processing information
  • #messages: Obtaining externalized messages
  • #dates: Working with java.util.Date objects
  • #calendars: Analogous to #dates but for java.util.Calendar objects
  • #numbers: Formatting numeric values
  • #uris: Escaping/unescaping URI/URL parts
  • #conversions: Use the Conversion Service for type coercing
  • #temporals: Working with the java.time API
  • #strings: Manipulating Strings
  • #objects: Working with objects in general
  • #bools: Bool operations
  • #arrays/lists/sets/maps: Working with different container types
  • #aggregates: aggregates on arrays or collections
  • #ids: Helper for dealing with id attributes

You find all of the available objects and their methods here:

In addition to expression utility objects, there are also expression basic objects:

  • #ctx: The IContext of the template
  • #vars/root: Synonyms for #ctx (not recommended)
  • #locale: Direct access to `Locale of the current request

Let’s look at a few of the utility objects in more detail.

Working with Strings

The #strings object helps to manipulate Strings directly within a template. It provides common String operations, making it easier to perform tasks like concat, trim, replace, capitalize, or create a substring without resorting to custom utility classes or doing it beforehand in Java.

One practical example is concatenating Strings, which is null-friendly, as it replaces them with an empty String instead of rendering the word “null”:

Concatenating Strings
<p th:text="${#strings.concat('Hello, ', user.firstname, ' ', user.lastname, '!')}"></p>

As you see, we can mix literals with expressions for the arguments.

Besides manipulating Strings, there are methods for conditionals, too:

  • Boolean contains(target, fragment)
  • Boolean containsIgnoreCase(target, fragment)
  • Boolean equals(first, second)
  • Boolean equalsIgnoreCase(first, second)
  • Boolean startsWith(target, prefix)
  • Boolean endWith(target, prefix)

You should definitely check out the documentation for the full list.

Working with Dates

The #dates utility object gives us way to format java.util.Date instance, get the current date (so much for our custom dialect), and get specific properties of a date.

One common use case is formatting a date:

Formatting Dates
<p>Log date: <span th:text="${#dates.format(logEvent.date, 'yyyy-MM-dd')}"></span></p>

As expressions can be nested, we could format the current date as we want by using two calls:

Nested Calls
<p>Today: <span th:text="${#dates.format(#dates.createToday(), 'EEEE, MMMM d, yyyy')}"></span></p>

We can access a date’s component quite easily to create easier expression than using format:

  • Integer day(target)
  • Integer month(target)
  • String monthName(target)
  • String monthNameShort(target)
  • Integer year(target)
  • Integer dayOfWeek(target)
  • String dayOfWeekName(target)
  • String dayOfWeekNameShort(target)
  • Integer hour(target)
  • Integer minute(target)
  • Integer second(target)
  • Integer millisecond(target)

Unlike #strings, the #dates object has no methods for conditionals.

If we want to use java.util.Calendar instead of Date, we can use #calendars which has an analogous API. And if we have to deal with the java.time API, we can use #temporals.

Creating Your Own Utility Object

While the built-in expression objects cover many common use cases, we might encounter scenarios where a custom expression object is preferable, like repetitive domain-specific tasks. By creating our own object, we can encapsulate specific logic and expose it directly in our templates.

Custom expression utility objects are based around IExpressionObjectFactory that has three methods in need of implementation:

Custom Expression Object Factory
public class CustomExpressionObjectFactory implements IExpressionObjectFactory {

  @Override
  public Set<String> getAllExpressionObjectNames() {
    return Set.of("custom");
  }

  @Override
  public Object buildObject(IExpressionContext context,
                            String expressionObjectName) {
    return new MyCustomUtils();
  }

  @Override
  public boolean isCacheable(final String expressionObjectName) {
    return true;
  }
}

The factory provides on or more expression objects based on their names.

The buildObject(...) method creates new instances of an expression object based on its name.

The isCachable(...) method dictates if the expression object needs to be recreated every time it’s used, or if it can be cached for a template.

The expression utility object type itself can be anything, as all its non-static public methods are exposed to the template via the registered name.

After creating the type and factory, we can add it to a Dialect:

Custom Expression Utility Object Dialect
public class CustomExpressionUtilityObjectDialect
    extends AbstractDialect
    implements IExpressionObjectDialect {

  private static final IExpressionObjectFactory CACHED_FACTORY = new CustomExpressionObjectFactory(); 

  public CustomExpressionUtilityObjectDialect() {
    super("CustomExpressionDialect");
  }

  @Override
  public IExpressionObjectFactory getExpressionObjectFactory() {
    return CACHED_FACTORY;
  }
}

Just register the Dialect with the template engine, and the expression utility object is available throughout our Thymeleaf templates, providing reusable utility functions tailored to your application’s needs.


Custom Template Resolving

Thymeleaf provides a range of template resolvers out of the box, like ClassLoaderTemplateResolver or FileTemplateResolver, which we looked at in the first part of this series.

But what if our templates are stored in a custom location, like a database or a remote service?

Don’t fret, we can easily create our own template resolver to load templates from any source!

Custom Processing for Data Transformation

As an example, we pretend to implement a database-based loader. However, the actual loading isn’t shown, as we’re only interested in the Thymeleaf-relevant parts for this article.

Like with many other customizations shown previously, we don’t start with the lowest level, the ITemplateResolver, but with an abstract class that provides a lot of functionality to get started: AbstractConfigurableTemplateResolver. This way, we just need a constructor and a single method for a minimalistic implementation, but still can override more methods if needed:

Resolving templates from database
public class DatabaseTemplateResolver extends AbstractConfigurableTemplateResolver {

  // THIS IS HOW WE ACCESS THE DATABASE AND DEPENDS ON
  // YOUR APPLICATION (OUT OF SCOPE FOR THE ARTICLE)
  private final TemplateDAO dao;

  public DatabaseTemplateResolver(TemplateDAO dao) {
    super();
    this.dao = dao;
    setTemplateMode(TemplateMode.HTML); // Redundant, this is the defaultSet the default template mode.
    setCharacterEncoding("UTF-8");      // This might be necessary, there's no default
  }

  @Override
  protected ITemplateResource computeTemplateResource(
      IEngineConfiguration configuration,
      String ownerTemplate,
      String template,
      String resourceName,
      String characterEncoding,
      Map<String, Object> templateResolutionAttributes) {

    // STEP 1: Get template from database
    String templateContent = this.dao.findTemplateByName(templateName);

    // STEP 2: Validation
    if (templateContent == null) {
      return null;
    }

    // STEP 3: Create a ITemplateResource
    return new StringTemplateResource(templateContent);
  }
}

Then, we need to register the resolver with the template engine:

Adding template resolver
DatabaseTemplateResolver resolver = new DatabaseTemplateResolver(...);
templateEngine.addTemplateResolver(resolver);

That’s it!

Caching with Custom Template Resolvers

If we only need a binary choice for caching, we could call setCacheable(boolean) either in the constructor, or before registering the resolver. But there’s an option to have more fine-grained control by overriding computeValidity(...):

Caching Control
@Override
protected ICacheEntryValidity computeValidity(
    IEngineConfiguration configuration,
    String ownerTemplate,
    String template,
    Map<String, Object> templateResolutionAttributes) {

  // CUSTOM CONDITION FOR CACHING GOES HERE
  if (...) {
    return AlwaysValidCacheEntryValidity.INSTANCE;
  }

  return NonCacheableCacheEntryValidity.INSTANCE;
}

The method arguments don’t provide much to make an informed decision, and the built-in resolvers mostly use pattern-matching and if the resolver is cacheable in general. The templateResolutionAttributes are set if the template engine is called with a TemplateSpec instead of the template name:

Processing a TemplateSpec
TemplateSpec spec = new TemplateSpec("template-name",
                                     Map.of("cacheable", false));

templateEngine.process(spec, context);

Controlling caching balances performance with freshness, ensuring we get don’t get outdated template content. However, caching can be tricky at times, so proceed with caution.


Conclusion

Throughout this article series, we’ve explored the many facets of working with Thymeleaf: from the basics of template creation to diving deeper into advanced topics like custom Dialects, Expression Objects, and template resolving.

Thymeleaf has proven itself to be a versatile and robust solution for any templating need. Its natural templating approach makes collaboration with other people simpler without sacrificing any features. The extensive expression language and built-in processors make Thymeleaf a good fit for applications of any size and complexity.

One of Thymeleaf’s standout features is its extensibility. Creating custom processors or expression objects can tailor Thymeleaf to quite domain-specific needs. This ensures that Thymeleaf is adaptable and future-proof to unique project requirements.

Even though it wasn’t part of the series, it still should be mentioned that Thymeleaf offers seamless integration with the Spring ecosystem. Its native support, including form binding and validation, makes it an ideal companion for Spring-based web applications.

In conclusion, Thymeleaf combines the best of both worlds: the simplicity of natural templates with the power of a fully-featured and easily customizable template engine. Whether you’re building a small project or a large-scale enterprise application, I absolutely to check out Thymeleaf!


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Resources

Series: Templating with Thymeleaf