JAX-RS - Applying HATEOAS

[Updated: Dec 26, 2017, Created: Dec 26, 2017]

HATEOAS (Hypermedia as the Engine of Application State) is a constraint of REST application architecture where REST web resources provide responses which contain links of further resources to access.

In SOA/WSDL services, there is a rigid contract between client and server and client needs full pre knowledge of all endpoints it can access.

In REST world, the client only needs to know one main resource URL as the entry point. Based on HATEOAS, the entry point response (or any REST response) dynamically embeds the links of other resources that client can further explore or follow.

JAX-RS provides classes UriBuilder and Link (RFC 5988 based) which help to prepare and embed the links in responses.

Example

A domain class:

package com.logicbig.example;

import javax.ws.rs.core.Link;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.Arrays;
import java.util.List;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {
  private int id;
  private String name;
  private int age;
  @XmlJavaTypeAdapter(Link.JaxbAdapter.class)
  private List<Link> links;
    .............
}

In above class, we are using JAXB based annotations, so that we can return response in XML or JSON. Also XmlJavaTypeAdapter annotation is used for custom marshaling. Link.JaxbAdapter (a subclass of XmlAdapter) maps the JAX-RS Link type to a value that can be marshalled and unmarshalled by JAXB.

A JAX-RS resource with HATEOAS links

@Path("persons")
public class PersonsResource {

  @GET
  @Produces(MediaType.APPLICATION_XML)
  public Response getPersons(@Context UriInfo uriInfo) {
      List<Person> persons = PersonService.Instance.getPersons();
      persons.forEach(p -> initLinks(p, uriInfo));

      GenericEntity<List<Person>> genericEntity =
              new GenericEntity<List<Person>>(persons) {};

      Link self = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder())
                      .rel("self").build();
      Link historyLink = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()
                                                    .path("history")
                                                    .queryParam("year", "1990"))
                             .rel("history").build();

      return Response.ok(genericEntity)
                     .links(self, historyLink).build();
  }

  @GET
  @Produces(MediaType.APPLICATION_XML)
  @Path("{id}")
  public Response getPerson(@PathParam("id") int id, @Context UriInfo uriInfo) {
      Person person = PersonService.Instance.getPersonById(id);

      Link self = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder())
                      .rel("self").build();

      return Response.ok(person)
                     .links(self).build();
  }


  private void initLinks(Person person, UriInfo uriInfo) {
      //create self link
      UriBuilder uriBuilder = uriInfo.getRequestUriBuilder();
      uriBuilder = uriBuilder.path(Integer.toString(person.getId()));
      Link.Builder linkBuilder = Link.fromUriBuilder(uriBuilder);
      Link selfLink = linkBuilder.rel("self").build();
      //also we can add other meta-data by using: linkBuilder.param(..),
      // linkBuilder.type(..), linkBuilder.title(..)
      person.setLinks(Arrays.asList(selfLink));
  }
}

In above resource class, to produce JSON responses, just use MediaType.APPLICATION_JSON instead of MediaType.APPLICATION_XML (the related decadency of jersey-media-moxy is already included in project browser below).

Client

public class ExampleClient {
  public static void main(String[] args) {
      Client client = ClientBuilder.newClient();
      WebTarget target =
              client.target("http://localhost:8080/persons");
      Response response = target.request()
                                .get();
      System.out.printf("status: %s%n", response.getStatus());
      System.out.println("-- response headers --");
      response.getHeaders().entrySet().stream()
              .forEach(e -> System.out.println(e.getKey() + " = " + e.getValue()));
      System.out.printf("-- response body --%n%s%n", response.readEntity(String.class)
                                                             //just for pretty xml output
                                                             .replaceAll("(</\\w+>)", "$1\n")
                                                             .replaceAll("/>", "/>\n"));
  }
}
status: 200
-- response headers --
Server = [Apache-Coyote/1.1]
Content-Length = [305]
Date = [Tue, 26 Dec 2017 18:54:05 GMT]
Link = [<http://localhost:8080/persons/history?year=1990>; rel="history", <http://localhost:8080/persons>; rel="self"]
Content-Type = [application/xml]
-- response body --
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><people><person><id>1</id>
<name>Tina</name>
<age>35</age>
<links href="http://localhost:8080/persons/1" rel="self"/>
</person>
<person><id>2</id>
<name>Charlie</name>
<age>40</age>
<links href="http://localhost:8080/persons/2" rel="self"/>
</person>
</people>

The client can further access the resources by using the links provided in above response. For example:

public class ExampleClient2 {
  public static void main(String[] args) {
      Client client = ClientBuilder.newClient();
      WebTarget target =
              client.target("http://localhost:8080/persons/1");
      Response response = target.request()
                                .get();
      System.out.printf("status: %s%n", response.getStatus());
      System.out.println("-- response headers --");
      response.getHeaders().entrySet().stream()
              .forEach(e -> System.out.println(e.getKey() + " = " + e.getValue()));
      System.out.printf("-- response body --%n%s%n", response.readEntity(String.class)
                                                             //just for pretty xml output
                                                             .replaceAll("(</\\w+>)", "$1\n")
                                                             .replaceAll("/>", "/>\n"));
  }
}
status: 200
-- response headers --
Server = [Apache-Coyote/1.1]
Content-Length = [112]
Date = [Tue, 26 Dec 2017 18:55:20 GMT]
Link = [<http://localhost:8080/persons/1>; rel="self"]
Content-Type = [application/xml]
-- response body --
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><person><id>1</id>
<name>Tina</name>
<age>35</age>
</person>

Note:

While applying HATEOAS, the resources do not need to specify different links for POST (for update), DELETE etc, because 'self' link is sufficient for carrying out these requests (executed with different HTTP methods). The resource, of course, needs to implement all corresponding methods. Also, to create a new Person resource with id 3 (in above example), we can just use http://localhost:8080/persons/3 with PUT HTTP method (utilizing self header link of the main response).

Example Project

Dependencies and Technologies Used :

  • jersey-server 2.25.1: Jersey core server implementation.
  • jersey-container-servlet 2.25.1: Jersey core Servlet 3.x implementation.
  • jersey-media-moxy 2.25.1: Jersey JSON entity providers support module based on EclipseLink MOXy.
  • JDK 1.8
  • Maven 3.3.9

JAX-RS HATEOAS Example Select All Download
  • jaxrs-hateoas-example
    • src
      • main
        • java
          • com
            • logicbig
              • example

See Also