Thursday, February 19, 2015

HATEOAS REST APIs with Springboot Application

Overview:

This article will look at the basic concept behind HATEOAS(HATE-OAS). Basically, the response of your API will consist of hypermedia (URLs) as the means to navigate between resources(you will realise what I mean further down the line of this article). The article by no means tries to describe about RESTful API design(i.e. root entry point of the API, how well the API is documented and so forth). However, behind every well structured and organised REST API, HATEOAS is evident.

This article tries to show a simple application using Spring's HATEOAS library to implement as application.

You can find the sample code for this application here.

Prerequisite:

To run the application, you need the following installed.
Java 8
Maven 3

The pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 
 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.2.1.RELEASE</version>
 </parent>
 
 <groupId>org.fazlan</groupId>
 <artifactId>springboot.sample.hateoas.app</artifactId>
 <name>Spring Boot Sample Hateoas Application</name>
 <version>1.0.SNAPSHOT</version>
 <description>Spring Boot Sample Hateoas Application</description>

 <properties>
  <!-- The main class to start by executing java -jar -->
  <start-class>org.fazlan.hateoas.SampleHateoasSslApplication</start-class>
  <spring.boot.version>1.2.1.RELEASE</spring.boot.version>
        <spring-hateoas.version>0.16.0.RELEASE</spring-hateoas.version>
        <maven-compiler-plugin.version>3.2</maven-compiler-plugin.version>
        <java.version>1.8</java.version>
    </properties>
 
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.hateoas</groupId>
            <artifactId>spring-hateoas</artifactId>
            <version>${spring-hateoas.version}</version>
        </dependency>
 </dependencies>
 <build>
  <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven-compiler-plugin.version}</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>${spring.boot.version}</version>
   </plugin>
  </plugins>
 </build>
</project>

The REST Resources from the API:

org.fazlan.hateoas.web.comment.CommentResource.java


package org.fazlan.hateoas.web.comment;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.hateoas.ResourceSupport;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

public class CommentResource extends ResourceSupport {

    private Comment comment;
    private String text;

    @JsonCreator
    public CommentResource(@JsonProperty("text") String text) {
        this.text = text;
    }

    public CommentResource(Comment comment) {
        this.comment = comment;
    }

    public CommentResource withSefRef() {
        add(linkTo(methodOn(CommentController.class).getComment(comment.getId())).withSelfRel());
        return this;
    }

    public CommentResource withRel(String rel) {
        add(linkTo(methodOn(CommentController.class).getComment(comment.getId())).withRel(rel));
        return this;
    }

    public String getText() {
        if (text != null)
            return text;
        return comment.getText();
    }
}

org.fazlan.hateoas.web.post.PostResource.java


package org.fazlan.hateoas.web.post;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

public class PostResource extends ResourceSupport {

    private String content;
    private Post post;

    @JsonCreator
    public PostResource(@JsonProperty("content") String content) {
        this.content = content;
    }

    public PostResource(Post post) {
        this.post = post;
        add(linkTo(methodOn(PostController.class).getPost(post.getPid())).withSelfRel());
    }

    public String getContent() {
        if (content != null)
            return content;
        return post.getContent();
    }

    public Link getComments() {
        return  linkTo(methodOn(PostController.class).getComments(post.getPid())).withRel("comments");
    }
}

The REST Controllers:

org.fazlan.hateoas.web.post.PostController.java


UrlMethodDescription
/postsPOSTCreates a post
/posts/{id}/commentsPOSTAdds a comment to a given post
/postsGETLists all posts
/posts/{id}GETRetrieves a given post
/posts/{id}/commentsGETLists all comments associated with a given post


package org.fazlan.hateoas.web.post;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.fazlan.hateoas.repo.CommentRepo;
import org.fazlan.hateoas.repo.PostRepo;
import org.fazlan.hateoas.web.comment.CommentResource;

import java.util.List;

import static java.util.stream.Collectors.toList;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

@Controller
@RequestMapping("/posts")
public class PostController {

    @Autowired
    private PostRepo repo;

    @Autowired
    private CommentRepo commentRepo;

    @RequestMapping(method = POST)
    @ResponseBody
    public Link createPost(@RequestBody PostResource post) {
        return getPost(repo.create(post)).getId();
    }

    @RequestMapping(value = "/{id}", method = GET)
    @ResponseBody
    public PostResource getPost(@PathVariable("id") Integer id) {
        return new PostResource(repo.get(id));
    }

    @RequestMapping(method = GET)
    @ResponseBody
    public List<Link> listPosts() {
        return repo.list().stream()
                .map(PostResource::new)
                .map(PostResource::getId)
                .collect(toList());
    }

    @RequestMapping(value = "/{id}/comments", method = POST)
    @ResponseBody
    public PostResource addComment(@PathVariable("id") Integer postId, @RequestBody CommentResource comment) {
        repo.get(postId).addComment(commentRepo.get(commentRepo.create(comment)));
        return getPost(postId);
    }

    @RequestMapping(value = "/{id}/comments", method = GET)
    @ResponseBody
    public List<Link> getComments(@PathVariable("id") Integer postId) {
        return repo.get(postId).getComments()
                .stream()
                .map(CommentResource::new)
                .map(cr -> cr.withRel("comment").getLink("comment"))
                .collect(toList());
    }
}

org.fazlan.hateoas.web.comment.CommentController.java

UrlMethodDescription
/commentsGETLists all comments
/comments/{id}GETRetrieves a given comment


package org.fazlan.hateoas.web.comment;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Link;
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.ResponseBody;
import org.fazlan.hateoas.repo.CommentRepo;

import java.util.List;

import static java.util.stream.Collectors.toList;
import static org.springframework.web.bind.annotation.RequestMethod.GET;

@Controller
@RequestMapping("/comments")
public class CommentController {

    @Autowired
    private CommentRepo repo;

    @RequestMapping(method = GET)
    @ResponseBody
    public List<Link> listComments() {
        return repo.list().stream()
                .map(CommentResource::new)
                .map(CommentResource::withSefRef)
                .map(CommentResource::getId)
                .collect(toList());
    }

    @RequestMapping(value = "/{id}", method = GET)
    @ResponseBody
    public CommentResource getComment(@PathVariable("id") Integer id) {
        return new CommentResource(repo.get(id)).withSefRef();
    }
}

Running the Application

$ mvn clean spring-boot:run

Access the application at https://localhost:8443/posts Initially, this will return an empty list as there will be no posts. Lets create some resources now.

Running the Application
I'have used Advanced Rest Client Google plugin to create some resources as shown in the snapshots

1. Creating a Post

2. Creating several associated Comment resources


Navigating through the newly created resources via the hypermedia

1. Listing the Posts

2. Retrieving a given Post

3. Listing all Comments associated with a given Post

4. Retrieving a given Comment associated with a given Post


Summary

This article was a briefing of how to use Spring's HATEOAS with spring-boot to write a very simple RESTful API. Hope this will encourage you to explore more on HATEOAS and whether it suites your needs when writing well designed RESTful APIs.

You can find the sample code for this application here.