Producing some REST HATEOAS navigation elements with JAX-RS 2.0

Part of five-cents demo

This topic is part of the ‘new-client’ system of the Five-cents demo :

Five cents demo general schema - backend - new client

GitHubThe source code is available on GitHub, under /backends/client-brandnew and /framework/utils folder.

Before proceeding, you may have a look a the previous article : Exposing JEE JAX-RS resources under Thorntail with Swagger and Swagger-UI embedded

The Location header in the response of a POST request

One among others, a good practice in REST services is to fill the Location header in the response of POST actions.

Let’s send a POST to create a new client on our resource :

curl -X POST http://localhost:8080/api/v1/client -i -H "Content-Type: text/json" --data "{\"firstname\": \"Isabelle\", \"lastname\": \"PIVOT\", \"legacy-id\": 1009}"

And take a close look at the result of this POST request :

HTTP/1.1 201 Created
Connection: keep-alive
Location: http://localhost:8080/api/v1/client/8
Content-Type: application/octet-stream
Content-Length: 14
Date: Mon, 10 Sep 2018 18:50:18 GMT

The 201 HTTP code confirms the correct storage of the new client, and the Location HTTP header returns the URL that can be used by further requests to find or delete the client, making our Api more Restful.

To achieve this good principle of REST resource, we just need to pass the URI to the JAX-RS response object :

URI location = URI.create("client/" + clientId);
return Response
   .created(location)
   .entity("Client created")
   .build();

 

Pagination mechanisms for a response with a list of entities

In the same way, you may be used to the use of headers like ‘offset’, ‘min’, ‘max’ in the request of a GET operation to fetch a list of entities, and other headers like ‘X-Total-Count’ for instance to give the client the total number of entities.

Another nice possibility, consistent with HATEOAS navigation principle, consists of making the pagination on the resource side. The client gives a per-page and page header, and the server responds with Link headers, symplifying the navigation on the client side. GitHub uses this mechanism in their API : https://developer.github.com/v3/guides/traversing-with-pagination/

JAX-RS 2.0 provides javax.ws.rs.core.Link types to build these navigation elements on server-side, and we can provide a non-intrusive implementation with the help of javax.ws.rs.container.ContainerResponseFilter. With this class, the Link creation task will be delegated to specific dedicated objects.

Example of code to build automatic Link pagination

Let’s jump into the details, starting with the way to use this pagination mechanism. First of all, we can bring a low-level enterprise Java service with 3 navigation-flavored parameters, that returns a list of objects :

  • An orderBy parameter : to order the list according to a given field
  • A page parameter : to request for a specific page index, for the given order
  • A perPage parameter : to feed the number of elements expected per page

Here is the implementation of this method in com.fivecents.backends.clientbrandnew.enterprise.ClientEnterpriseService :

/**
 * Return all clients by an order, and with a pagination.
 * @return
 */
public Map<String, Object> getAllClients(String orderBy, int page, int perPage) {
   Map<String, Object> result = new HashMap<>();

   // Let's order the list, if required.
   if (orderBy == null) {
      orderBy = "id";
   }

   final String orderByToUse = orderBy;
   clients
      .sort((c1, c2) -> {
         switch(orderByToUse) {
            case "lastname":
               return c1.getLastName().compareTo(c2.getLastName());
            case "firstname":
               return c1.getFirstName().compareTo(c2.getFirstName());
            case "birthdate":
               return c1.getBirthDate().compareTo(c2.getBirthDate());
            default:
               return c1.getId() - c2.getId();
            }
   });

   // Now we can extract the required clients.
   int startingIndex = (page-1)*perPage;
   if (startingIndex < 0) { startingIndex = 0; }
   if (startingIndex > clients.size()-1) { startingIndex = clients.size()-1; }

   int endingIndex = Math.min(clients.size(), page*perPage);
   if (endingIndex < 0) { endingIndex = 0; }
   if (endingIndex > clients.size()) { endingIndex = clients.size(); }

   if (startingIndex > endingIndex) { endingIndex = startingIndex; }

   // Returns result.
   result.put("clients", clients.subList(startingIndex, endingIndex));
   result.put("total", clients.size());
   return result;
}

Fine, we have our smart service that takes into consideration ordering and paging. The REST resource method is pretty straightforward (ClientResource in package com.fivecents.backends.clientbrandnew.rest) :

@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Path("/")
@ApiOperation(value = "Get all clients", notes = "This operation returns clients in the database")
public Paginated<ClientListing> searchClients(
      @DefaultValue("id") @QueryParam("order_by") String orderBy,
      @DefaultValue("1") @QueryParam(PaginationConstants.QUERY_PARAM_PAGE) int page,
      @DefaultValue("10") @QueryParam(PaginationConstants.QUERY_PARAM_PER_PAGE) int perPage) {
   // Call the enterprise service.
   Map<String, Object> allClientsResult = clientEnterpriseService.getAllClients(orderBy, page, perPage); 
   List<Client> clients = (List<Client>) allClientsResult.get("clients");
   ClientListing clientListing = new ClientListing(clients);

   // We need to send back headers to help our callers to navigate easily into the clients.
   // This will be done outside this scope, with the help of the JAX-RS 2.0 container
   // response filter. All we need to do is sending back an entity inheriting from Pagination
   // interface.
   int totalCount = (int) allClientsResult.get("total");
   Paginated<ClientListing> paginatedClientListing = 
      new PaginatedImpl<ClientListing>(
         page,
         perPage,
         totalCount,
         clientListing);

   return paginatedClientListing;
}

The ClientListing object is a simple JAXB bean :

/**
 * This bean corresponds to the entity returned on JAX-RS search
 * operations.
 * 
 * @author Laurent CAILLETEAU
 */

@XmlRootElement(name="clients", namespace="")
public class ClientListing {

   private List<Client> clients;

   /**
    * Constructors.
    * @return
    */
   public ClientListing() {
      super();
   }
   public ClientListing(List<Client> clients) {
      this.clients = clients;
   }

   public List<Client> getClients() {
      return clients;
   }
   public void setClients(List<Client> clients) {
      this.clients = clients;
   }
}

All it costs is to wrap the ClientListing object to a PaginatedImpl<ClientListing> as the response of the REST method.

After a deploy of our application :

java -jar target/client-brand-new-thorntail.jar

We can make some requests to check the correct links. The service returns an initial list of 8 loaded clients, the default parameters for pagination are :

  • requested page : number 1
  • elements per page : 10

So this call :

curl "http://localhost:8080/api/v1/client" -H "Accept:application/json" -i

Actually returns the full list, with an ordering by id :

HTTP/1.1 200 OK
X-Total-Count: 8
Connection: keep-alive
Links: []
X-Page-Count: 1
Content-Type: application/json
Content-Length: 661
Date: Fri, 14 Sep 2018 15:22:10 GMT

{"clients":[
   {"id":0,"legacy-id":1001,"lastname":"MARTIN","firstname":"Adele","birthdate":1423954800000},
   {"id":1,"legacy-id":1002,"lastname":"SMITH","firstname":"Lucie","birthdate":1323126000000},
   {"id":2,"legacy-id":1003,"lastname":"CAILLETEAU","firstname":"Laurent","birthdate":231890400000},
   {"id":3,"legacy-id":1004,"lastname":"JORDAN","firstname":"Emmanuel","birthdate":181004400000},
   {"id":4,"legacy-id":1005,"lastname":"PIVOT","firstname":"Lucas","birthdate":589500000000},
   {"id":5,"legacy-id":1006,"lastname":"TORDU","firstname":"Salome","birthdate":257986800000},
   {"id":6,"legacy-id":1007,"lastname":"MOUTON","firstname":"Gerard","birthdate":-688611600000},
   {"id":7,"legacy-id":1008,"lastname":"MARCHAND","firstname":"Bernard","birthdate":-716691600000}
]}

Let’s now try to request for 3 elements per page, ordered by last name, and ask for the middle one :

curl "http://localhost:8080/api/v1/client?page=2&per_page=3&order_by=lastname" -H "Accept:application/json" -i

Result is :

HTTP/1.1 200 OK
X-Total-Count: 8
Connection: keep-alive
Links: [
   <http://localhost:8080/api/v1/client?per_page=3&page=1>; rel="first", 
   <http://localhost:8080/api/v1/client?per_page=3&page=3>; rel="last", 
   <http://localhost:8080/api/v1/client?per_page=3&page=1>; rel="prev", 
   <http://localhost:8080/api/v1/client?per_page=3&page=3>; rel="next"]
X-Page-Count: 3
Content-Type: application/json
Content-Length: 288
Date: Fri, 14 Sep 2018 15:29:57 GMT

{"clients":[
   {"id":0,"legacy-id":1001,"lastname":"MARTIN","firstname":"Adele","birthdate":1423954800000},
   {"id":6,"legacy-id":1007,"lastname":"MOUTON","firstname":"Gerard","birthdate":-688611600000},
   {"id":4,"legacy-id":1005,"lastname":"PIVOT","firstname":"Lucas","birthdate":589500000000}
]}

We receive the 3 elements of the second page, ordered by last name, and 4 links to navigate to the first, last page, previous and next page.

Zoom on the pagination classes

The code for pagination is contained in com.fivecents.rest.pagination.LinkPaginationContainerResponseFilter class, which inherits javax.ws.rs.container.ContainerResponseFilter. All JAX-RS responses will get through this filter, but only entities of Paginated type will be considered. For those types of responses, Links are created and put in HTTP headers of the response :

/**
 * This filter class implements JAX-RS 2.0 ContainerResponseFilter. All resources responses
 * will get through it, and we can build the Links elements outside the scope of the resource,
 * making a clean separation of concerns.
 * 
 * @author Laurent CAILLETEAU
 */
@Provider
public class LinkPaginationContainerResponseFilter implements ContainerResponseFilter {
   /**
    * The main filter method requested on every resource response.
    */
   @Override
   public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
      // Well, if no pagination, let's ignore.
      if (responseContext.getEntity() instanceof Paginated) {
         Paginated<?> entity = (Paginated<?>) responseContext.getEntity();

         // We can build the list of Links, with the information on pages we have.
         List<Link> allLinks = buildLinks(
            entity.requestedPageNumber(), 
            entity.totalNumberOfPages(), 
            requestContext.getUriInfo());

         // Let's feed the responseContext with these data.
         responseContext.setEntity(entity.requestedPageEntity());
         responseContext.getHeaders().addAll("Links", allLinks);
         responseContext.getHeaders().add(PaginationConstants.X_TOTAL_COUNT, entity.totalNumberOfElements());
         responseContext.getHeaders().add(PaginationConstants.X_PAGE_COUNT, entity.totalNumberOfPages());
      }
   }

   /** 
    * Method to help building the Links for a given response. 
    */
   private List<Link> buildLinks(int requestedPageNumber, int totalNumberOfPages, UriInfo uriInfo) {
      List<Link> allLinks = new ArrayList<>();
      if (requestedPageNumber == 1 && totalNumberOfPages == 1) {
         return allLinks;
      }

      // First and last links.
      Link linkFirst = Link.fromUriBuilder(uriInfo.getRequestUriBuilder()
         .replaceQueryParam(PaginationConstants.QUERY_PARAM_PAGE, PaginationConstants.FIRST_PAGE))
         .rel(PaginationConstants.RELATION_FIRST)
         .build();
      allLinks.add(linkFirst);

      Link linkLast = Link.fromUriBuilder(uriInfo.getRequestUriBuilder()
         .replaceQueryParam(PaginationConstants.QUERY_PARAM_PAGE, totalNumberOfPages))
         .rel(PaginationConstants.RELATION_LAST)
         .build();
      allLinks.add(linkLast);

      // Previous and next links, if required.
      if (requestedPageNumber > 1) {
         Link link = Link.fromUriBuilder(uriInfo.getRequestUriBuilder()
            .replaceQueryParam(PaginationConstants.QUERY_PARAM_PAGE, requestedPageNumber - 1))
            .rel(PaginationConstants.RELATION_PREV)
            .build();
         allLinks.add(link);
      }

      if (requestedPageNumber < totalNumberOfPages) {
         Link link = Link.fromUriBuilder(uriInfo.getRequestUriBuilder()
            .replaceQueryParam(PaginationConstants.QUERY_PARAM_PAGE, requestedPageNumber + 1))
            .rel(PaginationConstants.RELATION_NEXT)
            .build();
         allLinks.add(link);
      }

      return allLinks;
   }
}

We now have a concise implementation for Link REST navigation with Java EE 7 and JAX-RS 2.0 new classes.

Now what ?

We have a simple REST resource exposed through Thorntail microservice server. We can improve it a little bit, let’s move on to the next article : WebSocket simple implementation for checking javax.interceptor.Interceptors moderator actions

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: