Thymeleaf With JavaEE 8
Learn how to configure the open source tool Thymeleaf as a view technology with JavaEE 8.
Join the DZone community and get the full member experience.
Join For FreeBackground
In our organization, (and so as for many others, I believe), Apache Tiles had been a go to framework for rendering template based views. And it worked great with Spring. In recent years, thymeleaf has evolved as a mature and feature rich view technology.
Few weeks ago, I started evaluating Thymeleaf as a replacement for Apache Tiles and loved it from the start. Although there are several articles for Thymeleaf + Spring, there aren't many for JavaEE; thus decided to write one myself, for those who are interested.
This article aims to provide a brief introduction about configuring Thymeleaf as a view and templating technology with MVC 1.0 specified in JavaEE 8.
The Groundwork
web.xml
Here is the simple web.xml. Since we will be using annotation based configuration, there is not much to include here.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
</web-app>
beans.xml
There is nothing much to this file either, except bean-discovery-mode is set to 'all'. This will ensure that application server configures all the annotated beans.
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>
Libraries
We would primarily need libraries for MVC 1.0 specification and its reference implementation along with Thymeleaf core ones. Please find links to these libraries in the reference section at the end of the article.
Setting up everything ...
Application Setup
First step is to configure our JAX-RS application. Create an Application class (or any other one as you like) annotated with @ApplicationPath. The argument to this annotation will become the application path after deployment.
@ApplicationPath("api")
public class Application extends javax.ws.rs.core.Application
{
private static final Logger logger = LogManager.getLogger(Application.class);
}
Controller Setup
If you are familiar with JAX-RS, setting up MVC 1.0 controller won't be much of a revelation. There are couple of things to notice -
Container can inject models (similar to other resources). Controller methods can set the model attributes similar to that of the HashMap
Annotate your class with @Controller. This annotation is provided by the MVC 1.0 specification which tells container to treat this class as a MVC controller.
Following is the code for a Controller for serving a login page -
@Path("login")
@Controller
public class LoginController
{
@Inject
Models models;
private static final Logger logger = LogManager.getLogger(LoginController.class);
@GET
@Produces(MediaType.TEXT_HTML)
public String viewLogin()
{
models.put("company", "Acme Products");
models.put("product", "JavaEE 8 Bootstrap");
return "login";
}
}
viewLogin method returns a string "login", indicating that a view named "login" is to be served.
Thymeleaf Setup
Following code shows how to configure Thymeleaf for serving the content based on the "view" returned from the controller.
@ApplicationScoped
public class ThymeleafViewEngine
extends ViewEngineBase
{
@Inject
private ServletContext servletContext;
private TemplateEngine engine;
@Override
public boolean supports(String view)
{
return !view.contains(".");
}
@PostConstruct
public void postConstruct()
{
TemplateResolver resolver = new ServletContextTemplateResolver();
resolver.setPrefix("/WEB-INF/layouts/");
resolver.setSuffix(".xhtml");
resolver.setTemplateMode(StandardTemplateModeHandlers.XHTML.getTemplateModeName());
resolver.setCacheable(false);
engine = new TemplateEngine();
engine.setTemplateResolver(resolver);
}
@Override
public void processView(ViewEngineContext context) throws ViewEngineException
{
try
{
HttpServletRequest request = context.getRequest();
HttpServletResponse response = context.getResponse();
WebContext ctx = new WebContext(request, response, servletContext, request.getLocale());
ctx.setVariables(context.getModels());
request.setAttribute("view", context.getView());
engine.process(context.getView().equals("login") ? "login-template":"default-template", ctx, response.getWriter());
}
catch (IOException e)
{
throw new ViewEngineException(e);
}
}
}
Here is what the above code does -
The class extends ViewEngineBase and is annotated as @ApplicationScoped. This enables this class to be treated as MVC ViewEngine and used when a controller returns a view.
- postConstruct() method initializes Thymeleaf template engine with template directory and template mode is set as HTML5
- For production environment, resolver.setCacheable(true) is recommended.
- processView() method is called when a controller returns a view, in this case "login" view.
- Thymeleaf templates can't access MVC model directly, thus the model is copied to the Thymeleaf context using ctx.setVariables(context.getModels());
- In case of Apache Tiles, single view represents a template and its sub components using a XML configuration. To achieve this with Thymeleaf, the view returned by the controller is used to set the template to be used. Thus with context.getView().equals("login") ? "login-template" : "default-template", we configure login-template for "login" view and default-template for rest of the views.
- We then set view name as a request attribute so that we can tell the template where to look for its view specific content components.
Setting up the view template
Following code is a typical template for rendering a view. The template used scoped variable ${view} to substitude the value of "view" in the template.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Bootstrap</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!-- Favicon -->
<link rel="icon" type="image/x-icon" th:href="@{/images/favicon.ico}" />
<!-- Common styles -->
<link rel="stylesheet" type="text/css" th:href="@{/styles/common.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/styles/form.css}" />
<!-- View specific styles -->
<link rel="stylesheet" type="text/css" th:href="@{/styles/login-layout.css}" />
<th:block th:include="${view} :: styles" />
<!-- Common scripts -->
<script type="text/javascript" th:src="@{/scripts/jquery-2.1.3.min.js}"></script>
<script type="text/javascript" th:src="@{/scripts/velocity.min.js}"></script>
<script type="text/javascript" th:src="@{/scripts/commonts.js}"></script>
<script type="text/javascript" th:src="@{/scripts/form.js}"></script>
<!-- View specific scripts -->
<script type="text/javascript" th:src="@{/scripts/login.js}"></script>
<th:block th:include="${view} :: scripts" />
</head>
<body>
<div class="body">
<div class="page">
<div class="branding">
<div class="company-branding" th:text="${company}">Some Default Company</div>
<div class="product-branding" th:text="${product}">Bootstrap</div>
</div>
<div th:replace="${view} :: page">
</div>
</div>
</div>
</body>
</html>
The directory structure for web content is as below -
Setting up the "view"
Login view (login.xhtml) specifies various content components which will be included by the template while rendering full page.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="styles">
</head>
<head th:fragment="scripts">
</head>
<body th:fragment="page">
<form class="login-form" name="login-form" method="post" th:action="@{/login.json}">
<h2 class="form-header">Customer Login</h2>
<div class="form-body">
<div class="field-wrapper">
<label><input type="text" name="user-name" value="" placeholder="User Name" autocomplete="off" /></label>
</div>
<div class="field-wrapper">
<label><span /> <input type="password" name="password" value="" placeholder="Password" autocomplete="off" /></label>
</div>
</div>
<div class="form-options">
<a th:href="@{/forgot-password}">Forgot Password</a>
<a th:href="@{/sign-up}">Sign Up</a>
</div>
<div class="form-footer">
<input type="submit" value="Login" />
<input type="reset" value="Cancel" />
</div>
</form>
</body>
</html>
Deployment
We will deploy our application on glassfish as it is the only application server that supports the MVC 1.0 draft specification and comes bundled with its reference implementation. Once the application is deployed, go to http://localhost:8080/JavaEE/api/login for viewing the login page (assuming your application uses JavaEE as context path).
Suggestions ?
Please leave a comment for any suggestion.
Source Code
Please visit https://github.com/kedar-joshi/Thymeleaf-JavaEE-MVC for the sources
References
For basic project structure https://dzone.com/articles/java-ee-8-mvc-getting-started-with-ozark
For JavaEE 8 reference https://javaee8.zeef.com/arjan.tijms
For MVC 1.0 basics http://www.bennet-schulz.com/2015/10/javaee-mvc-controllers.html and http://www.mscharhag.com/java-ee-mvc/a-detailed-look-on-mvc-controllers
MVC 1.0 reference homepage https://ozark.java.net/
For Thymeleaf download and documentation http://www.thymeleaf.org/download.html and http://www.thymeleaf.org/doc/articles/layouts.html
For Thyemleaf custom layouts http://blog.codeleak.pl/2013/11/thymeleaf-template-layouts-in-spring.html
Opinions expressed by DZone contributors are their own.
Comments