DEV Community

Thomas
Thomas

Posted on • Originally published at bootify.io

Integrate jte with Spring Boot

Jte is a modern template engine for Spring Boot that stands out for its minimalist syntax. What steps are necessary to integrate jte into our application and make it ready for use in the real world?

The current sample code for the setup described is available here.

Application configuration

The starting point is a Spring Boot application based on Maven without a frontend. We can easily create this using Bootify with start project. We could already choose jte as the frontend, however we would like to manually review the exact steps involved.

First, we expand our pom.xml with the following points.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- ... -->
    <dependencies>
        <dependency>
            <groupId>gg.jte</groupId>
            <artifactId>jte-spring-boot-starter-3</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>gg.jte</groupId>
            <artifactId>jte</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>5.3.7</version>
        </dependency>
        <!-- ... -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <profiles>
                        <profile>local</profile>
                    </profiles>
                </configuration>
            </plugin>
            <plugin>
                <groupId>gg.jte</groupId>
                <artifactId>jte-maven-plugin</artifactId>
                <version>3.2.1</version>
                <configuration>
                    <sourceDirectory>${project.basedir}/src/main/jte</sourceDirectory>
                    <targetDirectory>${project.build.directory}/classes</targetDirectory>
                    <contentType>Html</contentType>
                </configuration>
                <executions>
                    <execution>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>precompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
Enter fullscreen mode Exit fullscreen mode

  New dependencies and plugin config for jte

This adds jte, Bootstrap as a WebJar and a Spring Boot starter that expects all templates to be located in src/main/jte. To ensure that these are recompiled during development after every change, we add an application-local.properties file with the following entries to our project.

gg.jte.usePrecompiledTemplates=false
gg.jte.developmentMode=true
Enter fullscreen mode Exit fullscreen mode

During development, the local profile should be active so that these settings are used. We extend the existing application.properties with the following setting.

gg.jte.usePrecompiledTemplates=true
Enter fullscreen mode Exit fullscreen mode

Precompiled templates are expected in the classpath with this setting - this is already handled by our configured plugin, which stores all artifacts in target/classes so that they are integrated into our fat jar.

We need a helper to make further required functions available in our templates. To avoid having to define and pass an object as a parameter everywhere, we use a helper class with static methods.

public class JteContext {

    private static final LocalizationSupport localizer = WebUtils::getMessage;
    private static final ThreadLocal<ModelMap> model = new ThreadLocal<>();

    public static void init(final ModelMap model) {
        JteContext.model.set(model);
    }

    public static void reset() {
        JteContext.model.remove();
    }

    public static Content localize(final String key, final Object... params) {
        return localizer.localize(key, params);
    }

    public static ModelMap getModel() {
        return model.get();
    }

    public static String getMsgSuccess() {
        return ((String)model.get().getAttribute(WebUtils.MSG_SUCCESS));
    }

    public static String getMsgInfo() {
        return ((String)model.get().getAttribute(WebUtils.MSG_INFO));
    }

    public static String getMsgError() {
        return ((String)model.get().getAttribute(WebUtils.MSG_ERROR));
    }

    public static String getRequestUri() {
        return WebUtils.getRequest().getRequestURI();
    }

}
Enter fullscreen mode Exit fullscreen mode

  Helper methods for our templates

The LocalizationSupport is the recommended interface for internationalization. An interceptor is responsible for setting and removing the model for the duration of a request.

@Component
public class JteContextInterceptor implements HandlerInterceptor {

    @Override
    public void postHandle(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler, final ModelAndView modelAndView) {
        if (modelAndView != null) {
            JteContext.init(modelAndView.getModelMap());
        }
    }

    @Override
    public void afterCompletion(final HttpServletRequest request,
            final HttpServletResponse response, final Object handler, final Exception ex) {
        JteContext.reset();
    }

}
Enter fullscreen mode Exit fullscreen mode

This allows us to use all defined functions in our templates using @import static io.bootify.jte.util.JteContext.* and extend them as needed.

Layout and first pages

Real enterprise applications usually require a number of features that we want to integrate into our application. Firstly we want to add a layout.jte. In abbreviated form, it looks like this - including a parameter content, which we use to integrate the actual content of a template.

