Preparing Message Payloads with Thymeleaf

By | August 8, 2017

In this article I will show how to prepare message payloads with parameters in different formats using the Thymeleaf template engine. No previous knowledge of Thymeleaf is assumed.

Thymeleaf logo copyright © The Thymeleaf Team.

Thymeleaf logo copyright © The Thymeleaf Team.

As far as I am concerned there are several use cases for this. Some examples are:

  • System integration.
    Requests can be maintained as plain files and no code has to be written to create message payloads.
    In addition I am able to create messages for legacy systems, for which there is no library that supports marshalling of messages, and insert parameters into these messages.
  • Creating test request in test client code.
  • Generating mock response messages in mock services.

Setting Up the Project

I will use Spring Boot 2.0.0.M3, which is the latest version available as of writing this article, and Thymeleaf 3.0.7.RELEASE. The Thymeleaf version is the version specified in the Spring Boot Thymeleaf starter, so any version 3 of Thymeleaf will suffice.

Use the following link to generate the skeleton for this article’s example project.
For the curious, the above link uses the Spring Initializr web page to generate a Maven project with Java and Spring Boot.
Once the archive is downloaded, unpack it to a location of your choice and open/import the Maven project into your IDE.

Adding Dependencies

The test in the example project will use REST-Assured and Hamcrest. In REST-Assured I will use only the XML and JSON path parts.
Add the following dependencies to the pom.xml file in the <dependencies> element of the example project:

        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>3.0.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>java-hamcrest</artifactId>
            <version>2.0.0.0</version>
            <scope>test</scope>
        </dependency>

Tests

Being the flawless developer that I am, I always write my tests first 🙂 (it is significantly easier being flawless writing articles about writing code that you already have written while you pretend that you write the code for the first time). With the boasting out of the way, lets look at some tests. I’ve implemented my tests in the test class that Spring Initializr generated for me, that is MessagePayloadsWithThymeleafApplicationTests. There will be some syntax errors at first, since we haven’t implemented the Thymeleaf configuration class yet, but please bear with me.

package se.ivankrizsan.java.messagepayloadswiththymeleaf;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;

import io.restassured.path.json.JsonPath;
import io.restassured.path.xml.XmlPath;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;

/**
 * Tests obtaining different types of message payloads with parameters inserted
 * using Thymeleaf.
 *
 * @author Ivan Krizsan
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class MessagePayloadsWithThymeleafApplicationTests {
    /* Constant(s): */
    public static final String CURRENCY_SEK = "SEK";
    public static final String CURRENCY_NTD = "NTD";

    protected static final Logger LOGGER = LoggerFactory.getLogger(
        MessagePayloadsWithThymeleafApplicationTests.class);

    /* Instance variable(s): */
    @Autowired
    @Qualifier("messageTemplateEngine")
    protected SpringTemplateEngine mMessageTemplateEngine;

    /**
     * Tests retrieval of an XML message in which two parameter values are inserted.
     * Expected result: The message should be retrieved with the parameter values
     * inserted into it.
     */
    @Test
    public void xmlMessageTest() {
        /* Retrieve XML message with inserted parameter values. */
        final Context theContext = new Context();
        theContext.setVariable(ThymeleafConfiguration.TEMPLATE_FROMCURRENCY_PARAM, CURRENCY_SEK);
        theContext.setVariable(ThymeleafConfiguration.TEMPLATE_TOCURRENCY_PARAM, CURRENCY_NTD);
        final String theXmlMessage =
            mMessageTemplateEngine.process("xml/currency_conversion_request", theContext);

        /* Verify the XML message. */
        final XmlPath theXmlMessageXmlPath = XmlPath.with(theXmlMessage);
        assertThat(theXmlMessageXmlPath.getString("Envelope.Body.ConversionRate.FromCurrency"),
            is(CURRENCY_SEK));
        assertThat(theXmlMessageXmlPath.getString("Envelope.Body.ConversionRate.ToCurrency"),
            is(CURRENCY_NTD)
        );

        /* Log the message to the console for visual feedback. */
        LOGGER.info(theXmlMessage);
    }

    /**
     * Tests retrieval of an JSON message in which two parameter values are inserted.
     * Expected result: The message should be retrieved with the parameter values
     * inserted into it.
     */
    @Test
    public void jsonMessageTest() {
        /* Retrieve JSON message with inserted parameter values. */
        final Context theContext = new Context();
        theContext.setVariable(ThymeleafConfiguration.TEMPLATE_FROMCURRENCY_PARAM, CURRENCY_SEK);
        theContext.setVariable(ThymeleafConfiguration.TEMPLATE_TOCURRENCY_PARAM, CURRENCY_NTD);
        final String theJsonMessage =
            mMessageTemplateEngine.process("json/currency_conversion_request", theContext);

        /* Verify the JSON message. */
        final JsonPath theJsonMessageJsonPath = JsonPath.with(theJsonMessage);
        assertThat(theJsonMessageJsonPath.getString("conversion_rate.from_currency"),
            is(CURRENCY_SEK));
        assertThat(theJsonMessageJsonPath.getString("conversion_rate.to_currency"),
            is(CURRENCY_NTD)
        );

        /* Log the message to the console for visual feedback. */
        LOGGER.info(theJsonMessage);
    }

    /**
     * Tests retrieval of a text message in which two parameter values are inserted.
     * Expected result: The message should be retrieved with the parameter values
     * inserted into it.
     */
    @Test
    public void textMessageTest() {
	    /* Retrieve text message with inserted parameter values. */
        final Context theContext = new Context();
        theContext.setVariable(ThymeleafConfiguration.TEMPLATE_FROMCURRENCY_PARAM, CURRENCY_SEK);
        theContext.setVariable(ThymeleafConfiguration.TEMPLATE_TOCURRENCY_PARAM, CURRENCY_NTD);
        final String theTextMessage =
            mMessageTemplateEngine.process("text/currency_conversion_request", theContext);

        /* Verify the text message. */
        assertThat(theTextMessage, containsString(CURRENCY_SEK));
        assertThat(theTextMessage, containsString(CURRENCY_NTD));

        /* Log the message to the console for visual feedback. */
        LOGGER.info(theTextMessage);
    }
}

