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

Building a HATEOAS Hypermedia RESTful Record Store Web Service with Spring

DZone's Guide to

Building a HATEOAS Hypermedia RESTful Record Store Web Service with Spring

· Integration Zone
Free Resource

Share, secure, distribute, control, and monetize your APIs with the platform built with performance, time-to-value, and growth in mind. Free 90 day trial 3Scale by Red Hat

Introduction

  • This is a very simple example of developing a hypermedia-driven RESTful web service, using Spring HATEOAS
  • A companion project is available to download on github (using Java, Maven and Spring Boot)



HATEOAS? What is it?

  • HATEOAS ("Hypermedia as the Engine of Application State") is an approach to building RESTful web services, where the client can dynamically discover the actions available to it at runtime from the server. All the client should require to get started is an initial URI, and set of standardised media types. Once it has loaded the initial URI, all future application state transitions will be driven by the client selecting from choices provided by the server. 
  • The choices of available actions may be driven by state in time (e.g. different actions available to the user based on access control or stateful data through workflow or previous actions), or by available functionality (e.g. as new functionality becomes available - the client can hook into them with no design changes on its part)

Where did it come from?

  • HATEOAS constraint is an essential part of the "uniform interface" feature of REST, as defined in Roy Fielding's doctoral dissertation (Ray was one of the principal authors of the HTTP specification, and co-founder of the Apache HTTP server project)
  • In his view if an API is not being driven by hypertext, then it cannot be RESTful

The Record Store Spring Example

  • Purchase a copy of the Album:
    • http://localhost:8080/album/purchase/{id}
    • Note: this action is only displayed next to an album when the stock level is > 0 (an example of how the server will control the transitions available to the client based on current application state)
  • Look at the example below - first call to view album '3' shows it is available to purchase:

The Code

Application.java

package com.cor;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

/**
 * Spring Boot launch file.
 */
@ComponentScan
@EnableAutoConfiguration
public class Application {

   public static void main(String[] args) {
      SpringApplication.run(Application.class, args);
   }

}

Album.java

package com.cor.domain;

/**
 * Album (available to purchase - hence 'stockLevel' attribute).
 *
 */
public class Album {

   private final String id;
   private final String title;
   private final Artist artist;
   private int stockLevel;

   public Album(final String id, final String title, final Artist artist, int stockLevel) {
      this.id = id;
      this.title = title;
      this.artist = artist;
      this.stockLevel = stockLevel;
   }

   public String getId() {
       return id;
   }

   public String getTitle() {
       return title;
   }

   public Artist getArtist() {
      return artist;
   }

   public int getStockLevel() {
      return stockLevel;
   }

   public void setStockLevel(int stockLevel) {
      this.stockLevel = stockLevel;
   }
}

Artist.java

package com.cor.domain;

/**
 * Music Artist/Group.
 *
 */
public class Artist {

   private final String id;
   private final String name;

   public Artist(final String id, final String name) {
      this.id = id;
      this.name = name;
   }

   public String getId() {
      return id;
   }

   public String getName() {
      return name;
   }

}

MusicService.java

package com.cor.service;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.cor.domain.Album;
import com.cor.domain.Artist;

/**
 * Hard coded simulation of a Service + Data Access Layer.
 */
@Service
public class MusicService {

   private Map albums;
   private Map artists;

   /**
    * Constructor populates the 'database' (i.e. Maps) of Artists and Albums.
    */
   public MusicService() {

      albums = new HashMap<String, Album>();
      artists = new HashMap<String, Album>();

      Artist artist1 = new Artist("opeth", "Opeth");
      Artist artist2 = new Artist("cfrost", "Celtic Frost");
      artists.put(artist1.getId(), artist1);
      artists.put(artist2.getId(), artist2);

      Album album1 = new Album("1", "Heritage", artist1, 2);
      Album album2 = new Album("2", "Deliverance", artist1, 3);
      Album album3 = new Album("3", "Pale Communion", artist1, 0);
      Album album4 = new Album("3", "Monotheist", artist2, 1);
      albums.put(album1.getId(), album1);
      albums.put(album2.getId(), album2);
      albums.put(album3.getId(), album3);
      albums.put(album4.getId(), album4);

   }

   public Collection getAllAlbums() {
      return albums.values();
   }

   public Album getAlbum(final String id) {
      return albums.get(id);
   }

   public Artist getArtist(final String id) {
      return artists.get(id);
   }
}
  • This is just a mock service that simulates a service/data access layer
  • It provides some methods to retrieve Album and Artist data, and also to purchase an Album (which for now will just reduce the stock level by 1)

ArtistController.java

package com.cor.controller;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Resource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cor.domain.Artist;
import com.cor.service.MusicService;

@Controller
public class ArtistController {
@Autowired
private MusicService musicService;

   @RequestMapping(value = "/artist/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
   @ResponseBody
   public Resource<Artist> getArtist(@PathVariable(value = "id") String id) {
Artist a = musicService.getArtist(id);
      Resource<Artist> resource = new Resource(a);
      resource.add(linkTo(methodOn(ArtistController.class).getArtist(id)).withSelfRel());
      return resource;
   }

}
  • Spring web controller for Artist operations

AlbumController.java

package com.cor.controller;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Resource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cor.domain.Album;
import com.cor.service.MusicService;

@Controller
public class AlbumController {

@Autowired
private MusicService musicService;

   @RequestMapping(value = "/albums", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
   @ResponseBody
   public Collection<Resource<Album>> getAllAlbums() {

      Collection albums = musicService.getAllAlbums();
      List<Resource<Album>> resources = new ArrayList<Resource<Album>>();
      for (Album album : albums) {
         resources.add(getAlbumResource(album));
      }
      return resources;

   }

   @RequestMapping(value = "/album/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
   @ResponseBody
   public Resource<Album> getAlbum(@PathVariable(value = "id") String id) {

      Album album = musicService.getAlbum(id);
      return getAlbumResource(album);

   }

   private Resource<Album> getAlbumResource(Album album) {

      Resource<Album> resource = new Resource<Album>(album);

      // Link to Album
      resource.add(linkTo(methodOn(AlbumController.class).getAlbum(album.getId())).withSelfRel());
      // Link to Artist
      resource.add(linkTo(methodOn(ArtistController.class).getArtist(album.getArtist().getId())).withRel("artist"));
      // Option to purchase Album
      if (album.getStockLevel() > 0) {
          resource.add(linkTo(methodOn(AlbumController.class).purchaseAlbum(album.getId())).withRel("album.purchase"));
      }

      return resource;

   }

   @RequestMapping(value = "/album/purchase/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
   @ResponseBody
   public Resource<Album> purchaseAlbum(@PathVariable(value = "id") String id) {

      Album a = musicService.getAlbum(id);
      a.setStockLevel(a.getStockLevel() - 1);
      Resource<Album> resource = new Resource<Album>(a);
      resource.add(linkTo(methodOn(AlbumController.class).getAlbum(id)).withSelfRel());
      return resource;

   }
}
  • Spring web controller for Album operations
  • Note how in here a check is made on stock Levels in determining whether to allow the client to make a purchase

Building and running the example

Building

  • You will require Maven installed and configured (see .... for more details on this)
  • The example project uses Spring Boot to run the example. This basically builds a JAR file and spins up an embedded Tomcat container using a very simple command (so no WAR file is built, and no deployment to an external server is needed). This is great for getting up and running quickly - all you need is Maven and a JDK.

Running

Running with Eclipse STS

  • If you use Eclipse STS - you can just run the project as a Spring Boot App (just right-click the project in the package explorer and click 'Run As .. Spring Boot App'

Running with Maven

  • On the command line, navigate to the project folder root (containing pom.xml), and use 'mvn clean install spring-boot:run'

Either of these will start the Spring Boot deployment, and you should see a screen similar to this:

Changing the Embedded Tomcat Port

  • Just change the 'server.port' setting in application.properties

Further Reading

 - http://en.wikipedia.org/wiki/HATEOAS

 - http://en.wikipedia.org/wiki/Roy_Fielding

 - http://projects.spring.io/spring-hateoas/

 - http://roy.gbiv.com/

Discover how you can achielve enterpriese agility with microservices and API management

Topics:
java ,enterprise-integration ,web services ,spring ,rest ,hateaos

Published at DZone with permission of Adrian Milne, 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 }}