@import static io.bootify.jte.util.JteContext.*

@param gg.jte.Content pageTitle
@param gg.jte.Content content


<!DOCTYPE html>
<html lang="en">
    <head>
        <title>${pageTitle} - ${localize("app.title")}</title>
        <link href="/webjars/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet" />
        <script src="/webjars/bootstrap/5.3.7/js/bootstrap.bundle.min.js" defer></script>
    </head>
    <body>
        <header class="bg-light">
            <div class="container">
                <nav class="navbar navbar-light navbar-expand-md">
                    <a href="/" class="navbar-brand">
                        <img src="/images/logo.png" alt="${localize("app.title")}" width="30" height="30" class="d-inline-block align-top" />
                        <span class="ps-1">${localize("app.title")}</span>
                    </a>
                </nav>
            </div>
        </header>
        <main class="my-5">
            <div class="container">
                @if (getMsgSuccess() != null)
                    <p class="alert alert-success mb-4" role="alert">${getMsgSuccess()}</p>
                @elseif (getMsgInfo() != null)
                    <p class="alert alert-info mb-4" role="alert">${getMsgInfo()}</p>
                @elseif (getMsgError() != null)
                    <p class="alert alert-danger mb-4" role="alert">${getMsgError()}</p>
                @endif
                ${content}
            </div>
        </main>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

  A simple jte layout

This allows us to continue with the actual pages. A file src/main/jte/error.jte is automatically picked up by Spring Boot for error pages.

@import static io.bootify.jte.util.JteContext.*

@param Integer status
@param String error


@template.layout(pageTitle=localize("error.page.headline", status, error), content=@`
    <h1 class="mb-4">${status} - ${error}</h1>
    <p>${localize("error.page.message")}</p>
`)
Enter fullscreen mode Exit fullscreen mode

  A simple error page

Here, we are using our prepared layout directly - the principle is clear. A homepage could look like this.

@import static io.bootify.jte.util.JteContext.*


@template.layout(pageTitle=localize("home.index.headline"), content=@`
    <h1 class="mb-4">${localize("home.index.headline")}</h1>
    <p class="mb-5">${localize("home.index.text")}</p>
    <div class="col-md-4 mb-5">
        <h4 class="mb-3">${localize("home.index.exploreEntities")}</h4>
        <div class="list-group">
            <a href="/products" class="list-group-item list-group-item-action">${localize("product.list.headline")}</a>
        </div>
    </div>
`)
Enter fullscreen mode Exit fullscreen mode

We have already completed all necessary steps so that our application compiles and displays jte templates.

More complex setups

The next step is to make our application support forms based on Spring MVC. There are no direct functions from jte for this - instead, we can extend our JteContext class with practical helpers.

public class JteContext {

    // ...

    public static BindingResult getBindingResult(final String object) {
        final BindingResult bindingResult = ((BindingResult)model.get().getAttribute(
                "org.springframework.validation.BindingResult." + object));
        if (bindingResult == null) {
            return emptyBindingResult;
        }
        return bindingResult;
    }

    public static String getFieldValue(final String object, final String field) {
        return ((String)getBindingResult(object).getFieldValue(field));
    }

    @SuppressWarnings("unchecked")
    public static <T> T getFieldValue(final String object, final String field,
            final Class<T> type) {
        final Object dto = model.get().getAttribute(object);
        if (dto == null) {
            return null;
        }
        try {
            final Field classField = dto.getClass().getDeclaredField(field);
            classField.setAccessible(true);
            return ((T)classField.get(dto));
        } catch (final Exception ex) {
            throw new RuntimeException(ex);
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

  Extensions for form handling

The second variant of getFieldValue is needed to obtain the value with its actual type. Jte templates are always compiled into real Java code, so casting may be necessary. Additional functions such as isAuthenticated() for Spring Security can be added to JteContext as needed.

Complete CRUD functionality and an inputRow.jte template can be seen in the Spring Boot jte example.

Jte's minimalist approach is very appealing, especially if you are used to the complexity of Thymeleaf. Although you have to write your own helpers for some important features, the code is very easy to understand and extend.

» Learn more

Top comments (0)