Block Image

In this article we will put into practice what we said in the post Richardson Maturity Model and REST levels, i.e. we will implement a RESTful Web Service Level 3.

To do this, we will use Spring HATEOAS. This Spring module will allow us to create a Web Service REST conforming to level 3 of the Richardson Maturity Model.

Prerequisites

  1. Set up a jdk (we will use the version 8, but you can use successive versions).
  2. Installing maven (https://maven.apache.org/install.html).
  3. Get to know the Richardson Maturity Model..

First step: import the following Maven dependencies

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

We use JPA for CRUD operations carried out by REST services, we use the Lombok library to auto-generate getters, setters and others usual methods of a Javabean and finally we use H2 as a database.

Second step: let's create the entities User and Car

We create a subpackage entities and there we write the classes that will map the USER and CAR tables:

@Entity
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String surname;

    //tax code - codice fiscale
    @Column(unique = true, nullable = false)
    @EqualsAndHashCode.Include
    private String code;

    private String address;

    @OneToMany(mappedBy = "user", cascade = { CascadeType.PERSIST, CascadeType.MERGE})
    @ToString.Exclude
    private List<Car> cars = new ArrayList<>();
}
@Entity
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
public class Car implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    @EqualsAndHashCode.Include
    private String plate;

    private String name;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

Let's analyze the code:

  1. With @Entity we are indicating to JPA that the class will map a database table.
  2. @Getter, @Setter, @EqualsAndHashCode and @ToString are Lombok's annotations that allow us to self-generate the getter, setter, equals/hashcode and toString methods respectively. With onlyExplicitlyIncluded = true we also indicate to Lombok that only the fields annotated with @EqualsAndHashCode.Include must be compared in the equals and hashcode methods.
  3. With @Column(unique = true, nullable = false) we tell JPA that the code field of User and the plate field of Car must be UNIQUE and non-null.
  4. With @OneToMany and @ManyToOne we create the one to many bi-directional relationship between User and Car.
Note 1: The Hibernate documentation recommends to map in the equals and hashcode methods the fields that in the db will be UNIQUE.
Note 2: All fields of a class annotated with @Entity will be automatically mapped as columns on the db. Use the @Column annotation only when we need to specify attributes within the annotation, as unique, nullable. Moreover, these two attributes only serve in the db creation phase, i.e. they work only if we have JPA create the tables, otherwise they are unused by the framework.

Third step: I create entities repositories with Spring Data Repository

Inside the package repos, we write the 2 User and Car repositories interfaces:

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByCode(String code);
}
public interface CarRepository extends JpaRepository<Car, Long> {

    Optional<Car> findByPlate(String plate);
}

Note that for UserRepository we added the findByCode method and for CarRepository the findByPlate method. This is because we prefer that you can search for a single entity through its domain field rather than its id. In doing so, we hide a db implementation from the client (what is the primary key for each entity, what type it is, which could be the primary key of another entity) and we have advantages in terms of security.


Fourth step: let's create the DTOs

Within the package dtos, we create User and Car DTOs.

@Relation(collectionRelation = "users")
@Data
@AllArgsConstructor
public class UserDTO extends RepresentationModel<UserDTO> {

    private String name;

    private String surname;

    private String code;

    private String address;
}
@Relation(collectionRelation = "cars")
@Data
@AllArgsConstructor
public class CarDTO extends RepresentationModel<CarDTO> {


    private String plate;

    private String name;

}

Note that both DTOs extend the Spring HATEOAS RepresentationModel.\ library class. Extending it, we can add the links in the JSONs that will map these Java classes, typical of the REST level 3 services (we are practically integrating HATEOAS).
With @Relation(collectionRelation = "users") we tell HATEOAS the root name that we will have to display in the JSON when we display the collection related to the User class.

We also omit the id field as we do not want to give this information to the client.

Fifth step: let's write the services

We create the services for User and Car in the package services. We report here only the one related to User, but at the end of the post you will find the link of the complete project on my GitHub.

@Service
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    private final UserAssembler userAssembler;

    private final CarAssembler carAssembler;

    private final PagedResourcesAssembler pagedResourcesAssembler;


    public UserServiceImpl(UserRepository userRepository, UserAssembler userAssembler, PagedResourcesAssembler pagedResourcesAssembler, CarAssembler carAssembler) {
        this.userRepository = userRepository;
        this.userAssembler = userAssembler;
        this.carAssembler = carAssembler;
        this.pagedResourcesAssembler = pagedResourcesAssembler;
    }


    @Override
    public CollectionModel<UserDTO> findAll(int page, int size, String[] sort, String dir) {
        PageRequest pageRequest;
        Sort.Direction direction;
        if(sort == null) {
            pageRequest = PageRequest.of(page, size);
        }
        else {
            if(dir.equalsIgnoreCase("asc")) direction = Sort.Direction.ASC;
            else direction = Sort.Direction.DESC;
            pageRequest = PageRequest.of(page, size, Sort.by(direction, sort));
        }
        Page<User> users = userRepository.findAll(pageRequest);
        if(! CollectionUtils.isEmpty(users.getContent())) return pagedResourcesAssembler.toModel(users, userAssembler);
        return null;
    }

    @Override
    public UserDTO findByCode(String code) {
        User user = userRepository.findByCode(code).orElse(null);
        if(user != null) {
            return userAssembler.toModel(user);
        }
        return null;
    }

    @Override
    public CollectionModel<CarDTO> findUserCars(String code) {
        User user = userRepository.findByCode(code).orElse(null);
        if(user != null && (! CollectionUtils.isEmpty(user.getCars())) ) {
            return carAssembler.toCollectionModel(user.getCars());
        }
        return null;
    }

    @Transactional
    @Override
    public UserDTO insert(User user) {
        user.getCars().forEach(car -> car.setUser(user));
        return userAssembler.toModel(userRepository.save(user));
    }
}

Let's analyze the code:

  1. We declare two variables of type UserAssembler and CarAssembler. In the next steps we will see the implementation of these 2 classes. For now We just need to know that they are Converters used by Spring HATEOAS to map entities in DTOs.
  2. We use a variable of type PagedResourcesAssembler of Spring, which allows us to insert in the JSON the information related to the pagination.
  3. The findAll method accepts in input an integer page representing the page number (from 0 to n), an integer size indicating the number of entities inside the page, a string sort (optional) that indicates a possible field on which the pagination must be ordered and finally a string dir that can respectively be asc or desc (ascending or descending order).
  4. The other methods are trivial and do not deserve a detailed analysis.

Sixth step: let's write the resources of User and Car

Within the package resources, we create User and Car RestControllers. Also here we report only the User one:

@RestController
@RequestMapping("/users")
public class UserResource {

    private UserService userService;

    public UserResource(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public ResponseEntity findAll(@RequestParam(required = false, defaultValue = "0") Integer page,
                                  @RequestParam(required = false, defaultValue = "3") Integer size,
                                  @RequestParam(required = false) String[] sort,
                                  @RequestParam(required = false, defaultValue = "asc") String dir) {

        CollectionModel<UserDTO> users = userService.findAll(page, size, sort, dir);
        if(users != null) {
            return ResponseEntity.ok(users);
        }
        return ResponseEntity.noContent().build();
    }


    @GetMapping("/{code}")
    public ResponseEntity findByCode(@PathVariable String code) {
        UserDTO userDTO = userService.findByCode(code);
        if(userDTO == null)  return ResponseEntity.notFound().build();

        return ResponseEntity.ok(userDTO);
    }

    @GetMapping("/{code}/cars")
    public ResponseEntity findUserCars(@PathVariable String code) {
        CollectionModel<CarDTO> cars = userService.findUserCars(code);
        if(cars != null) return ResponseEntity.ok(cars);

        return ResponseEntity.noContent().build();
    }

    @PostMapping
    public ResponseEntity insertUser(@RequestBody User user) {
        UserDTO userDTO = userService.insert(user);
        return ResponseEntity //
                .created(userDTO.getRequiredLink(IanaLinkRelations.SELF).toUri()) //
                .body(userDTO);
    }
}

This class trivially does not contain logic, but simply recalls the business logic of services.
The User resource is also mapped with /users:

  1. The findAll is mapped as a GET method with the URL /users. If the service returns a non-empty collection, we have an HTTP Response 200 OK, otherwise a 204 No Content.
  2. The findByCode is mapped as GET method with the URL /users/{code}. If the resource is found, the service returns a 200 OK with the body of the entity, otherwise a 404 Not Found is returned.
  3. The GET /users/{code}/cars method is used to search for cars of a user with input code.
  4. he insertUser method is mapped with the URL /users (POST). It returns a 201 Created, with empty body and in the header is added a Location field with a value equal to the URL of the newly created resource. In addition, it is given the possibility to insert a User together with his carts thanks to JPA's Cascade.

The PUT and DELETE methods are trivial so we omit them. These services dedicated to the user resource fully respect Richardson Maturity Model level 3 best practices!

Seventh step: let's write (finally) the Assemblers classes User and Car

Inside the package assemblers we write the UserAssembler and CarAssembler classes that act as Converters "entity to dto". (we also report here only that of User):

@Component
public class UserAssembler implements RepresentationModelAssembler<User, UserDTO> {


    @Override
    public UserDTO toModel(User entity) {
        UserDTO userDTO = new UserDTO(entity.getName(), entity.getSurname(), entity.getCode(), entity.getAddress());
        userDTO.add(linkTo(methodOn(UserResource.class).findUserCars(entity.getCode())).withRel("cars"));
        userDTO.add(linkTo(methodOn(UserResource.class).findByCode(entity.getCode())).withSelfRel());
        return userDTO;
    }
}

Very simply, given an entity User, we create with the constructor a UserDTO and add, through add methods, linkTo, methodOn, the link to get the User's cars and the self one. In addition, the class contains the default toCollectionModel method that allows you to map a collection of entities into a collection of DTOs.

Eighth step: let's create an application.properties and a data.sql file

application.properties:

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=

data.sql:

INSERT INTO user (id, name, surname, code, address) VALUES (1, 'Vincenzo', 'Racca', 'cf1', 'via Roma');
INSERT INTO user (id, name, surname, code, address) VALUES (2, 'Pippo', 'Pluto', 'cf2', 'via Chiaia');
INSERT INTO user (id, name, surname, code, address) VALUES (3, 'Gennaro', 'Esposito', 'cf3', 'via Napoli');
INSERT INTO user (id, name, surname, code, address) VALUES (4, 'Gennaro', 'Albergo', 'cf4', 'via Roma');
INSERT INTO user (id, name, surname, code, address) VALUES (5, 'Mario', 'Rossi', 'cf5', 'via Piave');

INSERT INTO car (plate, name, user_id) VALUES ('BF8013RR', 'Toyota', 1);
INSERT INTO car (plate, name, user_id) VALUES ('AF8013RR', 'Fiat', 1);
INSERT INTO car (plate, name, user_id) VALUES ('CF8013RR', 'Lancia', 2);
INSERT INTO car (plate, name, user_id) VALUES ('DF8013RR', 'Renault', 3);
INSERT INTO car (plate, name, user_id) VALUES ('EF8013RR', 'Smart', 3);

Let's analyze the file application.properties:

  1. With spring.jpa.show-sql = true we log hibernate queries.
  2. With spring.jpa.properties.hibernate.format_sql = true and spring.jpa.properties.hibernate.use_sql_comments = true we format queries so that they are clearer to see.
  3. With spring.h2.console.enabled = true we enable the H2 console at URL localhost:8080/h2-console to see the database data.

Ninth step: we test the Web Service

Let's start the main class of the project:

@SpringBootApplication
public class SpringHateoasApplication {

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

}

We call the service findAll:
localhost:8080/users, we will have the following response:

{
    "_embedded": {
        "users": [
            {
                "name": "Vincenzo",
                "surname": "Racca",
                "code": "cf1",
                "address": "via Roma",
                "_links": {
                    "cars": {
                        "href": "http://localhost:8080/users/cf1/cars"
                    },
                    "self": {
                        "href": "http://localhost:8080/users/cf1"
                    }
                }
            },
            {
                "name": "Pippo",
                "surname": "Pluto",
                "code": "cf2",
                "address": "via Chiaia",
                "_links": {
                    "cars": {
                        "href": "http://localhost:8080/users/cf2/cars"
                    },
                    "self": {
                        "href": "http://localhost:8080/users/cf2"
                    }
                }
            },
            {
                "name": "Gennaro",
                "surname": "Esposito",
                "code": "cf3",
                "address": "via Napoli",
                "_links": {
                    "cars": {
                        "href": "http://localhost:8080/users/cf3/cars"
                    },
                    "self": {
                        "href": "http://localhost:8080/users/cf3"
                    }
                }
            }
        ]
    },
    "_links": {
        "first": {
            "href": "http://localhost:8080/users?page=0&size=3"
        },
        "self": {
            "href": "http://localhost:8080/users?page=0&size=3"
        },
        "next": {
            "href": "http://localhost:8080/users?page=1&size=3"
        },
        "last": {
            "href": "http://localhost:8080/users?page=1&size=3"
        }
    },
    "page": {
        "size": 3,
        "totalElements": 5,
        "totalPages": 2,
        "number": 0
    }
}

Note that each entity in the collection has the link to know the cars of each of them and the self link. Moreover, thanks to the PagedResourcesAssembler class, the response contains links to the first page, the next and the last one, plus to have the information on the page (size, totalElements, totalPages and number).

The response is paged with 3 entities as we have in the Rest Controller these default values:

public ResponseEntity findAll(@RequestParam(required = false, defaultValue = "0") Integer page,
                              @RequestParam(required = false, defaultValue = "3") Integer size,
                              @RequestParam(required = false) String[] sort,
                              @RequestParam(required = false, defaultValue = "asc") String dir)

We can, however, customize the paging request in the following way:
/users?page=0&size=2&sort=code

We call a findByCode like this:
http://localhost:8080/users/cf1, we will have this response:

{
    "name": "Vincenzo",
    "surname": "Racca",
    "code": "cf1",
    "address": "via Roma",
    "_links": {
        "cars": {
            "href": "http://localhost:8080/users/cf1/cars"
        },
        "self": {
            "href": "http://localhost:8080/users/cf1"
        }
    }
}

We will analyze the query made by JPA from the logs:

select
    user0_.id as id1_1_,
    user0_.address as address2_1_,
    user0_.code as code3_1_,
    user0_.name as name4_1_,
    user0_.surname as surname5_1_ 
from
    user user0_ 
where
    user0_.code=?

Banally, the query is very light because it involves only the USER table. The JSON response, however, gives us the opportunity to know also the cars of this user; we make the call to the link cars that we returned the previous JSON:
http://localhost:8080/users/cf1/cars

JSON response:

{
    "_embedded": {
        "cars": [
            {
                "plate": "BF8013RR",
                "name": "Toyota",
                "_links": {
                    "user": {
                        "href": "http://localhost:8080/cars/BF8013RR/user"
                    },
                    "self": {
                        "href": "http://localhost:8080/cars/BF8013RR"
                    }
                }
            },
            {
                "plate": "AF8013RR",
                "name": "Fiat",
                "_links": {
                    "user": {
                        "href": "http://localhost:8080/cars/AF8013RR/user"
                    },
                    "self": {
                        "href": "http://localhost:8080/cars/AF8013RR"
                    }
                }
            }
        ]
    }
}

So in this way we have the pro that response bodies are always light, why you pay and why the relationships between entities are simple links. Moreover in this way the client can easily derive in cascade all the url of the entities of the application.

In contrast, the client will have to make multiple calls to REST. But since the answers are very light, the pro beat widely cons.

Conclusions

We have seen how to create a RESTful Web Service that respects the best practices of level 3 of the Richardson Maturity Model thanks to the help of Spring HATEOAS.

You can find the complete project on my github at this link: Spring HATEOAS

Posts about Spring: Spring

Recommended books about Spring:

  • Pro Spring 5 (Spring from scratch a hero): https://amzn.to/3KvfWWO
  • Pivotal Certified Professional Core Spring 5 Developer Exam: A Study Guide Using Spring Framework 5 (for Spring certification): https://amzn.to/3KxbJSC
  • Pro Spring Boot 2: An Authoritative Guide to Building Microservices, Web and Enterprise Applications, and Best Practices (Spring Boot of the detail): https://amzn.to/3TrIZic