Modern web applications with Jakarta EE and Tomcat: Part 5 - Setting up Jakarta MVC
10 Aug 2022 - Tobias ErdleThis article is the fifth part of the series "Modern web applications with Jakarta EE and Tomcat" and demonstrates setting up Jakarta MVC. Jakarta MVC provides an API which enables developers to implement server-side rendered frontends based on several APIs which are already present in the example application.
Topics covered during this article series
- Setting up Jakarta REST with Jersey
- Setting up Jakarta Bean Validation
- Setting up Jakarta Contexts and Dependency Injection
- Setting up Jakarta Persistence
- Setting up Jakarta MVC for server-side rendered frontends (this article)
- Content Negotiation with Jakarta REST
- Unit and integration testing
Jakarta MVC in a nutshell
Jakarta MVC specifies an API for implementing web applications following the Model-View-Controller pattern (MVC). This pattern separates the following components:
- Model: the data which has to be presented within the view
- View: the UI presented to the user
- Controller: controls Model and View. As an example, the controller loads the business data, stores it inside the model and decides which view needs to be rendered.
For an easy integration into the Jakarta EE ekosystem Jakarta MVC is built upon CDI, Jakarta REST and Bean Validation. Jakarta REST is thereby used to implement the controller layer, Bean Validation for validating the input which comes e. g. from HTML forms and CDI for loose coupling between the components and providing extensibility.
To render a view, Jakarta MVC needs a implemented ViewEngine
. The specification requires a compatible MVC implementation to provide JSP and JSF support, which is quit handy, because
JSP is available in each servlet container. Unfortunately it has a bad reputation, because using JSP with Scriptlets ignores nearly all good software development practices and leads often to unmaintainable code. But using it as a plain rendering engine with Jakarta MVC is a easy and clean way to implement software as you'll see later. Each view template needs to be stored
inside a specified directory inside the WEB-INF
folder. This default directory for this is WEB-INF/views
, but that can be changed by Jakarta REST property ViewEngine.VIEW_FOLDER
.
To declare a Jakarta REST resource as controller the resource class or methods inside it have to be annotated with @Controller
and must be handled by CDI. Also they need to return...
void
and the@View
annotation filled with the name of the template, e. g.@View("index.jsp")
- a String containing the view or redirect, e. g.
return "index.jsp"
/return "redirect:/foobar"
- a
jakarta.ws.rs.core.Response
containing the view or redirect as its entity
For more details on this topic please refer to the specification chapter about "Controllers".
Integrating Jakarta MVC
Within this chapter, Jakarta MVC is going to be integrated into the example application and a few use-cases will be explained in detail. Because there are a lot more, please refer to the example repository to have a look into the other resource methods.
As rendering engine JSP will be used, because it's included in Tomcat and enough for the example use-cases. Personally I learned to like JSP when using them with MVC because they are shipped with nearly any server, the tool support is good and using them in combination with CDI instead of scriptlets make them a powerful and maintainable choice.
Adding the necessary dependencies
To start with Jakarta MVC the required dependencies need to be added to the pom.xml
. Because JSPs are used as template engine, an JSTL implementation is added
too.
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>jakarta.mvc</groupId>
<artifactId>jakarta.mvc-api</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.krazo</groupId>
<artifactId>krazo-jersey</artifactId>
<version>2.0.2</version>
</dependency>
Jakarta MVC applications need just the jakarta.mvc:jakarta.mvc-api
and, depending on the application server / utilized Jakarta REST implementation, the Eclipse
Krazo artifact, namely org.eclipse.krazo:krazo-[jersey|resteasy]
. There was a CXF compatible version which got dropped because of CXFs sometimes really special behaviors.
With these dependencies on board, the project can now be 'converted' into a Jakarta MVC project. In a later article I'll demonstrate how an application can be an MVC application and providing machine readable APIs at the same time.
Annotate the required classes
At the beginning you need to know only one Jakarta MVC annotation, namely @Controller
. This annotation can be applied on methods or classes. In the latter case, all Jakarta REST resource methods are intepreted as Jakarta MVC resources and handled by the appropriate filters and interceptors. So in the case of the example application, all methods of the PostsResource
shall be handled by Jakarta MVC. Therefore the PostResource
gets annotated with @Controller
.
import jakarta.mvc.Controller;
@Path("/posts")
@Controller
@RequestScoped
public class PostsResource {
// ommitted for brevity....
}
Now the real work can be started, which is to implement the controller's methods.
Add UI for read-only page
Most of the time, the first page implemented in CRUD applications, like the example app, is a kind of index page related to an entity. This page shows
a table of all entities and provides sometimes a search / filter on this table. Here the index page for the /posts
resource will be added to the application.
To get a better understanding of MVC's concepts, I'll explain the implementation steps for model, view and controller in separate sections.
Implement model
Let's start with the model. The model contains all data which shall be presented in the UI, so it's a simple Java Bean most of the time. Also it can contain
routines to convert data into specific formats, e.g. convert a java.time.LocalDateTime
into a String formatted to the german date style and so on. Personally I'm a
fan of fat models which do most of the formatting, so the view keeps clean and just shows the information. This is approach is also easier to test, as testing UIs
is slow and less reliable than a Unit Test on a simple Java Bean. And, last but not least, using an abstraction between e. g. the JPA entity and the UI prevents developers
from using patterns like the Open-Session-in-View Pattern which can cause heavy performance issues because of excessive lazy loading.
Now back to the example application. To present a Post
in the UI, there needs to be a model. The example calls it PostDTO
, because in this case it's just a simple projection
of the Post
entity. In more complex cases, a name like PostPage
or PostIndexPage
may be more suitable, especially when there is a lot of logic related to one specific page
and each page gets it's own model.
ThePostDTO
of the example application contains the following attributes. Because it's just a read-only POJO, all fields are final and only getters will be available.
public class PostDTO {
private final UUID id;
private final String title;
private final String content;
private final String publishedAt;
private final List<CommentDTO> comments;
// ommitted for brevity...
}
Now that the model exists, the controller method can be implemented.
Implement controller method
The method to provide a view showing all posts is going to replace the existing GET /posts
resource implementation, which was just a placeholder for demonstration
purposes before. The following code shows what the method and a new attribute look like. The additions are explained after the listing.
@Path("/posts")
@Controller
@RequestScoped
public class PostsResource {
@Inject
Models models;
//...
@GET
@UriRef("overview")
public Response index() {
models.put("posts", postService.findAll()
.stream().map(PostDTO::new).toList());
return Response.ok("posts.jsp").build();
}
//...
}
The first addition is an new attribute Models models
which gets injected by CDI. Models
is a class from Jakarta MVC which represents a Map
containing all values which
are usable inside the view.
On top of the method, the @UriRef
annotation was added. This annotation is a helpful utility which enables a developer to generate the URI
of the corresponding method by this name via MvcContext#uri
or MvcContext#uriBuilder
, so they don't need to hard-code them. This feature can be used in controllers as well as in
views itself. Because e. g. not all extensions of Krazo are capable of handling CDI, generating URIs inside the controller and passing them to the model can be necessary.
Inside the method, the result of postService.findAll()
will be converted into a list of PostDTO
s which are put into the Model
as posts
. With the name posts
, the collection of PostDTO
s is available in the view during rendering.
To return the view the approach using the Response
class was chosen. Even though this is the most verbose solution, it enables you to control the HTTP response
fine grained, e. g. by setting the HTTP status according to the result of the business method. In this case, only the view's name and HTTP status 200 - OK
is returned.
Implement JSP view
The last step towards the first Jakarta MVC UI is to implement the view. The example keeps the default directory and so the posts.jsp
is created
inside src/main/webapp/WEB-INF/views/posts.jsp
. Please ignore the <t:layout>
tag, as this is only a helper to ensure a consistent layout over all pages. The implementation can be found in src/main/webapp/WEB-INF/tags/layout.tag
.
<!DOCTYPE html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="t" tagdir="/WEB-INF/tags" %>
<t:layout page="Posts">
<h1>All Posts</h1>
<ul>
<c:forEach items="${posts}" var="post">
<li><a href="${mvc.uriBuilder('post').build(post.id)}">${post.title}</a></li>
</c:forEach>
</ul>
<c:if test="${posts.isEmpty()}">
<p>No posts created!</p>
</c:if>
<a href="${mvc.uri('new-post')}">Create new post</a>
</t:layout>
This listing shows two MVC concepts:
- How to access the model, like it is done in
<c:forEach items="${posts}" var="post">
. The${posts}
expression accesses the data which were stored inside theModel
during controller execution. - Generating an URI instead of hard-coding it. For example, the expression
${mvc.uriBuilder('post').build(post.id)}
generates the URI for a method annotated with@UriRef("post")
and passes the ID of the list item's post into it. Assuming the${post.id}
equals12345
, the URI template/posts/{id}
will be fetched and the build results into/posts/12345
. In case you the URI path to/foobar/{id}
and leave theUriRef
the same, no change inside the view would be necessary.
With this view included, the first read-only MVC UI is ready to be used. The following section will now show how to send information to the server utilizing the HTML form
and some slightly advanced topics like CSRF protection, HTTP method overwrite and the RedirectScope
.
Add UI for form
The next step is to add a view which can receive user input and send it to the server. This section will demonstrate how to receive, validate and save the input. As before, we start with adding the model class.
Implement model
In my opinion the easiest and cleanest way to handle form input in Jakarta REST / MVC applications is to use a simple POJO. This POJO receives the data and
contains all necessary annotations for this task and validation. The form for handling Post
input is shown in the following listing.
public class PostForm {
@FormParam("title")
@NotBlank(message = "{PostDTO.title.NotBlank}")
@Size(min = 10, max = 256, message = "{PostDTO.title.Size}")
@MvcBinding
private String title;
@FormParam("content")
@NotBlank(message = "{PostDTO.content.NotBlank}")
@MvcBinding
private String content;
//...
}
The class looks nearly like the one used in the examples without Jakarta MVC but is extended with one really important annotation: @MvcBinding
. This annotation
ensures that Bean Validation is triggered in a way that the BindingResult
, which we'll see later, is filled instead of returning a plain HTTP 400 - Bad Request
response.
In case you develop an MVC application and your validation doesn't work, check the presence of this annotation first ;)
And this was everything to do on the model side. Let's have a look into the controller method.
Implement controller method
Handling form input requires two resource methods: one GET
request returning the form UI and a POST
which handles the form input. In case of valid input, the data is processed
and afterwards the resource performs a redirect to a GET
resource. This pattern is also known as POST-Redirect-GET
and ensures, that the browser doesn't 'hang' in the POST
request
which can trigger unwanted requests. If the data is invalid, the POST
resource can show the view again which leads to the situation, that the browser 'hangs' in a POST
request which
is, in this case, a wanted behavior, because we have access to the validation errors and input data of the first form submit. Getting the data can be achieved by using a RedirectScoped
bean and performing a redirect too, but comes with more complexity. Which way you use depends surely on the business case, but returning the view from the POST
method is OK in most of the situations.
So to keep things easy, the example will use the simple implementation approach without a redirect after a failed validation. But at first let's have a look into the resource methods.
@Inject
BindingResult bindingResult;
//...
@GET
@Path("new")
@UriRef("new-post")
public Response newPost() {
return Response.ok("createpost.jsp").build();
}
@POST
@UriRef("create-post")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response create(@Valid @BeanParam final PostForm form) {
if (bindingResult.isFailed()) {
models.put("titleErrors", bindingResult.getErrors("title"));
models.put("contentErrors", bindingResult.getErrors("content"));
return Response.status(Response.Status.BAD_REQUEST).entity("createpost.jsp").build();
}
final var post = postService.create(form.getTitle(), form.getContent());
final var location = URI.create("/posts/" + post.getId());
return Response.created(location).entity("redirect:/posts/" + post.getId()).build();
}
The first line shows the already mentioned BindingResult
. The BindingResult
contains all validation errors which occured in the form object on attributes annotated with @MvcBinding
and different methods to be able to handle those errors. The BindingResult
is @RequestScoped
, so it can't be accessed e. g. after a redirect.
Again, the methods are annotated with @UriRef
to get a unique identifier independent of their URI. The GET
method returns the view containing the form, so there's nothing new to learn.
More interesting is the @POST
annotated method which consumes the form input. As you can see there is a single parameter @Valid @BeanParam final PostForm form
which contains the
form input that is validated by Bean Validation. The first thing to do in such a method handling write operations is to check if there are any validation errors, which can
be achieved with BindingResult#isFailed
. In case you miss this, you'll get a warning logged by Eclipse Krazo that there are unconsumed errors. If the input validation finds errors,
this method adds them to the model in separate entries, so they can be shown at their corresponding fields inside the view, as we se later. Then the response will be built with a
HTTP Status 400 - Bad Request
and the form which shall be shown again to give the user the possibility to fix their errors. If all input is valid, the Post
will be created and
a response with status 201 - Created
, the URI to the newly created resource and the redirect to the next view will be generated. Generating the URI is not mandatory, but working with this status and the Location header is a sign of an good API as non-MVC applications like SPAs can e. g. switch the page based on this header.
Let's have a look into the required views for this backend.
Implement JSP view
Now that the backend is implemented, again the view needs to be added. The following listing shows how the form can look like.
<!DOCTYPE html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="t" tagdir="/WEB-INF/tags" %>
<t:layout page="Create post">
<h1>Create post</h1>
<form action="${mvc.uri('create-post')}" method="post">
<label for="title">Title </label> <input id="title" type="text" name="title">
<c:if test="${titleErrors != null}">
<ul>
<c:forEach items="${titleErrors}" var="msg">
<li style="color: red;">${msg.message}</li>
</c:forEach>
</ul>
</c:if>
<label for="content">Content </label> <textarea id="content" name="content"></textarea>
<c:if test="${contentErrors != null}">
<ul>
<c:forEach items="${contentErrors}" var="msg">
<li style="color: red;">${msg.message}</li>
</c:forEach>
</ul>
</c:if>
<input type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}">
<button type="submit">Create post</button>
</form>
<hr>
<a href="${mvc.uri('overview')}">Back to post</a>
</t:layout>
As in the read-only view before, this one makes heavy use of the ${mvc.uri}
and ${mvc.uriBuilder}
methods to generate maintainable URIs. Also it contains nearly
only the <form>
which sends a POST
method. Inside the form, there are standard HTML <label>
and <input>
fields, but also the lists wrapped in <c:if>
, which check
if there are errors for the corresponding field. Those errors are the ones put into the Model
in the controller. You may recognize the hidden input <input type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}">
at the bottom of the form. This one is to provide the CSRF Token used for security purposes. It will be explained later.
These were all steps necessary to create a view with write access. But showing and creating new values are not everything, what about updates or deletions? This can be done
by adding different POST
methods on URIs like /posts/{id}/delete
or /posts/{id}/update
, but there are much better alternatives provided by HTTP. Besides the two HTTP verbs
GET
and POST
, there are PUT
, PATCH
and DELETE
(we ignored HEAD
and OPTIONS
here) exactly for those use cases. Unfortunately, HTML natively supports only GET
and POST
, but Jakarta MVC and Eclipse Krazo have a solution for this problem. The form method overwrite mechanism.
Overwrite HTTP method
As mentioned above, in an complex application there are more use-cases than fetching or creating data. Data needs to be updated, replaced, deleted and so on. Therefore the HTTP verbs
PUT
, PATCH
and DELETE
are intended, but HTML doesn't support them. But because using those verbs make HTTP resources more meaningful, Jakarta MVC implements a custom mechanism which
helps to use those verbs too. How this can be done is shown in this section.
To getting started, before Jakarta MVC 2.1 (which is in release candidate state at the moment of writing) you need to set Properties.HIDDEN_METHOD_FILTER_ACTIVE
to true
inside the
Jakarta REST application. This enables a filter which checks, like the name indicates, for a hidden form field named _method
in the default configuration. This form field can contain
the before mentioned verbs and the filter will replace the original POST
with the passed verb, so the controller executes e. g. a PATCH
instead. The form for an update view can
then look like this:
<form action="${mvc.uriBuilder('update-post').build(post.id)}" method="post">
<label for="title">Title </label> <input id="title" type="text" name="title" value="${post.title}">
<c:if test="${titleErrors != null}">
<ul>
<c:forEach items="${titleErrors}" var="msg">
<li style="color: red;">${msg.message}</li>
</c:forEach>
</ul>
</c:if>
<label for="content">Content </label> <textarea id="content" name="content">${post.content}</textarea>
<c:if test="${contentErrors != null}">
<ul>
<c:forEach items="${contentErrors}" var="msg">
<li style="color: red;">${msg.message}</li>
</c:forEach>
</ul>
</c:if>
<input type="hidden" name="_method" value="PATCH">
<input type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}">
<button type="submit">Update post</button>
</form>
As you can see in the first line, the <form>
method is set to post
, so we are using HTML standards. At the bottom there is an additional hidden field named _method
, which
contains PATCH
as its value. This, and the config which needs to be done before, leads to the execution of the corresponding PATCH
resource inside the controller class.
CSRF protection
As we noticed in the form examples before, there is always a hidden input field named ${mvc.csrf.name}
containing a value ${mvc.csrf.token}
. This field is
required to use the CSRF protection in case no HTTP Header is used for this task. Jakarta MVC defines three different
modes for CSRF protection:
CsrfOptions.OFF
: There won't be any CSRF protection (not recommended)CsrfOptions.EXPLICIT
: Only resource methods annotated with@CsrfProtected
are evaluated. Default by specification.CsrfOptions.IMPLICIT
: Any request on a HTTP verb indicating write actions gets evaluated
Inside the Jakarta REST application, the example application sets the Csrf.CSRF_PROTECTION
property to implicit, so the resources are required
to be protected by default and not another annotation is placed on top of the resources. This is the reason, only the hidden input fields are inside the
forms.
Because this topic is worth an article itself, I'll stop here for now and will demonstrate the last topic of this article: the RedirectScoped
beans.
Redirect scoped beans
Sometimes you want to transfer data between the origin and the target of a redirect, often things like success or error messages. Jakarta MVC defines the redirect scope for this kind of task which is an additional CDI scope. To use this feature, you need to create a POJO annotated with @RedirectScoped
and let this class implement Serializable
, because @RedirectScoped
is a passivating scope. This means that the data is serialized between requests and stored for example on the hard drive of the server. This way it can be read even after the redirect. The example application implements this use-case to demonstrate the redirect scope: In the GET /post/{id}
view, which displays a Post
with its Comment
s, there is an embedded form to add a comment. Because the POST
targets a subresource of /post/{id}
, returning the view requires some additonal effort which I don't want to spend. So in case the validation of the form fails, the CommentFormErrors
bean will be filled and extracted inside the controller method of GET /post/{id}
. This listing shows the @RedirectScoped
POJO:
@RedirectScoped
public class CommentFormErrors implements Serializable {
private Set<ParamError> authorErrors;
private Set<ParamError> contentErrors;
//...
}
As it is a simple CDI bean, it will be injected into the controller class and written / read by the according methods:
@GET
@UriRef("post")
@Path("{id}")
public Response show(@PathParam("id") final UUID id) {
final var post = postService.findById(id);
return post.map(p -> {
//..
// ---> READ THE REDIRECTED VALUES <---
if (commentFormErrors.containsErrors()) {
models.put("authorErrors", commentFormErrors.getAuthorErrors());
models.put("contentErrors", commentFormErrors.getContentErrors());
}
//...
}).orElseGet(() -> Response.status(Response.Status.NOT_FOUND).entity("404.jsp").build());
}
@POST
@UriRef("create-comment")
@Path("{id}/comments")
public Response commentsResource(@PathParam("id") final UUID postId, @Valid @BeanParam CommentForm dto) {
if (bindingResult.isFailed()) {
// ---> WRITE THE REDIRECTED VALUES <---
commentFormErrors.setAuthorErrors(bindingResult.getErrors("author"));
commentFormErrors.setContentErrors(bindingResult.getErrors("content"));
return Response.status(Response.Status.BAD_REQUEST).entity("redirect:/posts/" + postId).build();
}
//...
}
This way, the form errors can be shown even after the redirect happened. Last but not least I'll show a nice config option helping to configure the redirect scope behavior.
Per default, the redirect scoped bean's ID is set into the URI as query parameter, which is something like /posts/1234?org.eclipse.krazo.redirect.param.ScopeId=[some UUID]
.
This looks not nice, but can be configured to have a shorter named query param via Properties.REDIRECT_SCOPE_QUERY_PARAM_NAME
. Also, if you want to rely on cookies, you can set
Properties.REDIRECT_SCOPE_COOKIES
to true
inside the Jakarta REST application, so the ID is stored in a Cookie called org.eclipse.krazo.redirect.Cookie
instead of the URI. The name of this Cookie can be configured with Properties.REDIRECT_SCOPE_COOKIE_NAME
. But be aware of the fact that suppresing all Cookies in the browser may break your application.
Testing the application manually
To avoid to upload large images and cost a lot of bandwith (what I want to avoid on the whole blog), I'll just describe how to test the application. Build the war
with Maven and
deploy it to your Tomcat server OR run mvn -Ptomcat package org.codehaus.cargo:cargo-maven3-plugin:run
to start a Tomcat downloaded and managed by the Maven Cargo Plugin. Then navigate to the URI http://localhost:8080/posts
and you should see a small UI stating that there are no posts. Then you can navigate through all views, create, edit and delete posts and so on. If everything works: perfect! If not: please don't hesitate to write me an E-Mail or create an issue in the GitHub repository.
Conclusion
This article shows how Jakarta MVC can be integrated in a web application to provide the capability to implement server-side rendered UIs based on Jakarta REST and Jersey. It demonstrates that Jakarta MVC is easy to integrate and highly configurable, so simple or complex applications can be implemented with commonly known Jakarta EE specifications. Unfortunately, Jakarta MVC isn't in the Jakarta EE Platform at the moment, but when there is more adoption by the community, the specification may be added to it in the future. Nevertheless, Jakarta MVC and Eclipse Krazo are actively developed and will provide new features in the next releases.
Upcoming topic: Content Negotiation with Jakarta REST
Now that there is an UI instead of the really primitive resources before, only humans or screen scrapers can work with the HTTP APIs. To enable humans as well as machines to work with the data in their prefered formats, the next article will cover Content Negotiation.