HATEOAS paging with Spring MVC and Spring Data JPA

In a previous post, I exposed the principles of HATEOAS and illustrated these principles with an implementation based on Spring MVC and Spring HATEOAS. In this post, I’ll go further with the implementation of a paged REST web services that follows the guidelines of HATEOAS. This implementation is based again on Spring MVC and Spring HATEOAS, and uses Spring Data JPA‘s built-in paging features for the database backend.

Paged REST controller, no HATEOAS

The first version of our controller doesn’t implement HATEOAS guidelines, as the representation of a page contains the content itself (contact records) and information about the page itself (number, size, etc), but no link to navigate to the previous or to the next page.
Here is the first version of our controller:

@Controller
@RequestMapping("/contacts")
public class ContactController {
  @Autowired ContactRepository contactRepository;
  @RequestMapping(value="/pages",method=RequestMethod.GET)
  @ResponseBody
  public Page<Contact> contactsPages(@RequestParam int page,@RequestParam int size) {
    Pageable pageable = new PageRequest(
      page,size,new Sort("id")
    );
    Page<Contact> pageResult = contactRepository.findAll(pageable);
    return pageResult;
  }
}

Some explanations:

the client provides the number of the requested page and the size with HTTP parameters. The end of the URL would look like /contacts/pages?page=1&size=5.

the controller uses Spring Data JPA paging support. It builds a PageRequest object from the HTTP parameters, chooses to sort by ID, calls the Spring Data JPA repository, and gets a Page.

The controller returns the Page, Spring MVC takes care of generating some JSON from the Page.

Here is what the representation looks like in this version (content first and then page information):

{
   "content":[
      {
         "id":1,
         "firstname":"Joe",
         "lastname":"Dalton",
         "age":37
      },
      {
         "id":2,
         "firstname":"William",
         "lastname":"Dalton",
         "age":35
      },
      ... more records ...
   ],
   "size":5,
   "number":0,
   "sort":[
      {
         "direction":"ASC",
         "property":"id",
         "ascending":true
      }
   ],
   "totalPages":3,
   "numberOfElements":5,
   "totalElements":12,
   "firstPage":true,
   "lastPage":false
}

In the previous post, we worked on making the records HATEOAS: a contact record was a summary of the contact resource and contained a link to GET to the detail of the contact. We could apply the exact same techniques in this post, but we’ll leave the records alone and focus on the page itself. We want a page to expose links to the previous/next/first/last pages.
We will make these links available by wrapping a page in a link-aware custom class.

The link-aware page wrapper

The page wrapper has the following goals:

  • exposing the page – that’s why it implements Page and delegates all the methods to the underlying page (the one returned by the repository)
  • building and exposing the links – it builds the links in its constructor and exposes them thanks to a base class that Spring Data provides.

Here is the wrapper:

public class PageResource<T> extends ResourceSupport implements Page<T> {
  private final Page<T> page;
  public PageResource(Page<T> page, String pageParam,
      String sizeParam) {
    super();
    this.page = page;
    addPreviousLink(page, pageParam, sizeParam);
    addNextLink(page, pageParam, sizeParam);
    addFirstLink(page, pageParam, sizeParam);
    addLastLink(page, pageParam, sizeParam);
    addSelfLink(page, pageParam, sizeParam);
  }
  private void addPreviousLink(Page<T> page, String pageParam,
      String sizeParam) {
    if(page.hasPreviousPage()) {
      Link link = buildPageLink(pageParam,page.getNumber()-1,sizeParam,page.getSize(),Link.REL_PREVIOUS);
      add(link);
    }
  }
  private Link buildPageLink(String pageParam,int page,String sizeParam,int size,String rel) {
    String path = createBuilder()
        .queryParam(pageParam,page)
        .queryParam(sizeParam,size)
        .build()
        .toUriString();
    Link link = new Link(path,rel);
    return link;
  }
  private ServletUriComponentsBuilder createBuilder() {
    return ServletUriComponentsBuilder.fromCurrentRequestUri();
  }
  (...) // other links method
  // Page<T> interface methods
  @Override
  public int getNumber() {
    return page.getNumber();
  }
  @Override
  public int getSize() {
    return page.getSize();
  }
  (...) other Page<T> methods delegate to the delegate
}

 
I left only the parts that matter. There are different links but they’re all built in the same way. I use Spring MVC’s ServletUriComponentsBuilder to build the URL from the current request, with the appropriate HTTP parameters for the paging, and then create a Spring HATEOAS Link from the URL. The ServletUriComponentsBuilder is handy when it comes to build URL, as it handles path variables, expanding of the path template with values, and encoding.

New version of the controller

The controller doesn’t change much, it just needs to build a PageResource from the page and return it:

@Controller
@RequestMapping("/contacts")
public class ContactController {
  @Autowired ContactRepository contactRepository;
  @RequestMapping(value="/pages",method=RequestMethod.GET)
  @ResponseBody
  public PageResource<Contact> contactsPages(@RequestParam int page,@RequestParam int size) {
    Pageable pageable = new PageRequest(
      page,size,new Sort("id")
    );
    Page<Contact> pageResult = contactRepository.findAll(pageable);
    return new PageResource<Contact>(pageResult,"page","size");
  }
}

Here is how the JSON representation looks like now:

{
   "content":[
      {
         "id":1,
         "firstname":"Joe",
         "lastname":"Dalton",
         "age":37
      },
      {
         "id":2,
         "firstname":"William",
         "lastname":"Dalton",
         "age":35
      },
      ... more records ...
   ],
   "size":5,
   "number":0,
   "sort":[
      {
         "direction":"ASC",
         "property":"id",
         "ascending":true
      }
   ],
   "totalPages":3,
   "numberOfElements":5,
   "totalElements":12,
   "firstPage":true,
   "lastPage":false,
   "id":{
      "rel":"self",
      "href":"http://localhost:8080/hateoas/zen-contact/contacts/pages?page=0&size=5"
   },
   "links":[
      {
         "rel":"next",
         "href":"http://localhost:8080/hateoas/zen-contact/contacts/pages?page=1&size=5"
      },
      {
         "rel":"first",
         "href":"http://localhost:8080/hateoas/zen-contact/contacts/pages?page=0&size=5"
      },
      {
         "rel":"last",
         "href":"http://localhost:8080/hateoas/zen-contact/contacts/pages?page=2&size=5"
      },
      {
         "rel":"self",
         "href":"http://localhost:8080/hateoas/zen-contact/contacts/pages?page=0&size=5"
      }
   ]
}

Notice the id and the links fields. They come from the ResourceSupport our PageResource class extends.
A client of our REST web service is now able to navigate through the pages of our list of contacts. This client could be your favorite AJAX library, like JQuery, that would hit the web service and nicely formats the data in an HTML interface.

Conclusion

This post illustrated how to make a REST web service easier to use from the client’s point of view: result pages contain links to navigate through the dataset. The implementation has been made straightforward thanks to the use of Spring Data JPA, Spring MVC, and Spring HATEOAS.
Source code

3 réflexions sur “HATEOAS paging with Spring MVC and Spring Data JPA

  • 21 juillet 2014 à 12 h 02 min
    Permalien

    Hi Arnaud,

    Very nice article, I could implement it easily, thanks for this good explanation. I reckon there is now some alternative offered by the Spring framework though, that makes the hand coding of the PageResource class no longer required. But still, very good if only for educational purposes. Thanks !

    Kind Regards,

    Stephane Eybert

    Répondre
  • 31 juillet 2014 à 10 h 03 min
    Permalien

    Hi,
    Thank for this good example!
    Based on this, how would manage to be sure each T has its own Hateoas links ?
    Their should be at least the « self » one for the client to know where to go when it has choosen the element he wanted…

    Répondre

Répondre à JR Utily Annuler la réponse.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :