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 6 - Content Negotiation

This article is the sixt part of the series "Modern web applications with Jakarta EE and Tomcat" and demonstrates Content Negotiation in an application using Jakarta RESTful Web Services and Jakarta MVC. This example will demonstrate how this application can provide an HTML UI and JSON API just using different HTTP content types.

Topics covered during this article series

Attention: Dependency upgrade for Jakarta MVC and Eclipse Krazo

Because the introduced version of Eclipse Krazo (2.x) contains an incomplete handling for content negotiation, this post's code upgrades Jakarta MVC to version 2.1.0 and Krazo to 3.0.1. In version 2.0.2 of Eclipse Krazo this behavior will be fixed.

What is content negotiation

According to MDN, In HTTP, content negotiation is the mechanism that is used for serving different representations of a resource to the same URI to help the user agent specify which representation is best suited for the user.

This means, that by setting the accepted content type in the user agent, which is e. g. your application or browser, the server provides the requested information in the requested format when calling the same URI. In case this format can't be provided, the server may respond with HTTP status 415 - Unsupported Media Type.

For example, an user agent calls the URI http://myexampleapp/posts and accepts text/html as content type. The server can handle this type and responds with a HTML representation of the information. This could look like this:

<html>
  <body>
    <ul>
      <li>First post</li>
      <li>Second post</li>
      <li>Third post</li>
    </ul>
  </body>
</html>

Now another user agent, e. g. a blogging feed, wants to read the same information in a machine readable format. In this case it's application/json, which can be handled by the example application too. So the blogging feed would get somthing like this:

[
  {"title": "First post"},
  {"title": "Second post"},
  {"title": "Third post"}
]

Adding the necessary dependencies

Before get our hands dirty, let's add a few dependencies to enable JSON parsing inside the later programmed API. We'll add Jakarta JSON Binding (JSON-B) and the JSON Binding for Jersey.

<dependency>
    <groupId>jakarta.json.bind</groupId>
    <artifactId>jakarta.json.bind-api</artifactId>
    <version>3.0.0</version>
</dependency>

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-binding</artifactId>
    <version>3.0.4</version>
</dependency>

Those dependencies enable you to bind JSON via annotations to objects and handle JSON automatically inside Jersey resources.

Implementing Content Negotiation with Jersey

Implementing the handling of request and response content types is straight forward in Jersey. Jakarta REST defines the jakarta.ws.rs.Produces for the response and jakarta.ws.rs.Consumes annotation for the request content. The possible media types are defined in jakarta.ws.rs.core.MediaType. In the next sections we'll explore how to migrate the existing MVC application in a JSON and HTML supporting one.

Split MVC and other models

The first step we'll take is to split up the existing model into a MVC view model and a JSON model. The separation isn't mandatory, but I like to have different models for different use-cases. The MVC API will keep the existing DTOs, but the will be moved into the de.erdlet.blogging.blog.api.mvc package. Because the JSON model has no need for e. g. form representations, there will be a package de.erdlet.blogging.blog.api.nonmvc which contains only the JsonPostDTO and JsonCommentDTO. Those will be annotated with JSON-B annotations and look like the following listings:

JsonPostDTO:

public class JsonPostDTO {
    @JsonbProperty("post_id")
    private UUID id;

    @JsonbProperty("post_title")
    @NotBlank(message = "{JsonPost.title.NotBlank}")
    @Size(min = 10, max = 256, message = "{JsonPost.title.Size}")
    private String title;

    @JsonbProperty("post_content")
    @JsonbNillable(false)
    @NotBlank(message = "{JsonPost.content.NotBlank}")
    private String content;

    @JsonbProperty("post_publish_date")
    private String publishedAt;

    @JsonbProperty("post_comments")
    private List<JsonCommentDTO> comments;

  // ...
}

JsonCommentDTO:

public class JsonCommentDTO {

    @JsonbProperty("comment_id")
    private UUID id;

    @JsonbProperty("comment_author")
    @NotBlank(message = "{JsonComment.author.NotBlank}")
    @Size(min = 2, max = 256, message = "{JsonComment.author.Size}")
    private String author;

    @JsonbProperty("comment_content")
    @NotBlank(message = "{JsonComment.content.NotBlank}")
    @Size(min = 10, max = 256, message = "{JsonComment.content.Size}")
    private String content;

    @JsonbProperty("comment_publish_date")
    private String publishedAt;

  //...
}

Implement resources for other content types

The next step is to implement the resources which handle the different content types. To keep the controller code small, I extracted the code into the PostsResourceMvcDelegate and PostsResourceNonMvcDelegate. Those receive the already serialized input data and return Jakarta REST Responses. Now let's have a look into the GET /posts URI:

@Path("/posts")
@RequestScoped
public class PostsResource {

    @GET
    @Controller
    @UriRef("overview")
    @Produces(MediaType.TEXT_HTML)
    public Response indexMvc() {
        return mvcDelegate.handleIndex();
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response indexApi() {
        return nonMvcDelegate.handleIndex();
    }

  //...
}

You will recognize a few things here.

  1. The @Controller annotation has been moved from the class onto the MVC method. This is required to prevent the MVC implementation to handle all requests. Now only the annotated methods will be handled by MVC.
  2. The @Controller annotated method (and all others too) got a @Produces(MediaType.TEXT_HTML) annotation. This makes the content type more explicit but could be omitted since MVC sets this content type automatically.
  3. The indexApi method is new to the resource and is annotated with `@Produces(MediaType.APPLICATION_JSON). This tells the Jakarta REST implementation to serialize the response body into JSON.

Since those are only GET methods without values passed as parameters, let's check a POST method too:

@Path("/posts")
@RequestScoped
public class PostsResource {

  //...

    @POST
    @Controller
    @UriRef("create-post")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response createMvc(@Valid @BeanParam final PostForm form) {
        return mvcDelegate.handleCreate(form);
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response createApi(@Valid final JsonPostDTO dto) {
        return nonMvcDelegate.handleCreate(dto);
    }

  //...
}

Here you'll recognize a few things too:

  1. Here the createMvc method has been annotated with @Controller too, but doesn't produce HTML. This is due to the fact, that the POST method returns a redirect which has no content.
  2. The createMvc method consumes MediaType.APPLICATION_FORM_URLENCODED. This is the default format submitted by HTML forms and required to route the request to the correct resource.
  3. The new createApi method consumes JSON and automatically serializes it into the JsonPostDTO. Because its response doesn't return a body, it's not annotated with @Produces too.

In fact, that's all you need to know to get started with Content negotiation in Jakarta RESTful Web Services and Jersey. As always, let's test this in the upcoming section.

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. Inside the UI, you should be able to perform all actions successfully.

With the following requests, you can test the application via the JSON API:

### Get all posts with their comments
GET http://localhost:8080/posts
Accept: application/json

### Create a post
POST http://localhost:8080/posts
Content-Type: application/json
Accept: application/json

{
  "title":"My API produced blog",
  "post_content":"This is produced by the API"
}

### Get all posts with a little bit of example data
http://localhost:8080/posts

HTTP/1.1 200 
Content-Type: application/json
Content-Length: 770
Date: Mon, 15 Aug 2022 10:48:47 GMT
Keep-Alive: timeout=20
Connection: keep-alive

[
  {
    "post_comments": [
      {
        "comment_author": "John Doe",
        "comment_content": "Cool thing! Thanks a lot!",
        "comment_id": "8959dba0-8a3c-46b0-b486-6a0f0f29510e",
        "comment_publish_date": "15.08.2022"
      }
    ],
    "post_content": "This is also written in MVC. Really cool!\r\n\r\nEdit: This works too!",
    "post_id": "cdb22232-12a3-4b37-9630-27b79bab590a",
    "post_publish_date": "15.08.2022",
    "post_title": "My second MVC post!"
  },
  {
    "post_comments": [
      {
        "comment_author": "Tobias Erdle",
        "comment_content": "Yey! It works like a charm. Written via MVC UI!",
        "comment_id": "0e36f057-feaf-4371-bc91-072882932730",
        "comment_publish_date": "15.08.2022"
      }
    ],
    "post_content": "This is created by using the MVC UI!",
    "post_id": "1fb9c885-f26d-4d83-9940-33673f2823ae",
    "post_publish_date": "15.08.2022",
    "post_title": "My first MVC post!"
  }
]

#######################################################################################

Conclusion

This post shows how simple it is to implement content negotiation with Jakarta RESTful Web Services and Jersey. This HTTP concept will help you to implement complex applications which can serve people as well as other services without creating hurdles regarding preferred content types. Sure, this post has only covered the easiest use cases, but it'll give you a good introduction to dig into more complex problems.

Upcoming topic: Unit and integration testing the application

The next, and for this blog series, last post will cover testing the example application from different points of view. It'll cover simple unit-testing and integration testing, so we should be able to ensure functionality through all layers.

Resources