Obviously we can’t run the tests at this stage but I will examine the code close, since this is the kind of code you will write in your programs if you want to use Thymeleaf to retrieve, for instance, test messages or responses for a mock service.

  • There is an instance variable named mMessageTemplateEngine of the type SpringTemplateEngine.
    This is the object used when interacting with Thymeleaf. The astute reader notice that it is there is the word “Spring” in the name. Yes, the Spring-people have extended the Thymeleaf class TemplateEngine but the examples in this article work equally well using plain vanilla Thymeleaf – see note below.
  • The Thymeleaf class Context is used in all three tests to pass information to the template engine.
    I have used this class since retrieving message templates is not really closely tied to web applications and there is no servlet context, nor servlet request or response to set on the context, as required by, for instance, the WebContext context class in Thymeleaf.
    Of course, there is nothing stopping you from using Thymeleaf to retrieve message templates in a web application but I would use a separate template engine for message templates.
  • In all three test methods, the messages are retrieved in the same way.
    First an instance of the Thymeleaf class Context is created and then variables are set in the context. Finally the message is retrieved by invoking process on the template engine.
    The only thing that sets retrieving message templates of different formats, be it JSON, XML or plain text, apart is the template name.
  • In the xmlMessageTest method, REST-assured’s XML-path is used to retrieve the contents of the XML elements that are expected to be modified in the XML template.
  • In a similar manner in the jsonMessageTest method, REST-assureds JSON-path is used to retrieve the contents of the JSON elements expected to be modified in the JSON template.
  • In all three test methods I log the processed messages to the console.
    This is just to have some visual feedback for the sake of this example and nothing I would normally do.

A note on using the Thymeleaf TemplateEngine Type

If you want to create a Spring bean of the type TemplateEngine in your Spring Boot project, you may want to consider two things:

First, you need to add a dependency to the OGNL library. This is also true of you want to use Thymeleaf in a non-Spring project. The reason for this is that if you use the Spring classes that inherit from the Thymeleaf classes, they change the default expression language used by Thymeleaf from OGNL to Spring expression language. If you use Thymeleaf directly, then the default expression language is OGNL.

<dependency>
    <groupId>ognl</groupId>
    <artifactId>ognl</artifactId>
    <version>3.2.3</version>
</dependency>

Second, the Spring Boot Thymeleaf starter will create a Spring bean named templateEngine of the type TemplateEngine, so you will need to use @Qualifier on any autowired parameters etc if you decide to create a TemplateEngine bean with another name.

