Enterprise Java

REST Service Discoverability with Spring, part 5

This is the fifth 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. The previous article introduced the concept of Discoverability for the RESTful service, HATEOAS and followed with some practical scenarios driven by tests. This article will focus on the actual implementation of discoverability and satisfying the HATEOAS constraint in the REST Service using Spring 3.1.

Decouple Discoverability through events

Discoverability as a separate aspect or concern of the web layer should be decoupled from the controller handling the HTTP request. In order to do so, the Controller will fire off events for all the actions that require additional manipulation of the HTTP response:

@RequestMapping( value = "admin/foo/{id}",method = RequestMethod.GET )
@ResponseBody
public Foo get( @PathVariable( "id" ) Long id, 
    HttpServletRequest request, HttpServletResponse response ){
   Foo resourceById = RestPreconditions.checkNotNull( this.service.getById( id ) );
   
   this.eventPublisher.publishEvent
    ( new SingleResourceRetrieved( this, request, response ) );
   return resourceById;
}
@RequestMapping( value = "admin/foo",method = RequestMethod.POST )
@ResponseStatus( HttpStatus.CREATED )
public void create( @RequestBody Foo resource, 
    HttpServletRequest request, HttpServletResponse response ){
   RestPreconditions.checkNotNullFromRequest( resource );
   Long idOfCreatedResource = this.service.create( resource );
   
   this.eventPublisher.publishEvent
   ( new ResourceCreated( this, request, response, idOfCreatedResource ) );
}

These events can then be handled by any number of decoupled listeners, each focusing on it’s own particular case and each moving towards satisfying the overall HATEOAS constraint.

Also, the listeners should be the last objects in the call stack and no direct access to them is necessary; as such they are not public.

Make the URI of a newly created resource discoverable

As discussed in the previous post, the operation of creating a new resource should return the URI of that resource in the Location HTTP header of the response. :

@Component
class ResourceCreatedDiscoverabilityListener 
    implements ApplicationListener< ResourceCreated >{
   
   @Override
   public void onApplicationEvent( ResourceCreated resourceCreatedEvent ){
      Preconditions.checkNotNull( resourceCreatedEvent );
      
      HttpServletRequest request = resourceCreatedEvent.getRequest();
      HttpServletResponse response = resourceCreatedEvent.getResponse();
      long idOfNewResource = resourceCreatedEvent.getIdOfNewResource();
      
      this.addLinkHeaderOnResourceCreation( request, response, idOfNewResource );
   }
   void addLinkHeaderOnResourceCreation
    ( HttpServletRequest request, HttpServletResponse response, long idOfNewResource ){
      String requestUrl = request.getRequestURL().toString();
      URI uri = new UriTemplate( "{requestUrl}/{idOfNewResource}" )
       .expand( requestUrl, idOfNewResource );
      response.setHeader( HttpHeaders.LOCATION, uri.toASCIIString() );
   }
}

Unfortunately, dealing with the low level request and response objects is inevitable even in Spring 3.1, because first class support for specifying the Location is still in the works.

Get of single resource

Retrieving a single resource should allow the client to discover the URI to get all resources of that particular type:

@Component
class SingleResourceRetrievedDiscoverabilityListener 
 implements ApplicationListener< SingleResourceRetrieved >{
   
   @Override
   public void onApplicationEvent( SingleResourceRetrieved resourceRetrievedEvent ){
      Preconditions.checkNotNull( resourceRetrievedEvent );
      
      HttpServletRequest request = resourceRetrievedEvent.getRequest();
      HttpServletResponse response = resourceRetrievedEvent.getResponse();
      this.addLinkHeaderOnSingleResourceRetrieval( request, response );
   }
   void addLinkHeaderOnSingleResourceRetrieval
    ( HttpServletRequest request, HttpServletResponse response ){
      StringBuffer requestURL = request.getRequestURL();
      int positionOfLastSlash = requestURL.lastIndexOf( "/" );
      String uriForResourceCreation = requestURL.substring( 0, positionOfLastSlash );

      String linkHeaderValue = RESTURLUtil
       .createLinkHeader( uriForResourceCreation, "collection" );
      response.addHeader( LINK_HEADER, linkHeaderValue );
   }
}

Note that the semantics of the link relation make use of the “collection” relation type, specified and used in several microformats, but not yet standardized.

The Link header is one of the most used HTTP header for the purposes of discoverability. Because of this, some simple utilities are needed to ease the creation of it’s values on the server and to avoid introducing a third party library.

Discoverability at the root

The root is the entry point in the RESTful web service – it is what the client comes into contact with when consuming the API for the first time. If the HATEOAS constraint is to be considered and implemented throughout, then this is the place to start. The fact that most of the main URIs of the system have to be discoverable from the root shouldn’t come as much of a surprise by this point.

This is a sample controller method to provide discoverability at the root:

@RequestMapping( value = "admin",method = RequestMethod.GET )
@ResponseStatus( value = HttpStatus.NO_CONTENT )
public void adminRoot( HttpServletRequest request, final response ){
   String rootUri = request.getRequestURL().toString();
   
   URI fooUri = new UriTemplate( "{rootUri}/{resource}" ).expand( rootUri, "foo" );
   String linkToFoo = RESTURIUtil.createLinkHeader
    ( fooUri.toASCIIString(), REL_COLLECTION );
   response.addHeader( HttpConstants.LINK_HEADER, linkToFoo );
}

This is of course an illustration of the concept, to be read in the context of the proof of concept RESTful service of the series. In a more complex system there would be many more links, each with it’s own semantics defined by the type of link relation.

Discoverability is not about changing URIs

One of the more common pitfalls related to discoverability is the misunderstanding that, since the URIs are now discoverable, then they can be subject to change. This is however simply not the case, and for good reason: first, this is not how the web works – clients will bookmark the URIs and will expect them to work in the future. Second, the client shouldn’t have to navigate through the API to get to a certain state that could have been reached directly.

Instead, all URIs of the RESTful web service should be considered cool URIs, and cool URIs don’t change. Instead, versioning of the API can be used to solve the problem of a URI reorganization.

Caveats of Discoverability

As some of the discussions around the previous articles state, the first goal of discoverability is to make minimal or no use of documentation and have the client learn and understand how to use the API via the responses it gets. In fact, this shouldn’t be regarded as such a far fetched ideal – it is how we consume every new web page – without any documentation. So, if the concept is more problematic in the context of REST, then it must be a matter of technical implementation, not of a question of whether or not it’s possible.

That being said, technically, we are still far from the a fully working solution – the specification and framework support are still evolving, and because of that, some compromises may have to be made; these are nevertheless compromises and should be regarded as such.

Conclusion

This article covered the implementation of some of the traits of discoverability in the context of a RESTful Service with Spring MVC and touched on the concept of discoverability at the root. In the next articles I will focus on custom link relations and the Atom Publishing Protocol. In the meantime, check out the github project.

Reference: REST Service Discoverability with Spring, part 5 from our JCG partner Eugen Paraschiv at the baeldung blog.

Related Articles :

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button