Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Spring MVC and the HATEOAS constraint

DZone's Guide to

Spring MVC and the HATEOAS constraint

· Java Zone
Free Resource

Learn how to troubleshoot and diagnose some of the most common performance issues in Java today. Brought to you in partnership with AppDynamics.

HATEOAS is a REST architecture principle where hypermedia is used to change application state. To change state, the returned resource representation contains links thereby 'constraining' the client on what steps to take next.

The Spring-HATEOAS project aims to assist those writing Spring MVC code in the creation of such links and the assembly of resources returned to the clients. 

The example below will cover a simple scenario showing how links are created and returned for the resource, Bet. Each operation on the resource is described below:

  • createBet - this POST operation will create a Bet.
  • updateBet - this PUT operation will update the Bet.
  • getBet - this GET operation will retrieve a Bet.
  • cancelBet - this DELETE operation will cancel the Bet.
@Controller
@RequestMapping("/bets")
public class BetController {
 
 private BetService betService;
 private BetResourceAssembler betResourceAssembler;
 
 public BetController(BetService betService,
   BetResourceAssembler betResourceAssembler) {
  this.betService = betService;
  this.betResourceAssembler = betResourceAssembler;
 }
 
 @RequestMapping(method = RequestMethod.POST)
 ResponseEntity<BetResource> createBet(@RequestBody Bet body) {
  Bet bet = betService.createBet(body.getMarketId(),
    body.getSelectionId(), body.getPrice(), body.getStake(),
    body.getType());
  BetResource resource = betResourceAssembler.toResource(bet);
  return new ResponseEntity<BetResource>(resource, HttpStatus.CREATED);
 }
 
 @RequestMapping(method = RequestMethod.PUT, value = "/{betId}")
 ResponseEntity<BetResource> updateBet(@PathVariable Long betId,
   @RequestBody Bet body) throws BetNotFoundException, BetNotUnmatchedException {
  Bet bet = betService.updateBet(betId, body);
  BetResource resource = betResourceAssembler.toResource(bet);
  return new ResponseEntity<BetResource>(resource, HttpStatus.OK);
 }
 
 @RequestMapping(method = RequestMethod.GET, value = "/{betId}")
 ResponseEntity<BetResource> getBet(@PathVariable Long betId) throws BetNotFoundException {
  Bet bet = betService.getBet(betId);
  BetResource resource = betResourceAssembler.toResource(bet);
  if (bet.getStatus() == BetStatus.UNMATCHED) {
   resource.add(linkTo(BetController.class).slash(bet.getId()).withRel("cancel"));
  }
  return new ResponseEntity<BetResource>(resource, HttpStatus.OK);
 }
 
 @RequestMapping(method = RequestMethod.GET)
 ResponseEntity<List<BetResource>> getBets() {
  List<Bet> betList = betService.getAllBets();
  List<BetResource> resourceList = betResourceAssembler.toResources(betList);
  return new ResponseEntity<List<BetResource>>(resourceList, HttpStatus.OK);
 }
 
 @RequestMapping(method = RequestMethod.DELETE, value = "/{betId}")
 ResponseEntity<BetResource> cancelBet(@PathVariable Long betId) {
  Bet bet = betService.cancelBet(betId);
  BetResource resource = betResourceAssembler.toResource(bet);
  return new ResponseEntity<BetResource>(resource, HttpStatus.OK);
 }
 
 @ExceptionHandler
 ResponseEntity handleExceptions(Exception ex) {
  ResponseEntity responseEntity = null;
  if (ex instanceof BetNotFoundException) {
   responseEntity = new ResponseEntity(HttpStatus.NOT_FOUND);
  } else if (ex instanceof BetNotUnmatchedException) {
   responseEntity = new ResponseEntity(HttpStatus.CONFLICT);
  } else {
   responseEntity = new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
  }
  return responseEntity;
 }
  
}
All the operations will create a  BetResource for returning to the client. This is done by calling toResource on the  BetResourceAssembler class:
public class BetResourceAssembler extends ResourceAssemblerSupport<Bet, BetResource> {
 
 public BetResourceAssembler() {
  super(BetController.class, BetResource.class);
 }
 
 public BetResource toResource(Bet bet) {
  BetResource resource = instantiateResource(bet);
  resource.bet = bet;
        resource.add(linkTo(BetController.class).slash(bet.getId()).withSelfRel());
  return resource;
 }
 
}
This class extends  ResourceAssemblerSupport which requires the implementation of a  toResourcemethod as it implements the  ResourceAssembler interface. This is where the mapping between  Betand  BetResource is done. In this case,  BetResource is just a wrapper for  Bet so it is simply a case of setting the  bet attribute. The  instantiateResource method will return a  BetResource without any links so links can be added at this point if required. In this example a link to self is added. An alternative approach would be to use  createResourceWithId which will return a  BetResource with the self link. 
public class BetResource extends ResourceSupport {
  
 public Bet bet;
  
}
Also in this example, links are added to the  BetResource within the  BetController class to ensure the application of the HATEOAS constraint. If the REST service receives a GET request then a check is made on the status of the  Bet. If the  Bet is  UNMATCHED, then a link to cancel the  Betcan be added to the  BetResource. This is done in similar fashion to the self link but with the relationship attribute name of cancel.
An alternative approach to this is to build a link to a method as opposed to constructing a URI. 
resource.add(linkTo(methodOn(BetController.class).cancelBet(betId))
.withRel("cancel")); 
The  methodOn would create a proxy of the  BetController class and as a result the return type of the cancelBet method would have to be capable of proxying. Therefore in this example the return type of  cancelBet method would be  HttpEntity<Bet> and not  ResponseEntity<Bet>. If the latter, then the likely exception from the server would be:
[org.springframework.http.ResponseEntitycom.city81.hateoas.rest.BetResource> com.city81.hateoas.controller.BetController.getBet(java.lang.Long) throws com.city81.hateoas.BetNotFoundException]:org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class org.springframework.http.ResponseEntity]: common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
Back to the GET request, and the returned JSON for requesting a  Bet resource which has a status of  UNMATCHED is shown below:

{
 "links":[
  {"rel":"self","href": http://localhost:8080/hateoas-1-SNAPSHOT/bets/0},
  {"rel":"cancel","href": http://localhost:8080/hateoas-1-SNAPSHOT/bets/0} ],
 "bet":{"id":0,"marketId":1,"selectionId":22,"price":4.0,"stake":2.0,"type":"BACK","status":"UNMATCHED"}


The client can therefore use the self link for retrieving and updating the  Bet, and also the cancel link to effectively delete it.

This post describes just some of the functionality of the Spring-HATEOAS project which is evolving all the time. For an up to date and more detailed explanation, visit the  GitHub pages.




Understand the needs and benefits around implementing the right monitoring solution for a growing containerized market. Brought to you in partnership with AppDynamics.

Topics:

Published at DZone with permission of Geraint Jones, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}