Template Directories and Files

As I promised earlier, there will be three different types of templates; XML, JSON and text templates. As to prepare for projects where there are more than three templates, a separate directory will be used for each of the three formats.

  • In src/main/resources/templates create three directories named “json”, “text” and “xml”.
  • In each of the directories create a file that is named “currency_conversion_request” that has a postfix that matches the formats (“.xml”, “.json” and “.txt”).
    The result should look something like this in your IDE:

    Thymeleaf message templates directory structure.

JSON Message Template

In the “currency_conversion_request.json” file, enter the following:

{
  "conversion_rate" : {
        "from_currency" : "[(${from_currency})]",
        "to_currency" : "[(${to_currency})]"
  }
}

Note that:

  • In the value of the from_currency key, there is some kind of placeholder.
    This is a Thymeleaf placeholder used in JSON, and as we will see below, text message templates. Thymeleaf will replace this placeholder with the value of the variable from_currency found in the Thymeleaf context.
  • The placeholders include the characters [(${ and })].
    These characters will also be replaced/removed when a placeholder is replaced by a value.
  • In the value of the to_currency key there is also a placeholder similar to the one we just saw.
  • Apart from the two placeholders, there is nothing special about the JSON message.

Text Message Template

In the “currency_conversion_request.txt” file, enter the following:

ConversionRate:
    FromCurrency: [(${from_currency})]
    toCurrency: [(${to_currency})]

Note that:

  • As with the JSON message above, there is a placeholder for the from_currency.
  • Again there is a placeholder for the to_currency key.
  • The placeholders include the characters [(${ and })].
    These characters will also be replaced/removed when a placeholder is replaced by a value.
  • Apart from the two placeholders, there is nothing special about the text message.

XML Message Template

In the “currency_conversion_request.xml” file, enter the following:

<soapenv:Envelope
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:web="http://www.webserviceX.NET/"
    xmlns:th="http://www.thymeleaf.org">
    <soapenv:Header/>
    <soapenv:Body>
        <web:ConversionRate>
            <web:FromCurrency th:text="${from_currency}"></web:FromCurrency>
            <web:ToCurrency th:text="${to_currency}"></web:ToCurrency>
        </web:ConversionRate>
    </soapenv:Body>
</soapenv:Envelope>

Note that:

  • There is a namespace declaration for the Thymeleaf namespace looking like this:
    xmlns:th=”http://www.thymeleaf.org”
    This is required in XML templates used with a Thymeleaf template resolver that operates in XML mode. This namespace declaration will not, as we will see later when we run our tests, show up in the messages generated from this template.
  • There is an attribute th:text=”${from_currency}” in the <FromCurrency> element.
    We see that the namespace of this attribute is the Thymeleaf namespace, as declared earlier. As with the namespace declaration, this attribute declaration will not appear in the messages generated from this template. It will, however, tell Thymeleaf to insert the value of the from_currency variable into the <FromCurrency> element.
  • The contents of the <FromCurrency> element is empty.
    As before, Thymeleaf will insert the name of the currency to exchange from in this element.
  • The <ToCurrency> element will be handled in a similar manner by Thymeleaf.

Thymeleaf Configuration

The only thing that now remains before we will be able to run the tests in the example is the Thymeleaf configuration. Being a small example program, I created it in the same package as the main class.

package se.ivankrizsan.java.messagepayloadswiththymeleaf;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;

import java.util.Collection;
import java.util.Collections;

/**
 * Thymeleaf configuration for the message payloads example.
 *
 * @author Ivan Krizsan
 */
@Configuration
public class ThymeleafConfiguration {
    /* Constant(s): */
    /**
     * Path to root package/directory in which the different types of message
     * templates are found.
     */
    public static final String TEMPLATES_BASE = "classpath:/templates/";
    /** Pattern relative to templates base used to match XML templates. */
    public static final String XML_TEMPLATES_RESOLVE_PATTERN = "xml/*";
    /** Pattern relative to templates base used to match JSON templates. */
    public static final String JSON_TEMPLATES_RESOLVE_PATTERN = "json/*";
    /** Pattern relative to templates base used to match text templates. */
    public static final String TEXT_TEMPLATES_RESOLVE_PATTERN = "text/*";
    /* Parameter keys for all the message templates. */
    public static final String TEMPLATE_FROMCURRENCY_PARAM = "from_currency";
    public static final String TEMPLATE_TOCURRENCY_PARAM = "to_currency";

