Thursday, July 9, 2015

Tutorial: Writing tests with Spock

Overview:
This article looks at how to use Spock for writing tests. It's a yet another awesome project from google code.

Why Spock? Read here.

For me, it's the simplicity, expressiveness and less code to write, which makes the tests clean. Also, in-built BDD notations such as given, when, then, where etc.

You can find the sample code for this application here.

Prerequisite:
To run the application, you need the following installed.
Basic understanding on Groovy
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>  
   <groupId>org.fazlan.blogger</groupId>  
   <artifactId>spock-sample</artifactId>  
   <version>1.0-SNAPSHOT</version>  
   <build>  
     <plugins>  
       <plugin>  
         <groupId>org.apache.maven.plugins</groupId>  
         <artifactId>maven-compiler-plugin</artifactId>  
         <version>${maven-compiler-plugin.version}</version>  
         <configuration>  
           <source>${jdk.version}</source>  
           <target>${jdk.version}</target>  
         </configuration>  
       </plugin>  
       <!-- Mandatory plugins for using Spock -->  
       <plugin>  
         <!-- The gmavenplus plugin is used to compile Groovy code. To learn more about this plugin -->  
         <groupId>org.codehaus.gmavenplus</groupId>  
         <artifactId>gmavenplus-plugin</artifactId>  
         <version>${gmavenplus-plugin.version}</version>  
         <executions>  
           <execution>  
             <goals>  
               <goal>compile</goal>  
               <goal>testCompile</goal>  
             </goals>  
           </execution>  
         </executions>  
       </plugin>  
       <!-- Only required if names of spec classes don't match default Surefire patterns (`*Test` etc.) -->  
       <plugin>  
         <groupId>org.apache.maven.plugins</groupId>  
         <artifactId>maven-surefire-plugin</artifactId>  
         <version>${maven-surefire-plugin.version}</version>  
         <configuration>  
           <useFile>false</useFile>  
           <includes>  
             <include>**/*Spec.java</include>  
           </includes>  
         </configuration>  
       </plugin>  
     </plugins>  
   </build>  
   <dependencies>  
     <!-- Mandatory dependencies for using Spock -->  
     <dependency>  
       <groupId>org.spockframework</groupId>  
       <artifactId>spock-core</artifactId>  
       <version>${spock-core.version}</version>  
       <scope>test</scope>  
     </dependency>  
     <!-- Optional for using Spock -->  
     <!-- use a specific Groovy version rather than the one specified by spock-core -->  
     <dependency>  
       <groupId>org.codehaus.groovy</groupId>  
       <artifactId>groovy-all</artifactId>  
       <version>${groovy-all.version}</version>  
       <scope>test</scope>  
     </dependency>  
     <!--&lt;!&ndash; enables mocking of classes (in addition to interfaces) &ndash;&gt;-->  
     <!--<dependency>-->  
       <!--<groupId>cglib</groupId>-->  
       <!--<artifactId>cglib-nodep</artifactId>-->  
       <!--<version>${cglib-nodep.version}</version>-->  
       <!--<scope>test</scope>-->  
     <!--</dependency>-->  
     <!--&lt;!&ndash; enables mocking of classes without default constructor (together with CGLIB) &ndash;&gt;-->  
     <!--<dependency>-->  
       <!--<groupId>org.objenesis</groupId>-->  
       <!--<artifactId>objenesis</artifactId>-->  
       <!--<version>${objenesis.version}</version>-->  
       <!--<scope>test</scope>-->  
     <!--</dependency>-->  
   </dependencies>  
   <properties>  
     <gmavenplus-plugin.version>1.5</gmavenplus-plugin.version>  
     <maven-surefire-plugin.version>2.18.1</maven-surefire-plugin.version>  
     <spock-core.version>1.0-groovy-2.4</spock-core.version>  
     <groovy-all.version>2.4.3</groovy-all.version>  
     <cglib-nodep.version>3.1</cglib-nodep.version>  
     <objenesis.version>2.1</objenesis.version>  
     <maven-compiler-plugin.version>3.3</maven-compiler-plugin.version>  
     <jdk.version>1.8</jdk.version>  
   </properties>  
 </project>  

Writing the test

package org.fazlan.blogger.sample

import spock.lang.Specification


class SimpleCalculatorSpec extends Specification {

    Calculator calculator

    def setup() {
        calculator = new SimpleCalculator()
    }

    def "adds numbers and returns the sum of the numbers"() {

        when:
        def result = calculator.add(* numbers)

        then:
        result == expected_result

        where:
        numbers    | expected_result
        [0]        | 0
        [1]        | 1
        [1.4, 2.5] | 3.9
    }

    def "multiplies given numbers and returns the multiplication result"() {

        when:
        def result = calculator.multiply(* numbers)

        then:
        result == expected_result

        where:
        numbers  | expected_result
        [0]      | 0
        [1]      | 1
        [1, 0]   | 0
        [1.5, 2] | 3
    }
}

As seen above, it uses less code and no configuration compared to frameworks like cucumber-jvm or jBehave.

Summary
This article looks at how to use Spock for writing tests.

You can find the sample code for this application here.

Monday, June 29, 2015

Spring Boot JPA Tutorial with Integration Testing

Overview:
This article looks at how mix JPA with Spring Boot and write integration tests that only loads the persistence layer and exercises the repository implementation.

Also, I have tried to demonstrate the usage of JPA Specifications without having to convolute the repository API with JPA query method definitions to perform numerous search operations.

You can find the sample code for this application here.

Prerequisite:
To run the application, you need the following installed.
Java 7+
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.4.RELEASE</version>  
   </parent>  
   <groupId>org.fazlan.blogger</groupId>  
   <artifactId>jpa-sample</artifactId>  
   <version>1.0-SNAPSHOT</version>  
   <properties>  
     <skip.utests>false</skip.utests>  
     <skip.itests>true</skip.itests>  
     <testng.version>6.9.4</testng.version>  
     <assertj.version>2.1.0</assertj.version>  
     <hibernate-jpamodelgen.version>4.3.10.Final</hibernate-jpamodelgen.version>  
   </properties>  
   <dependencies>  
     <dependency>  
       <groupId>org.springframework.boot</groupId>  
       <artifactId>spring-boot-starter-data-jpa</artifactId>  
     </dependency>  
     <!--test related dependencies-->  
     <dependency>  
       <groupId>org.springframework.boot</groupId>  
       <artifactId>spring-boot-starter-test</artifactId>  
       <scope>test</scope>  
     </dependency>  
     <dependency>  
       <groupId>org.testng</groupId>  
       <artifactId>testng</artifactId>  
       <version>${testng.version}</version>  
       <scope>test</scope>  
     </dependency>  
     <dependency>  
       <groupId>org.assertj</groupId>  
       <artifactId>assertj-core</artifactId>  
       <version>${assertj.version}</version>  
       <scope>test</scope>  
     </dependency>  
     <!--sample specification metadata generator-->  
     <dependency>  
       <groupId>org.hibernate</groupId>  
       <artifactId>hibernate-jpamodelgen</artifactId>  
       <version>${hibernate-jpamodelgen.version}</version>  
     </dependency>  
     <!-- database for integration tests-->  
     <dependency>  
       <groupId>org.hsqldb</groupId>  
       <artifactId>hsqldb</artifactId>  
       <scope>runtime</scope>  
     </dependency>  
   </dependencies>  
   <build>  
     <plugins>  
       <plugin>  
         <artifactId>maven-compiler-plugin</artifactId>  
         <configuration>  
           <source>1.7</source>  
           <target>1.7</target>  
           <compilerArguments>  
             <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>  
           </compilerArguments>  
         </configuration>  
       </plugin>  
       <plugin>  
         <groupId>org.codehaus.mojo</groupId>  
         <artifactId>build-helper-maven-plugin</artifactId>  
         <executions>  
           <execution>  
             <id>add-source</id>  
             <phase>generate-sources</phase>  
             <goals>  
               <goal>add-source</goal>  
             </goals>  
             <configuration>  
               <sources>  
                 <source>${project.build.directory}/generated-sources/annotations/</source>  
               </sources>  
             </configuration>  
           </execution>  
         </executions>  
       </plugin>  
       <plugin>  
         <artifactId>maven-surefire-plugin</artifactId>  
         <configuration>  
           <skipTests>${skip.utests}</skipTests>  
         </configuration>  
       </plugin>  
       <plugin>  
         <artifactId>maven-failsafe-plugin</artifactId>  
         <executions>  
           <execution>  
             <id>integration-test</id>  
             <goals>  
               <goal>integration-test</goal>  
             </goals>  
             <configuration>  
               <skipTests>${skip.itests}</skipTests>  
               <includes>  
                 <include>**/*ITSpec.class</include>  
               </includes>  
             </configuration>  
           </execution>  
         </executions>  
       </plugin>  
     </plugins>  
   </build>  
   <profiles>  
     <profile>  
       <id>integration</id>  
       <properties>  
         <skip.utests>true</skip.utests>  
         <skip.itests>false</skip.itests>  
       </properties>  
     </profile>  
   </profiles>  
 </project>   

