Tobias Erdle's Blog

Development Engineer working with different technologies. Former Jakarta MVC & Eclipse Krazo Committer. All views are my own.

Github: erdlet | E-Mail: blog (at) erdlet (punkt) de

Modern web applications with Jakarta EE and Tomcat: Part 5 - Setting up Jakarta MVC

This 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

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:

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...

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 PostDTOs which are put into the Model as posts. With the name posts, the collection of PostDTOs 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:

  1. 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 the Model during controller execution.
  2. 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} equals 12345, 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 the UriRef 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:

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 Comments, 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.

Resources