    /* Instance variable(s): */


    /**
     * Creates the template resolver that retrieves XML message payloads.
     *
     * @return Template resolver.
     */
    @Bean
    public SpringResourceTemplateResolver xmlMessageTemplateResolver() {
        SpringResourceTemplateResolver theResourceTemplateResolver =
            new SpringResourceTemplateResolver();
        theResourceTemplateResolver.setPrefix(TEMPLATES_BASE);
        theResourceTemplateResolver.setResolvablePatterns(
            Collections.singleton(XML_TEMPLATES_RESOLVE_PATTERN));
        theResourceTemplateResolver.setSuffix(".xml");
        theResourceTemplateResolver.setTemplateMode("xml");
        theResourceTemplateResolver.setCharacterEncoding("UTF-8");
        theResourceTemplateResolver.setCacheable(false);
        theResourceTemplateResolver.setOrder(1);
        return theResourceTemplateResolver;
    }

    /**
     * Creates the template resolver that retrieves JSON message payloads.
     *
     * @return Template resolver.
     */
    @Bean
    public SpringResourceTemplateResolver jsonMessageTemplateResolver() {
        SpringResourceTemplateResolver theResourceTemplateResolver =
            new SpringResourceTemplateResolver();
        theResourceTemplateResolver.setPrefix(TEMPLATES_BASE);
        theResourceTemplateResolver.setResolvablePatterns(
            Collections.singleton(JSON_TEMPLATES_RESOLVE_PATTERN));
        theResourceTemplateResolver.setSuffix(".json");
        /*
         * There is no json template mode so the next line has
         * been commented out. Thymeleaf will recognize the
         * ".json" template resource suffix so there is no need
         * to set a template mode.
         */
        // theResourceTemplateResolver.setTemplateMode("json");
        theResourceTemplateResolver.setCharacterEncoding("UTF-8");
        theResourceTemplateResolver.setCacheable(false);
        theResourceTemplateResolver.setOrder(2);
        return theResourceTemplateResolver;
    }

    /**
     * Creates the template resolver that retrieves text message payloads.
     *
     * @return Template resolver.
     */
    @Bean
    public SpringResourceTemplateResolver textMessageTemplateResolver() {
        SpringResourceTemplateResolver theResourceTemplateResolver =
            new SpringResourceTemplateResolver();
        theResourceTemplateResolver.setPrefix(TEMPLATES_BASE);
        theResourceTemplateResolver.setResolvablePatterns(
            Collections.singleton(TEXT_TEMPLATES_RESOLVE_PATTERN));
        theResourceTemplateResolver.setSuffix(".txt");
        theResourceTemplateResolver.setTemplateMode("text");
        theResourceTemplateResolver.setCharacterEncoding("UTF-8");
        theResourceTemplateResolver.setCacheable(false);
        theResourceTemplateResolver.setOrder(3);
        return theResourceTemplateResolver;
    }

    /**
     * Creates the template engine for all message templates.
     *
     * @param inTemplateResolvers Template resolver for different types of messages etc.
     * Note that any template resolvers defined elsewhere will also be included in this
     * collection.
     * @return Template engine.
     */
    @Bean
    public SpringTemplateEngine messageTemplateEngine(
        final Collection<SpringResourceTemplateResolver> inTemplateResolvers) {
        final SpringTemplateEngine theTemplateEngine = new SpringTemplateEngine();
        for (SpringResourceTemplateResolver theTemplateResolver : inTemplateResolvers) {
            theTemplateEngine.addTemplateResolver(theTemplateResolver);
        }
        return theTemplateEngine;
    }
}

