Monday 3 June 2013

Exposing and Managing Links with Spring HATEOAS

As a follow up to the previous Spring HATEOAS post, this post will cover how to use the @ExposesResourceFor annotation as an alternative to using the resource's Controller to obtain links.

It'll also cover how to find links based on a particular relation type in a hypermedia enabled representation.

In addition to the classes in the previous post, the SettlementController class is responsible for settling bets and exposes one method, settleBet, in order to do this. To return a link to the settled bet, the SettlementController uses the linkTo method on the ControllerLinkBuilder class:

@RequestMapping(method = RequestMethod.PUT, value = "/{betId}")
ResponseEntity<SettlementResource> settleBet(@PathVariable Long betId) {

    Settlement settlement = this.settlementService.settleBet(betId);  
    SettlementResource resource = 
        settlementResourceAssembler.toResource(settlement);
    resource.add(
        linkTo(BetController.class).slash(betId).withSelfRel());
    return new ResponseEntity<SettlementResource>(resource, HttpStatus.CREATED);
}  

An alternative approach to this is to enable EntityLinks. By adding the @ExposesResourcesFor annotation to the BetController, the SettlementController need only know about the resource and not it's Controller class.

@Controller
@ExposesResourceFor(Bet.class)
@RequestMapping("/bets")
public class BetController {
...
}

The SettlementController by way of the EntityLinks interface can now obtain the link to a single resource or a link to a resource collection. It can either get the Link or a LinkBuilder. With the latter approach, the relation types can be overwritten:


@Controller
@RequestMapping("/settlements")
public class SettlementController {

    private SettlementService settlementService;
    private SettlementResourceAssembler settlementResourceAssembler;
    private EntityLinks entityLinks;

    @Autowired
    public SettlementController(SettlementService settlementService,
        SettlementResourceAssembler settlementResourceAssembler, EntityLinks entityLinks) {
        this.settlementService = settlementService;
        this.settlementResourceAssembler = settlementResourceAssembler;
        this.entityLinks = entityLinks;
    }

    @RequestMapping(method = RequestMethod.PUT, value = "/{betId}")
    ResponseEntity<SettlementResource> settleBet(@PathVariable Long betId) {

        Settlement settlement = this.settlementService.settleBet(betId);
        SettlementResource resource = 
            settlementResourceAssembler.toResource(settlement);
        resource.add(this.entityLinks.linkToSingleResource(Bet.class, betId));
        return new ResponseEntity<SettlementResource>(
            resource, HttpStatus.CREATED);
    } 
 
}

For a collection link, the call would be:

resource.add(this.entityLinks.linkToCollectionResource(Bet.class)); 

To obtain the builders:

LinkBuilder linkFor = this.entityLinks.linkFor(Bet.class);
LinkBuilder linkForSingleResource = 
    this.entityLinks.linkForSingleResource(Bet.class, betId);

To enable all this functionality, the @EnableEntityLinks annotation must be in your Spring configuration.

Another useful feature that comes with Spring HATEOAS, is the ability to discover links. For a given hypermedia enabled representation, the Link Discoverer API can find a link or links for a particular relation type. For example, from the previous post, to find the cancelBet relation type link of a ResourceSupport object would be:

LinkDiscoverer discoverer = new DefaultLinkDiscoverer();
Link link = discoverer.findLinkWithRel("cancelBet", resource.toString());

The DefaultLinkDiscoverer will expect JSON but since 0.5 of Spring HATEOAS, the HAL variant has been supported via the HalLinkDiscoverer class.

Although still evolving, the Spring HATEOAS project provides a good foundation for implementing a HATEOAS compliant REST service. But if all you require is to expose and manage domain objects via a REST web service then the Spring Data REST project which applies the HATEOAS constraint to persisted entities is worth considering and the Restbucks example an ideal tutorial.


3 comments:

  1. Hi Geraint.
    Firstly thanks for providing such a clear walkthrough of Spring HATEOAS. I am currently trying to develop a small PoC/test app of my own in this area however I am having difficulty defining a particular REST method.
    Whilst I have successfully built support for calls of the equivalent of GET /bets/1, I'm having difficulty defining a controller method that simply returns a list of object links and not the actual objects themselves. For example I was hoping to support a GET call to /bets which would return just an array of link objects to the set of defined bets, and not an array of the actual bet objects themselves.

    Any examples or advice you could provide here to help me out would be enormously appreciated.
    Many thanks,
    Steve

    ReplyDelete
  2. Hi Steve,

    Thanks for your comment.

    One way to do this is by adding links to a ResourceSupport object and returning this object as the body of the ResponseEntity. The below replacement getBets method does this by iterating over the bet ids. You'll need to think about the rel types for navigation if you're returning links in this fashion but for the sake of this example I've just set them to be the bet id:

    @RequestMapping(method = RequestMethod.GET)
    ResponseEntity getBets() {
    ResourceSupport resourceSupport = new ResourceSupport();
    List betIdList = betService.getAllBetIds();
    for (Long id : betIdList) {
    resourceSupport.add(linkTo(BetController.class).slash(id).
    withRel(id.toString()));
    }
    return new ResponseEntity(resourceSupport, HttpStatus.OK);
    }

    Hope this helps. I'll try and get the Spring Hateoas example up on GitHub in the coming weeks.

    thanks
    Geraint

    ReplyDelete
  3. Hi Geraint,

    Thanks very much for the example. Using this I've been able to implement a set of getAll() controller methods that return ID-based links to the items within a given collection. I've also added a count header to these responses for use specifically within a head request as a kind of 'peek' at how many items are available for a given collection.
    In developing this I also ran into the 'Could not generate CGLIB subclass of class' error which you're blog also referenced, along with a fix.

    So thanks for the enormously clear advice and examples. Its saved me most likely days of trawling the net.
    Cheers,
    Steve

    ReplyDelete

Note: only a member of this blog may post a comment.