Defining the Entity classes
Following is a partial view of the City entity annotated.

package org.fazlan.blogger.sample.domain.entity;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "CITIES")
public class City implements Serializable {

    @Id
    @GeneratedValue
    @Column(name = "CITY_ID", nullable = false, unique = true)
    private Long id;

    @Column(name = "NAME", nullable = false)
    private final String name;

    @Column(name = "STATE", nullable = false)
    private final String state;

    @Column(name = "COUNTRY", nullable = false)
    private final String country;

    // rest of the code is omitted for clarity
}

Then we construct a City instance using Builder patter as follows,

package org.fazlan.blogger.sample.domain.entity;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "CITIES")
public class City implements Serializable {

    // rest of the code is omitted for clarity 

    @SuppressWarnings("unused")
    private City() {
        this(builder(null, null));
    }

    private City(Builder builder) {
        this.name = builder.name;
        this.state = builder.state;
        this.country = builder.country;
    }

    // getter/setter code is omitted for clarity 

    public static Builder builder(String name, String country) {
        return new Builder(name, country);
    }

    public static class Builder {
        private String name;
        private String state;
        private String country;

        public Builder(String name, String country) {
            this.name = name;
            this.country = country;
        }

        public Builder state(String state) {
            this.state = state;
            return this;
        }

        public City build() {
            return new City(this);
        }
    }
}

Using Builder pattern it is easy to construct immutable objects without having lengthy constructor arguments.

Defining the Repository classes
Our Repository extends org.springframework.data.jpa.repository.JpaRepository and
org.springframework.data.jpa.repository.JpaSpecificationExecutor;


package org.fazlan.blogger.sample.domain.repo;

import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface CityRepository 
extends JpaRepository<City, Long>, JpaSpecificationExecutor<City> {
}

Writing an Integration Test Specification
Defining our integration properties

application-integration.properties - where "integration" is the profile under test ( application-{profile}.properties )


spring.datasource.url: jdbc:hsqldb:mem:integration


Since we're only interested to test the repositories and the associated entities, let's load only the context related to persistence, Doing so, we are not loading the entire application context, which can help to speed up our tests at least my one nanosecond.

package org.fazlan.blogger.sample.domain;

import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@Configuration
@EnableJpaRepositories(basePackages = "org.fazlan.blogger.sample.domain.repo")
@EntityScan(basePackages = "org.fazlan.blogger.sample.domain.entity")
@Import({ DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class })
public class DomainTestConfig {
}

Above, ensures only the repositories defined in "org.fazlan.blogger.sample.domain.repo" are loaded, and entities defined in are "org.fazlan.blogger.sample.domain.entity" loaded.

To setup the data required for out test, lets create a Test Fixture (much like an Object Mother). This will be used by our test.

package org.fazlan.blogger.sample.domain.repo;

import org.fazlan.blogger.sample.domain.entity.City;

class CityRepositoryFixture {

    static final String CITY_MELBOURNE = "Melbourne";
    static final String CITY_SYDNEY = "Sydney";
    static final String STATE_NSW = "NSW";
    static final String STATE_VICTORIA = "Victoria";
    static final String COUNTRY_AUSTRALIA = "Australia";

    static City getMelbourne() {
        return City.builder(CITY_MELBOURNE, COUNTRY_AUSTRALIA).state(STATE_VICTORIA).build();
    }

    static City getSydney() {
        return City.builder(CITY_SYDNEY, COUNTRY_AUSTRALIA).state(STATE_NSW).build();
    }

    static City getAustralianCities() {
        return City.builder(null, COUNTRY_AUSTRALIA).build();
    }
}

Now that we have the text fixture, lets write a test to exercise our wiring of entities with repositories.

package org.fazlan.blogger.sample.domain.repo;

import org.fazlan.blogger.sample.domain.DomainTestConfig;
import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@SpringApplicationConfiguration(classes = DomainTestConfig.class)
public class CityRepositoryITSpec 
       extends AbstractTransactionalTestNGSpringContextTests {

    @Autowired
    private CityRepository repository;

    private City melbourne;

    @BeforeMethod
    public void beforeEachTest() {
        melbourne = CityRepositoryFixture.getMelbourne();
    }

    @Test
    public void creates_city_given_name_and_state_country() {
        Long cityId = repository.save(melbourne).getId();

        assertThat(repository.getOne(cityId)).isEqualTo(melbourne);
    }

}

To run the tests, simply run

mvn clean integration-test -P integration

Above tests if a city gets created as expected. Now lets try to test retrieval of cities. Lets assume we need to retrieve a city given its name and country. Also, all cities in a given country. This could have achieved via JPA Query methods such as follows,

package org.fazlan.blogger.sample.domain.repo;

import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CityRepository extends JpaRepository<City, Long>, JpaSpecificationExecutor<City> {
   City findByNameAndCountry(String name, String country);

   List<City> findByCountry(String country);
}

Doing so, works fine. However, it convolutes the Repository interface when we need to add more query methods to retrieve cities based on different criteria. Better alternative to this would be to use method that accepts a common interface and later can be passed with different implementations to do the same ( Strategy Pattern ).

org.springframework.data.jpa.repository.JpaSpecificationExecutor interface allows exactly that as follows,

package org.springframework.data.jpa.repository;

. . .
import org.springframework.data.jpa.domain.Specification;

public interface JpaSpecificationExecutor<T> {

 T findOne(Specification<T> spec);

 List<T> findAll(Specification<T> spec);

 // rest omitted for clarity
}

We only have to extend org.springframework.data.jpa.domain.Specification in our repository to access these methods. Once extends, we can write our custom search specifications without changing the Repository interface.

To generate metadata model required by the Specification to ensure type safety of fields, we use the following maven dependency and plugin in our pom.xml

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <version>${hibernate-jpamodelgen.version}</version>
</dependency>

<plugin>
 <artifactId>maven-compiler-plugin</artifactId>
 <configuration>
    <source>1.7</source>
    <target>1.7</target>
    <compilerArguments>
      <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
    </compilerArguments>
 </configuration>
</plugin>

This generates the metadata model denoted by <Entity>_.java (e.i: City_.java , which is later used in the Specifications as follows)

Defining custom search specifications.
Our search specifications

package org.fazlan.blogger.sample.domain.spec;

import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.data.jpa.domain.Specification;

abstract class CitySpec implements Specification<City> {

    protected final City criteria;

    protected CitySpec(City city) {
        this.criteria = city;
    }
}
class CityByCountry extends CitySpec {

    public CityByCountry(City city) {
        super(city);
    }

    @Override
    public Predicate 
           toPredicate(Root<City> city, CriteriaQuery<?> cq, CriteriaBuilder cb) {
        return cb.equal(city.get(City_.country), criteria.getCountry());
    }
}
class CityByNameCountry extends CitySpec {

    CityByNameCountry(City city) {
        super(city);
    }

    @Override
    public Predicate 
           toPredicate(Root<City> city, CriteriaQuery<?> cq, CriteriaBuilder cb) {
        return cb.and(
                cb.equal(city.get(City_.name), criteria.getName()),
                cb.equal(city.get(City_.country), criteria.getCountry())
        );
    }
}

NOTE: We have made the implementations to package access level. So, we have to create a factory to return different creteria via a public interface.

package org.fazlan.blogger.sample.domain.spec;

import org.fazlan.blogger.sample.domain.entity.City;
import org.springframework.data.jpa.domain.Specification;

public class CitySpecFactory {

    private CitySpecFactory() {}

    public static Specification<City> byNameAndCountry(City city) {
        return new CityByNameCountry(city);
    }

    public static Specification<City> byCountry(City city) {
        return new CityByCountry(city);
    }
}

Now we can write test to exercise the retrieval of cities

@SpringApplicationConfiguration(classes = DomainTestConfig.class)
public class CityRepositoryITSpec 
       extends AbstractTransactionalTestNGSpringContextTests {

    //rest are omitted for clarity

    @Test
    public void finds_city_given_name_and_country() {
        repository.save(melbourne);

        City result = repository.
                             findOne(CitySpecFactory.
                             byNameAndCountry(melbourne));

        assertThat(result).isEqualTo(melbourne);
    }

    @Test
    public void finds_all_cities_given_country() {
        City sydney = CityRepositoryFixture.getSydney();
        repository.save(Arrays.asList(melbourne, sydney));

        List<City> results = repository.
                              findAll(CitySpecFactory.
                              byCountry(CityRepositoryFixture.
                              getAustralianCities()));

        assertThat(results).containsAll(Arrays.asList(melbourne, sydney));
    }
}

Summary
This article looks at how mix JPA with Spring Boot and write integration tests that only loads the persistence layer and exercises the repository implementation.

Also, I have tried to demonstrate the usage of JPA Specifications without having to convolute the repository API with JPA query method definitions to perform numerous search operations.

You can find the sample code for this application here.

Saturday, May 16, 2015

Modularising AngularJS Web front using CommonJS

Overview:

This article tries to demonstrate how AngularJS and Twitter Bootstrap can be used to implement a web application that looks slick and modular.

The source and instructions can be found

https://github.com/fazlan/realestate-demo-angular-app

You can view the Online demo

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.