Note that:

  • There are three Spring beans of the type SpringResourceTemplateResolver.
    The different template resolvers handle the three different formats; XML, JSON and text templates. The reason for having different template resolvers is that the resolvers operate in different modes and that the templates for the different formats reside in their own directories, as we saw when creating the template files earlier.
    If I were to use pure Thymeleaf without Spring, then I would use ClassLoaderTemplateResolver template resolvers in this example and I would also have to modify the configuration of the template resolvers slightly.
  • Each of the template resolvers have the same prefix.
    The prefix sets the root of the location where the templates can be found. In this case it is in the templates directory on the classpath.
  • Each of the template resolvers have different resolvable patterns configured.
    The resolvable patterns is used by the resolver to determine whether a requested template should be resolved by the resolver or passed on to the next resolver. Note that the template suffix, such as .xml and .json, is not included in the resolvable patterns.
  • Each of the template resolvers have different suffix set on them.
    Since this example uses different suffixes for the different template types, .xml, .json and .txt, the template resolver needs for a type need to be configured with the appropriate suffix.
    Thus when resolving a template, a template resolver will use the prefix + template name + suffix to find the location and name of the template which to look for. The template name is, as we saw when implementing the tests, supplied when processing a template.
  • Each of the template resolvers have a different template mode configured.
    The reason for this is using templates in different format. We could have used text template mode for all templates but I felt that using a template mode that matched the type of template file is more appropriate.
  • All template resolvers are configured not to cache templates.
    Thymeleaf allows for caching of templates and setting a cache eviction policy etc. Not caching templates allow modifications of the templates while the application is running and the modifications being picked up the next time a template is resolved. If you want to be able to modify templates while running your application then having the templates located on the classpath may not be the best option.
  • Each of the template resolvers have a different order configured.
    Template resolvers are kept in a chain ordered by the order attribute. When resolving a template, the first template resolver in the chain first gets a chance to resolver the template. If it cannot resolver the template, then the next resolver will be tried etc.
    Template resolvers that does not have an order will be executed last in the chain.<
  • There is a Spring bean named messageTemplateEngine.
    This is the template engine used by in the tests in this example to resolver message templates.
  • The messageTemplateEngine Spring bean definition takes a parameter of the type Collection<SpringResourceTemplateResolver>.
    This tells Spring to inject all the Spring beans of the type SpringResourceTemplateResolver into this bean at creation time. If there are Spring beans of this type defined elsewhere in the application, they will also be included in this collection.

Running the Tests

If we now run the tests implemented earlier, they should all pass and, among a lot of other output to the console, we should be able to see messages of the three different formats.

The JSON message should look like this:

2017-08-07 18:49:39.939  INFO 8101 --- [           main] MessagePayloadsWithThymeleafApplicationTests : {
  "conversion_rate" : {
        "from_currency" : "SEK",
        "to_currency" : "NTD"
  }
}

The XML message should look like this:

2017-08-07 18:49:40.115  INFO 8101 --- [           main] MessagePayloadsWithThymeleafApplicationTests : <soapenv:Envelope
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:web="http://www.webserviceX.NET/">
    <soapenv:Header/>
    <soapenv:Body>
        <web:ConversionRate>
            <web:FromCurrency>SEK</web:FromCurrency>
            <web:ToCurrency>NTD</web:ToCurrency>
        </web:ConversionRate>
    </soapenv:Body>
</soapenv:Envelope>

Finally, the text message should look like this:

2017-08-07 18:49:40.125  INFO 8101 --- [           main] MessagePayloadsWithThymeleafApplicationTests : ConversionRate:
    FromCurrency: SEK
    toCurrency: NTD

This concludes the example using Thymeleaf to prepare message payloads.
The example project can be found on GitHub.

Happy coding!

3 thoughts on “Preparing Message Payloads with Thymeleaf

  1. xtian

    Where did you get the “json” template mode from? It is not mentioned in the docs and is not a valid value in org.thymeleaf.templatemode.TemplateMode. Is it some undocumented feature?

    Reply
    1. Ivan Krizsan Post author

      Hello!
      You are correct, “json” is not a valid template mode. In fact, when you run the tests in my example program, you can see the following being written in the log:
      Unknown Template Mode 'json'. Must be one of: 'HTML', 'XML', 'TEXT', 'JAVASCRIPT', 'CSS', 'RAW'. Using default Template Mode 'HTML'.
      If you then take a look in the JavaDoc for the setTemplateMode method in the class AbstractConfigurableTemplateResolver, you can read the following:

      Note that this template mode also may not be applied if the template resource name ends in a known file name suffix: .html, .htm, .xhtml, .xml, .js, .json, .css, .rss, .atom, .txt.

      Thus, it is unnecessary to set the template mode to “json” since regardless of what you set it to, it will be ignored since the template resource name ends in .json (for the JSON example).
      In fact, I would argue that it is bad to attempt to set the template mode to “json”, since it is not a valid template mode and can confuse people reading my code.
      Thanks for pointing this out!
      Happy coding!

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *