I’m a software engineer with a passion for REST, TDD and clean code, Web Security and Data Mining. Baeldung is about all of these and more. Eugen is a DZone MVB and is not an employee of DZone and has posted 20 posts at DZone. You can read more from them at their website. View Full User Profile

REST Pagination in Spring

03.09.2012
| 12493 views |
  • submit to reddit

This is the seventh of a series of articles about setting up a secure RESTful Web Service using Spring 3.1 and Spring Security 3.1 with Java based configuration. This article will focus on the implementation of pagination in a RESTful web service.

The REST with Spring series:


Page as resource vs Page as representation

The first question when designing pagination in the context of a RESTful architecture is whether to consider the page an actual resource or just a representation of resources. Treating the page itself as a resource introduces a host of problems such as no longer being able to uniquely identify resources between calls. This, coupled with the fact that outside the RESTful context, the page cannot be considered a proper entity, but a holder that is constructed when needed makes the choice straightforward: the page is part of the representation.

The next question in the pagination design in the context of REST is where to include the paging information:

  • in the URI path: /foo/page/1
  • the URI query: /foo?page=1

Keeping in mind that a page is not a resource, encoding the page information in the URI is no longer an option.

Page information in the URI query

Encoding paging information in the URI query is the standard way to solve this issue in a RESTful service. This approach does however have one downside – it cuts into the query space for actual queries:

/foo?page=1&size=10

The Controller

Now, for the  implementation – the Spring MVC Controller for pagination is straightforward:

@RequestMapping( value = "admin/foo",params = { "page", "size" },method = GET )
@ResponseBody
public List< Foo > findPaginated(
 @RequestParam( "page" ) int page, @RequestParam( "size" ) int size,
 UriComponentsBuilder uriBuilder, HttpServletResponse response ){
   
   Page< Foo > resultPage = service.findPaginated( page, size );
   if( page > resultPage.getTotalPages() ){
      throw new ResourceNotFoundException();
   }
   eventPublisher.publishEvent( new PaginatedResultsRetrievedEvent< Foo >
    ( Foo.class, uriBuilder, response, page, resultPage.getTotalPages(), size ) );
   
   return resultPage.getContent();
}

The two query parameters are defined in the request mapping and injected into the controller method via @RequestParam; the HTTP response and the Spring UriComponentsBuilder are injected in the Controller method to be included in the event, as both will be needed to implement discoverability.

Discoverability for REST pagination

Withing the scope of pagination, satisfying the HATEOAS constraint of REST means enabling the client of the API to discover the next and previous pages based on the current page in the navigation. For this purpose, the Link HTTP header will be used, coupled with the officialnext“, “prev“, “first” and “last” link relation types.

In REST, Discoverability is a cross cutting concern, applicable not only to specific operations but to types of operations. For example, each time a Resource is created, the URI of that resource should be discoverable by the client. Since this requirement is relevant for the creation of ANY Resource, it should be dealt with separately and decoupled from the main Controller flow.

With Spring, this decoupling is achieved with events, as was thoroughly discussed in the previous article focusing on Discoverability of a RESTful service. In the case of pagination, the event – PaginatedResultsRetrievedEvent – was fired in the Controller, and discoverability is achieved in a listener for this event:

void addLinkHeaderOnPagedResourceRetrieval(
 UriComponentsBuilder uriBuilder, HttpServletResponse response,
 Class clazz, int page, int totalPages, int size ){
   
   String resourceName = clazz.getSimpleName().toString().toLowerCase();
   uriBuilder.path( "/admin/" + resourceName );
   
   StringBuilder linkHeader = new StringBuilder();
   if( hasNextPage( page, totalPages ) ){
      String uriNextPage = constructNextPageUri( uriBuilder, page, size );
      linkHeader.append( createLinkHeader( uriForNextPage, REL_NEXT ) );
   }
   if( hasPreviousPage( page ) ){
      String uriPrevPage = constructPrevPageUri( uriBuilder, page, size );
      appendCommaIfNecessary( linkHeader );
      linkHeader.append( createLinkHeader( uriForPrevPage, REL_PREV ) );
   }
   if( hasFirstPage( page ) ){
      String uriFirstPage = constructFirstPageUri( uriBuilder, size );
      appendCommaIfNecessary( linkHeader );
      linkHeader.append( createLinkHeader( uriForFirstPage, REL_FIRST ) );
   }
   if( hasLastPage( page, totalPages ) ){
      String uriLastPage = constructLastPageUri( uriBuilder, totalPages, size );
      appendCommaIfNecessary( linkHeader );
      linkHeader.append( createLinkHeader( uriForLastPage, REL_LAST ) );
   }
   response.addHeader( HttpConstants.LINK_HEADER, linkHeader.toString() );
}


In short, the listener logic checks if the navigation allows for a next, previous, first and last pages and, if it does, adds the relevant URIs to the Link HTTP Header. It also makes sure that the link relation type is the correct one – “next”, “prev”, “first” and “last”. This is the single responsibility of the listener (the full code here).

Test Driving Pagination

Both the main logic of pagination and discoverability should be extensively covered by small, focused integration tests; as in the previous article, the rest-assured library is used to consume the REST service and to verify the results.

These are a few example of pagination integration tests; for a full test suite, check out the github project (link at the end of the article):

@Test
public void whenResourcesAreRetrievedPaged_then200IsReceived(){
   Response response = givenAuth().get( paths.getFooURL() + "?page=1&size=10" );
   
   assertThat( response.getStatusCode(), is( 200 ) );
}
@Test
public void
 whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived(){
   Response response = givenAuth().get(
    paths.getFooURL() + "?page=" + randomNumeric( 5 ) + "&size=10" );
   
   assertThat( response.getStatusCode(), is( 404 ) );
}
@Test
public void
 givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources(){
   restTemplate.createResource();
   
   Response response = givenAuth().get( paths.getFooURL() + "?page=1&size=10" );
   
   assertFalse( response.body().as( List.class ).isEmpty() );
}

Test Driving Pagination Discoverability

Testing Discoverability of Pagination is relatively straightforward, although there is a lot of ground to cover. The tests are focused on the position of the current page in navigation and the different URIs that should be discoverable from each position:

@Test
public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext(){
   Response response = givenAuth().get( paths.getFooURL()+"?page=0&size=10" );

   String uriToNextPage = extractURIByRel( response.getHeader( LINK ), REL_NEXT );
   assertEquals( paths.getFooURL()+"?page=1&size=10", uriToNextPage );
}
@Test
public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage(){
   Response response = givenAuth().get( paths.getFooURL()+"?page=0&size=10" );
   
   String uriToPrevPage = extractURIByRel( response.getHeader( LINK ), REL_PREV );
   assertNull( uriToPrevPage );
}
@Test
public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious(){
   Response response = givenAuth().get( paths.getFooURL()+"?page=1&size=10" );
   
   String uriToPrevPage = extractURIByRel( response.getHeader( LINK ), REL_PREV );
   assertEquals( paths.getFooURL()+"?page=0&size=10", uriToPrevPage );
}
@Test
public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable(){
   Response first = givenAuth().get( paths.getFooURL()+"?page=0&size=10" );
   String uriToLastPage = extractURIByRel( first.getHeader( LINK ), REL_LAST );
   
   Response response = givenAuth().get( uriToLastPage );
   
   String uriToNextPage = extractURIByRel( response.getHeader( LINK ), REL_NEXT );
   assertNull( uriToNextPage );
}


These are just a few examples of integration tests consuming the RESTful service.

Getting All Resources

On the same topic of pagination and discoverability, the choice must be made if a client is allowed to retrieve all the Resources in the system at once, or if the client MUST ask for them paginated.

If the choice is made that the client cannot retrieve all Resources with a single request, and pagination is not optional but required, then several options are available for the response to a get all request.

One option is to return a 404 (Not Found) and use the Link header to make the first page discoverable:

Link=<http://localhost:8080/rest/api/admin/foo?page=0&size=10>; rel=”first“, <http://localhost:8080/rest/api/admin/foo?page=103&size=10>; rel=”last


Another option is to return redirect – 303 (See Other) – to the first page of the pagination.

A third option is to return a 405 (Method Not Allowed) for the GET request.

REST Paginag with Range HTTP headers

A relatively different way of doing pagination is to work with the HTTP Range headersRange, Content-Range, If-Range, Accept-Ranges – and HTTP status codes – 206 (Partial Content), 413 (Request Entity Too Large), 416 (Requested Range Not Satisfiable). One view on this approach is that the HTTP Range extensions were not intended for pagination, and that they should be managed by the Server, not by the Application.

Implementing pagination based on the HTTP Range header extensions is nevertheless technically possible, although not nearly as common as the implementation discussed in this article.

Conclusion

This article covered the implementation of Pagination in a RESTful service with Spring, discussing how to implement and test Discoverability. For a full implementation of pagination, check out the github project.

From the original REST Pagination in Spring of the REST with Spring series

Published at DZone with permission of Eugen Paraschiv, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Goel Yatendra replied on Thu, 2012/03/15 - 1:40pm

What do you mean by "This approach does however have one downside – it cuts into the query space for actual queries..."? Are you referring to the size of the query string on the URL, or the number of query parameters?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.