Mapping HTTP content types

With the same mindset of giving developers as much productivity as possible, Java EE includes standards to transparently map POJOs to JSON or XML. The example you saw with JAX-RS implicitly used JSON-B to map our User types to JSON objects and arrays, respectively.

This again uses the principle of convention over configuration. If nothing is else specified JSON-B assumes to map the POJO properties directly as JSON object key-value pairs. The user's id was present in the JSON output as well.

The same holds true for the Java Architecture for XML Binding (JAXB) and its XML binding, which was included in Java EE much earlier than JSON-B. Both standards support a declarative configuration approach using annotations that are placed on the mapped Java types. If we're about to change the JSON representation of the type, we annotate the corresponding fields:

import javax.json.bind.annotation.JsonbProperty;
import javax.json.bind.annotation.JsonbTransient;

public class User {

    @JsonbTransient
    private long id;

    @JsonbProperty("username")
    private String name;

    ...
}

If we want to implement more sophisticated resource mapping, such as in the Hypermedia book examples shown before, we can do so using the declarative mapping approach. For instance, to map the links into the books resource, we can use a map containing links and link relations:

public class Book {

    @JsonbTransient
    private long id;

    private String name;
    private String author;
    private String isbn;

    @JsonbProperty("_links")
    private Map<String, URI> links;

    ...
}

These links are set in the JAX-RS resource appropriately:

@Path("books")
@Produces(MediaType.APPLICATION_JSON)
public class BooksResource {

    @Inject
    BookStore bookStore;

    @Context
    UriInfo uriInfo;

    @GET
    public List<Book> getBooks() {
        List<Book> books = bookStore.getBooks();
        books.forEach(this::addLinks);
        return books;
    }

    @GET
    @Path("{id}")
    public Book getBook(@PathParam("id") long id) {
        Book book = bookStore.getBook(id);
        addLinks(book);
        return book;
    }

    private void addLinks(Book book) {
        URI selfUri = uriInfo.getBaseUriBuilder()
                .path(BooksResource.class)
                .path(BooksResource.class, "getBook")
                .build(book.getId());

        book.getLinks().put("self", selfUri);
        // other links
    }
}

The output of the list of books will look similar to the following:

[
    {
         "name": "Java",
         "author": "Duke",
         "isbn": "123-2-34-456789-0",
         "_links": {
             "self": "https://api.example.com/books/12345",
             "author": "https://api.example.com/authors/2345",
             "related-books": "https://api.example.com/books/12345/related"
        }
    },
    ...
]

Using this approach, we can now programmatically introduce links with relations that are used and being followed within the client. However, using a Hypermedia approach pretty quickly reaches the point where a declarative mapping introduces too much overhead on the model. The map of links and relations already is not part of the business domain, but a technical necessity and should therefore be questioned. We could introduce transfer object types that separate the technical mapping from the domain model. But this would certainly introduce a lot of duplication and clutter our project with a number of classes that serve no value to the business.

Another challenge to be faced is the flexibility that Hypermedia requires. Even for simpler examples that make use of Hypermedia controls, we want to specify and include links and actions depending on the current state of the system. It's in the nature of Hypermedia to control the flow of clients and direct them to certain resources. For example, a client response should only include the action to place an order if a book is in stock or certain credit is on their account. This requires the response mapping to be changeable on demand. Since a declarative mapping can't be changed easily at runtime, we would need a more flexible approach.

Since Java EE 7, there is the Java API for JSON Processing (JSON-P) standard which provides programmatic mapping of JSON structures in a builder pattern-like fashion. We can simply invoke the builder types JsonObjectBuilder or JsonArrayBuilder to create arbitrary complex structures:

import javax.json.Json;
import javax.json.JsonObject;
...

JsonObject object = Json.createObjectBuilder()
    .add("hello", Json.createArrayBuilder()
        .add("hello")
        .build())
    .add("key", "value")
    .build();

The resulting JSON object looks as follows:

{
    "hello": [
        "hello"
    ],
    "key": "value"
}

Especially in situations where we need a lot of flexibility such as in Hypermedia this approach is quite helpful. The JSON-P standard, as well as JSON-B or JAXB, seamlessly integrates with JAX-RS. JAX-RS resource methods that return JSON-P types, such as JsonObject, will automatically return the JSON content type together with the corresponding response. No further configuration is required. Let's have a look how the example containing resource links is implemented using JSON-P.

import javax.json.JsonArray;
import javax.json.stream.JsonCollectors;

@Path("books")
public class BooksResource {

    @Inject
    BookStore bookStore;

    @Context
    UriInfo uriInfo;

    @GET
    public JsonArray getBooks() {
        return bookStore.getBooks().stream()
                .map(this::buildBookJson)
                .collect(JsonCollectors.toJsonArray());
    }

    @GET
    @Path("{id}")
    public JsonObject getBook(@PathParam("id") long id) {
        Book book = bookStore.getBook(id);
        return buildBookJson(book);
    }

    private JsonObject buildBookJson(Book book) {
        URI selfUri = uriInfo.getBaseUriBuilder()
                .path(BooksResource.class)
                .path(BooksResource.class, "getBook")
                .build(book.getId());

        URI authorUri = ...

        return Json.createObjectBuilder()
                .add("name", book.getName())
                .add("author", book.getName())
                .add("isbn", book.getName())
                .add("_links", Json.createObjectBuilder()
                        .add("self", selfUri.toString())
                        .add("author", authorUri.toString()))
                .build();
    }
}

The JSON-P objects are created dynamically using a builder pattern approach. We have full flexibility over the desired output. This approach of using JSON-P is also advisable if a communication needs a representation of an entity different to the current model. In the past, projects always introduced transfer objects or DTOs for this purpose. Here the JSON-P objects are in fact transfer objects. By using this approach, we eliminate the need for another class that also duplicates the majority of structures of the model entity.

However, there is also some duplication in this example. The property names of the resulting JSON objects are now provided by strings. To refactor that example a little bit, we would introduce a single point of responsibility, such as a managed bean responsible for creating the JSON-P objects from the model entities.

This bean, for example EntityBuilder, would be injected in this and other JAX-RS resource classes. Then the duplication is still existent, but encapsulated in that single point of responsibility and reused from multiple resource classes. The following code shows an example EntityBuilder for books and potentially other objects to be mapped to JSON.

public class EntityBuilder {

    public JsonObject buildForBook(Book book, URI selfUri) {
        return Json.createObjectBuilder()
                ...
    }
}

If the representation to some endpoint or external system differs from our model, we won't be able to fully avoid duplication without other downsides. By using this approach, we decouple the mapping logic from the model and have full flexibility. The mapping of the POJO properties happens in the builder pattern invocations. Compared to introducing separate transfer object classes and mapping them in another functionality, this results in less obfuscation and ultimately less code.

Let's take up on the Hypermedia example using the add-to-cart Siren actions again. This example gave an idea of the potential content of Hypermedia responses. For responses like these, the output needs to be dynamic and flexible, depending on the application's state. Now we can imagine the flexibility and strength of a programmatic mapping approach such as JSON-P. This output is not really feasible using declarative POJO mapping, which would introduce a quite complex graph of objects. In Java EE, it is advisable to either use JSON-P in a single responsibility or a third-party dependency for the desired content type.

For mapping Java objects into JSON or XML payloads, JAXB, JSON-B, and JSON-P offers seamless integration into other Java EE standards, such as JAX-RS. Besides the integration into JAX-RS that we just saw we can also integrate CDI injection; this interoperability holds true as for all modern Java EE standards.

JSON-B type adapters enable to map custom Java types that are unknown to JSON-B. They transform custom Java types into known and mappable types. A typical example is serializing references to objects as identifiers:

import javax.json.bind.annotation.JsonbTypeAdapter;

public class Employee {

    @JsonbTransient
    private long id;
    private String name;
    private String email;

    @JsonbTypeAdapter(value = OrganizationTypeAdapter.class)
    private Organization organization;

    ...
}

The type adapter specified on the organization field is used to represent the reference as the organization's ID. To resolve that reference, we need to look up valid organizations. This functionality can be simply injected into the JSON-B type adapter:

import javax.json.bind.adapter.JsonbAdapter;

public class OrganizationTypeAdapter implements JsonbAdapter<Organization, String> {

    @Inject
    OrganizationStore organizationStore;

    @Override
    public String adaptToJson(Organization organization) {
        return String.valueOf(organization.getId());
    }

    @Override
    public Organization adaptFromJson(String string) {
        long id = Long.parseLong(string);
        Organization organization = organizationStore.getOrganization(id);

        if (organization == null)
            throw new IllegalArgumentException("Could not find organization for ID " + string);

        return organization;
    }
}

This example already shows the benefit of having several standards that work well with each other. Developers can simply use and integrate the functionalities without spending time on configuration and plumbing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.79.206