Spring Boot + REST Jersey (Adding Spring Security 4) Part 4


In the previous post we explained how to use HATEOAS step by step using Spring boot Spring Boot + REST Jersey (Adding Spring HATEOAS and MapStruct) Part 3, and we are going to use this project as base to implement Spring Security with basic authentication.

Step 1: Configuration

Spring boot has starter dependencies that are very useful to add new modules to our application, in this example we will add the spring-boot-starter-security dependency as follows:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Step 2: Modifying our Repository

In the post Spring Boot + REST Jersey (Adding Spring HATEOAS and MapStruct) Part 3 we created the repositories UserRepository and RoleRepository. In this example we are going to add a method to UserRepository that will find a user by username.

/**
 *
 */
package com.raidentrance.repositories;

import org.springframework.data.repository.CrudRepository;
import com.raidentrance.entities.User;

/**
 * @author raidentrance
 *
 */
public interface UserRepository extends CrudRepository<User, Integer> {
	User findByUsername(String username);
}

We don’t need to implement the method findByUsername(String username) because Spring data will create the implementation based in a convention.

Step 3: Adding an AuthenticatorService

AuthenticatorService will be the responsible to execute the authentication in the application, the logic will be: Find a user and its role and return it, we don’t need to create a logic to compare users or passwords it will be done by Spring.

/**
 *
 */
package com.raidentrance.service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.raidentrance.entities.User;
import com.raidentrance.repositories.UserRepository;

/**
 * @author raidentrance
 *
 */
@Service
public class AuthenticatorService implements UserDetailsService {
	@Autowired
	private UserRepository userRepository;

	private static final Logger LOG = LoggerFactory.getLogger(AuthenticatorService.class);

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		LOG.info("Trying to authenticate to {}", username);
		User user = userRepository.findByUsername(username);
		if (user == null) {
			throw new UsernameNotFoundException("Username " + username + " not found");
		} else {
			Collection<? extends GrantedAuthority> authorities = getGrantedAuthorities(user);
			return new org.springframework.security.core.userdetails.User(username, user.getPassword(), authorities);
		}
	}

	private Collection<? extends GrantedAuthority> getGrantedAuthorities(User user) {
		List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
		list.add(new GrantedAuthority() {
			private static final long serialVersionUID = 2409931876244987359L;
			@Override
			public String getAuthority() {
				return user.getRole().getName();
			}
		});
		return list;
	}
}

Step 4: Configuring Spring security

Once we created the AuthenticatorService we need to define which endpoints do we want to authenticate.

/**
 *
 */
package com.raidentrance.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import com.raidentrance.service.AuthenticatorService;

/**
 * @author raidentrance
 *
 */
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	@Autowired
	private AuthenticatorService authenticatorService;

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(authenticatorService);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.httpBasic().and().authorizeRequests().anyRequest().authenticated();
	}
}

The method configureGlobal is used to define who is going to search the user, in this example it will use AuthenticatorService previously created.

The method configure is used to define the protected url’s and the authentication mechanism. In this example we are going to use Basic authentication.

Enpoint:  http://localhost:8080/users

Header: Authorization Basic cmFpZGVudHJhbmNlOnN1cGVyU2VjcmV0

How to create the header

The value of the header Authorization is created in the following way:

Authorization Basic   :  It is the name of the header and the authentication mechanism

cmFpZGVudHJhbmNlOnN1cGVyU2VjcmV0 : This is the value and it is the user:password in base64, in this way it is raidentrance:superSecret in base64.

Testing with CURL

curl http://localhost:8080/users -XGET --user raidentrance:superSecret

Testing with Postman

captura-de-pantalla-2016-09-08-a-las-10-14-35-a-m

captura-de-pantalla-2016-09-08-a-las-10-14-58-a-m

You can find the complete code in the following link : https://github.com/raidentrance/spring-boot-example/tree/part4-adding-security

If you want to learn more about Spring and REST we recommend the following books:

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com

Spring Boot + REST Jersey (Adding Spring HATEOAS and MapStruct) Part 3


In the previous examples we explained the configuration of Spring Boot + REST Jersey Part 1 and Spring Boot + REST Jersey (Adding Spring data) Part 2. Now we are going to take the previous examples as base to show how HATEOAS works by using Spring Boot + Jersey.

Step 1: Configuration

Spring boot has starter dependencies that are very useful to add new modules to our application, in this example we will add the spring-hateoas dependency as follows:

<dependency>
    <groupId>org.springframework.hateoas</groupId>
    <artifactId>spring-hateoas</artifactId>
</dependency>

Step 2: Adding entities

In the previous example we learned how to use Spring data to get information from a database and how to use Spring Jersey to expose this information in a web service. In this example we created the entity User, now we are going to create a new entity named Role.

/**
 *
 */
package com.raidentrance.entities;

import java.io.Serializable;

import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * @author raidentrance
 *
 */
@Entity
@Table(name = "ROLE")
public class Role implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID_ROLE")
    private Integer idRole;

    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 45)
    @Column(name = "NAME")
    private String name;

    @Size(max = 100)
    @Column(name = "DESCRIPTION")
    private String description;

    private static final long serialVersionUID = 3428234636660051311L;

    ........
}

And we are going to modify the entity User.

/**
 *
 */
package com.raidentrance.entities;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

/**
 * @author raidentrance
 *
 */
@Entity
@Table(name ="USER")
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "USER_ID")
    private Integer idUser;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "PASSWORD")
    private String password;

    @JoinColumn(name = "ROLE_ID", referencedColumnName = "ID_ROLE")
    @ManyToOne(optional = false)
    private Role role;
.....
}

Step 3: Updating the sql scripts

As we created a new entity we are going to need a new table to store the information, in the previous example we created 2 files schema.sql and init.sql, they contain the scripts to create the tables and to fill the data.

schema.sql

CREATE TABLE ROLE(
ID_ROLE INTEGER PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(45) NOT NULL ,
DESCRIPTION VARCHAR(100) NULL
);

CREATE TABLE USER(
USER_ID INTEGER PRIMARY KEY AUTO_INCREMENT,
USERNAME VARCHAR(100) NOT NULL,
PASSWORD VARCHAR(100) NOT NULL,
ROLE_ID INTEGER NOT NULL,
    FOREIGN KEY (ROLE_ID)
    REFERENCES ROLE (ID_ROLE)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION);

init.sql

INSERT INTO ROLE (NAME,DESCRIPTION)VALUES("ADMIN","Administrator");
INSERT INTO ROLE (NAME,DESCRIPTION)VALUES("USER","Normal user");

INSERT INTO USER (USERNAME,PASSWORD,ROLE_ID)VALUES('raidentrance','superSecret',1);
INSERT INTO USER (USERNAME,PASSWORD,ROLE_ID)VALUES('john','smith',1);
INSERT INTO USER (USERNAME,PASSWORD,ROLE_ID)VALUES('juan','hola123',2);

HATEOAS is the abbreviation of Hypermedia as the Engine of Application state and it will allow to navigate across all the REST resources without any documentation, in this way each of the resources will return a link to its resource.

Step 4: Creating DTO’s with HATEOAS support

Now we have to create Dto’s to support the links generated by HATEOAS by extending of the class ResourceSupport.

/**
 *
 */
package com.raidentrance.dto;

import java.io.Serializable;
import org.springframework.hateoas.ResourceSupport;

/**
 * @author raidentrance
 *
 */
public class RoleDto extends ResourceSupport implements Serializable {

    private Long idRole;

    private String name;

    private String description;

    ........

}
/**
 *
 */
package com.raidentrance.dto;

import java.io.Serializable;
import org.springframework.hateoas.ResourceSupport;
import com.raidentrance.entities.Role;

/**
 * @author raidentrance
 *
 */
public class UserDto extends ResourceSupport implements Serializable {

    private Long idUser;

    private String username;

    private String password;

    private RoleDto role;

..........
}

As you can see the Dto’s extends from the class ResourceSupport, it allows to have support to include links to the resources.

Step 5: Creating an Abstract Assembler

The next step is create an abstract assembler, this will be useful to generate the links to the resources.

/**
 *
 */
package com.raidentrance.assembler;

import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.jaxrs.JaxRsLinkBuilder;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;

import jersey.repackaged.com.google.common.base.Preconditions;

/**
 * @author raidentrance
 *
 */
public abstract class JaxRsResourceAssemblerSupport<T, D extends ResourceSupport>
        extends ResourceAssemblerSupport<T, D> {
    private final Class<?> controllerClass;

    public JaxRsResourceAssemblerSupport(Class<?> controllerClass, Class<D> resourceType) {

        super(controllerClass, resourceType);
        this.controllerClass = controllerClass;
    }

    @Override
    protected D createResourceWithId(Object id, T entity, Object... parameters) {
        Preconditions.checkNotNull(entity);
        Preconditions.checkNotNull(id);
        D instance = instantiateResource(entity);
        instance.add(JaxRsLinkBuilder.linkTo(controllerClass).slash(id).withSelfRel());
        return instance;
    }
}

Step 6: Creating a mapper

Now we are going to create a mapper to transform from JPA entities to Dto’s, in order to do it we will use MapStruct.

/**
 *
 */
package com.raidentrance.mapper;

import org.mapstruct.Mapper;
import com.raidentrance.dto.RoleDto;
import com.raidentrance.dto.UserDto;
import com.raidentrance.entities.Role;
import com.raidentrance.entities.User;

/**
 * @author raidentrance
 *
 */

@Mapper
public interface UserMapper {
    UserDto userEntityToUser(User entity);

    User userToUserEntity(UserDto dto);

    RoleDto roleEntityToRole(Role entity);

    Role roleToRoleEntity(RoleDto role);
}

Step 7: Adding endpoints

The first step will be create the resource for “users” and “roles”, in the following steps we will explain how to implement each method.
UserResource.java

/**
 *
 */
package com.raidentrance.resource;

import java.util.ArrayList;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.raidentrance.entities.User;
import com.raidentrance.repositories.UserRepository;
import jersey.repackaged.com.google.common.collect.Lists;

/**
 * @author raidentrance
 *
 */

@Component
@Path("/users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {

    ......
}

RoleResource.java

/**
 *
 */
package com.raidentrance.resource;

import javax.ws.rs.Consumes;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

/**
 * @author raidentrance
 *
 */
@Component
@Path("/roles")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class RoleResource {
......
}

Step 8: Registering the endpoints

Like in the previous examples we need to register each resource in Jersey.

/**
 *
 */
package com.raidentrance.config;

import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;

import com.raidentrance.resource.RoleResource;
import com.raidentrance.resource.UserResource;

/**
 * @author raidentrance
 *
 */
@Component
public class JerseyConfig extends ResourceConfig {
    public JerseyConfig() {
         register(UserResource.class);
         register(RoleResource.class);
    }
}

Step 9: Creating assemblers

Now we have to create the assemblers, these components will be responsible to transform from Entities to Dto’s and to include the links generated by HATEOS.

RoleAssembler.java

/**
 *
 */
package com.raidentrance.assembler;

import org.mapstruct.factory.Mappers;
import org.springframework.stereotype.Component;
import com.raidentrance.dto.RoleDto;
import com.raidentrance.entities.Role;
import com.raidentrance.mapper.UserMapper;
import com.raidentrance.resource.RoleResource;

/**
 * @author raidentrance
 *
 */
@Component
public class RoleAssembler extends JaxRsResourceAssemblerSupport<Role, RoleDto> {

    private UserMapper mapper = Mappers.getMapper(UserMapper.class);

    public RoleAssembler() {
        super(RoleResource.class, RoleDto.class);
    }

    @Override
    public RoleDto toResource(Role entity) {
        RoleDto role = createResourceWithId(entity.getIdRole(), entity);
        RoleDto result = mapper.roleEntityToRole(entity);
        result.add(role.getLinks());
        return result;
    }
}

UserAssembler.java

/**
 *
 */
package com.raidentrance.assembler;

import org.mapstruct.factory.Mappers;
import org.springframework.beans.factory.annotation.Autowired;
import com.raidentrance.dto.RoleDto;
import com.raidentrance.dto.UserDto;
import com.raidentrance.entities.User;
import com.raidentrance.mapper.UserMapper;
import com.raidentrance.resource.UserResource;

/**
 * @author raidentrance
 *
 */
@Component
public class UserAssembler extends JaxRsResourceAssemblerSupport<User, UserDto> {
    @Autowired
    private RoleAssembler assembler;

    private UserMapper mapper = Mappers.getMapper(UserMapper.class);

    public UserAssembler() {
        super(UserResource.class, UserDto.class);
    }

    @Override
    public UserDto toResource(User entity) {
        UserDto resource = createResourceWithId(entity.getIdUser(), entity);
        UserDto result = mapper.userEntityToUser(entity);
        RoleDto role = assembler.toResource(entity.getRole());
        result.add(resource.getLinks());
        result.setRole(role);
        return result;
    }
}

Step 10: Adding the repository to the entity Role

As we are using Spring data, it is necessary to create the repository of the entity Role in order to be able to get the data from the database.

/**
 *
 */
package com.raidentrance.repositories;

import org.springframework.data.repository.CrudRepository;

import com.raidentrance.entities.Role;

/**
 * @author raidentrance
 *
 */
public interface RoleRepository extends CrudRepository<Role, Integer>{

}

Step 11: Completing the endpoints

Now we are going to add the missing code to the endpoints that we have created.

UserResource.java

/**
 *
 */
package com.raidentrance.resource;

import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.raidentrance.assembler.UserAssembler;
import com.raidentrance.entities.User;
import com.raidentrance.repositories.UserRepository;
import jersey.repackaged.com.google.common.collect.Lists;

/**
 * @author raidentrance
 *
 */

@Component
@Path("/users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {

    @Autowired
    private UserAssembler userAssembler;

    @Autowired
    private UserRepository userRepository;

    @GET
    public Response getUsers() {
        List<User> users = Lists.newArrayList(userRepository.findAll());
        return Response.ok(userAssembler.toResources(users)).build();
    }

    @GET
    @Path("/{idUser}")
    public Response getById(@PathParam("idUser") Integer idUser) {
        User requested = userRepository.findOne(idUser);
        return Response.ok(userAssembler.toResource(requested)).build();
    }

}

RoleResource.java

/**
 *
 */
package com.raidentrance.resource;

import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.raidentrance.assembler.RoleAssembler;
import com.raidentrance.entities.Role;
import com.raidentrance.repositories.RoleRepository;
import jersey.repackaged.com.google.common.collect.Lists;

/**
 * @author raidentrance
 *
 */
@Component
@Path("/roles")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class RoleResource {

    @Autowired
    private RoleRepository roleRepository;
    @Autowired
    private RoleAssembler assembler;

    @GET
    public Response getRoles() {
        List<Role> role = Lists.newArrayList(roleRepository.findAll());
        return Response.ok(assembler.toResources(role)).build();
    }

    @GET
    @Path("/{idRole}")
    public Response getById(@PathParam("idRole") Integer idRole) {
        Role requested = roleRepository.findOne(idRole);
        return Response.ok(assembler.toResource(requested)).build();
    }
}

Step 12: Compile the application

As we are using MapStruct we need to generate the Mappers, in order to do it we have to execute the command mvn generate-sources, once the mappers are created we are ready to compile the application by using the command mvn clean install.

Step 13: Testing all together

The last step is to execute the class SprinBootSampleApplication and access to the following urls:

Open the url http://localhost:8080/users

[
{
idUser: 1,
username: "raidentrance",
password: "superSecret",
role: {
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
},
links: [
{
rel: "self",
href: "http://localhost:8080/users/1"
}
]
},
{
idUser: 2,
username: "john",
password: "smith",
role: {
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
},
links: [
{
rel: "self",
href: "http://localhost:8080/users/2"
}
]
},
{
idUser: 3,
username: "juan",
password: "hola123",
role: {
idRole: 2,
name: "USER",
description: "Normal user",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/2"
}
]
},
links: [
{
rel: "self",
href: "http://localhost:8080/users/3"
}
]
}
]

http://localhost:8080/users/1

{
idUser: 1,
username: "raidentrance",
password: "superSecret",
role: {
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
},
links: [
{
rel: "self",
href: "http://localhost:8080/users/1"
}
]
}

http://localhost:8080/roles

[
{
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
},
{
idRole: 2,
name: "USER",
description: "Normal user",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/2"
}
]
}
]

http://localhost:8080/roles/1

{
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
}

If you want to learn more about web services we recommend the following books:

You can find the complete code in the following link https://github.com/raidentrance/spring-boot-example/tree/part3-adding-hateoas

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com

Spring Boot + REST Jersey (Agregando Spring Hateoas y Dto) Parte 3


En los ejemplos anteriores se explicó la configuración básica de Jersey utilizando Spring boot y la configuración de Spring data en el mismo proyecto. En este tutorial se explicará de forma simple tomando los dos proyectos anteriores como base el uso y funcionamiento de HATEOAS en REST utilizando Spring boot + Jersey.

1. Configuración, Spring boot generó starter dependencies, estas dependencias contienen todos los recursos necesarios para utilizar el módulo de spring deseado de una forma simple y manejable de una forma más simple, las versiones de estas dependencias no son requeridas ya que se heredan del proyecto padre de spring boot.Para configurar Spring HATEOAS se requiere agregar la siguiente dependencia:

<dependency>
    <groupId>org.springframework.hateoas</groupId>
    <artifactId>spring-hateoas</artifactId>
</dependency>

2. Agregando una entidad extra, en el ejemplo anterior se explicó de forma simple como utilizar Spring data para obtener información y Spring Jersey para exponerla en web services REST. En dicho ejemplo se utilizó una entidad llamada User para realizar las pruebas, en este ejemplo se modificará la entidad User y se agregará una nueva llamada Role.

/**
 *
 */
package com.raidentrance.entities;

import java.io.Serializable;

import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * @author raidentrance
 *
 */
@Entity
@Table(name = "ROLE")
public class Role implements Serializable {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "ID_ROLE")
	private Integer idRole;

	@Basic(optional = false)
	@NotNull
	@Size(min = 1, max = 45)
	@Column(name = "NAME")
	private String name;

	@Size(max = 100)
	@Column(name = "DESCRIPTION")
	private String description;

	private static final long serialVersionUID = 3428234636660051311L;

	........
}

Ahora se modifica la entidad usuario.

/**
 *
 */
package com.raidentrance.entities;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

/**
 * @author raidentrance
 *
 */
@Entity
@Table(name ="USER")
public class User implements Serializable {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "USER_ID")
	private Integer idUser;

	@Column(name = "USERNAME")
	private String username;

	@Column(name = "PASSWORD")
	private String password;

	@JoinColumn(name = "ROLE_ID", referencedColumnName = "ID_ROLE")
	@ManyToOne(optional = false)
	private Role role;
.....
}

3. Actualización a scripts sql, la entidad usuario ha sido modificada y se ha agregado una entidad nueva llamada role, para soportar esto se deben realizar las siguientes modificaciones al schema de la la base de datos, del mismo modo que en el ejemplo anterior la definición del schema se realizará en el archivo schema.sql y la inserción de los datos de ejemplo en el archivo init.sql.

schema.sql

CREATE TABLE ROLE(
ID_ROLE INTEGER PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(45) NOT NULL ,
DESCRIPTION VARCHAR(100) NULL
);

CREATE TABLE USER(
USER_ID INTEGER PRIMARY KEY AUTO_INCREMENT,
USERNAME VARCHAR(100) NOT NULL,
PASSWORD VARCHAR(100) NOT NULL,
ROLE_ID INTEGER NOT NULL,
    FOREIGN KEY (ROLE_ID)
    REFERENCES ROLE (ID_ROLE)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION);

init.sql

INSERT INTO ROLE (NAME,DESCRIPTION)VALUES("ADMIN","Administrator");
INSERT INTO ROLE (NAME,DESCRIPTION)VALUES("USER","Normal user");

INSERT INTO USER (USERNAME,PASSWORD,ROLE_ID)VALUES('raidentrance','superSecret',1);
INSERT INTO USER (USERNAME,PASSWORD,ROLE_ID)VALUES('john','smith',1);
INSERT INTO USER (USERNAME,PASSWORD,ROLE_ID)VALUES('juan','hola123',2);

 

HATEOAS es la abreviatura de Hypermedia as the Engine of Application state y permitirá navegar a través de los recursos REST, de tal modo que cada uno de los recursos devueltos debe contener un link al recurso completo. Con esto es posible acceder a los recursos de forma completa sin necesidad de documentación.

4. Creando Dtos con soporte para HATEOAS, lo primero que se debe hacer para agregar soporte para HATEOS es crear Dto’s con soporte a los links para realizar la navegación.

/**
 *
 */
package com.raidentrance.dto;

import java.io.Serializable;
import org.springframework.hateoas.ResourceSupport;

/**
 * @author raidentrance
 *
 */
public class RoleDto extends ResourceSupport implements Serializable {

	private Long idRole;

	private String name;

	private String description;

	........

}

/**
 *
 */
package com.raidentrance.dto;

import java.io.Serializable;
import org.springframework.hateoas.ResourceSupport;
import com.raidentrance.entities.Role;

/**
 * @author raidentrance
 *
 */
public class UserDto extends ResourceSupport implements Serializable {

	private Long idUser;

	private String username;

	private String password;

	private RoleDto role;

..........
}

Se puede observar que los Dto’s creados heredan de la clase ResourceSupport la cuál brindará soporte para agregar links a los recursos que serán devueltos por los servicios REST.

5. Creando un AbstractAssembler, a continuación se muestra un assembler abstracto, el cuál servirá de base para generar los links para los recursos que serán devueltos por los servicios rest.

/**
 *
 */
package com.raidentrance.assembler;
 
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.jaxrs.JaxRsLinkBuilder;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
 
import jersey.repackaged.com.google.common.base.Preconditions;
 
/**
 * @author raidentrance
 *
 */
public abstract class JaxRsResourceAssemblerSupport<T, D extends ResourceSupport>
        extends ResourceAssemblerSupport<T, D> {
    private final Class<?> controllerClass;
 
    public JaxRsResourceAssemblerSupport(Class<?> controllerClass, Class<D> resourceType) {
 
        super(controllerClass, resourceType);
        this.controllerClass = controllerClass;
    }
 
    @Override
    protected D createResourceWithId(Object id, T entity, Object... parameters) {
        Preconditions.checkNotNull(entity);
        Preconditions.checkNotNull(id);
        D instance = instantiateResource(entity);
        instance.add(JaxRsLinkBuilder.linkTo(controllerClass).slash(id).withSelfRel());
        return instance;
    }
}

6. Creando mapper, el siguiente paso es crear un mapper para transformar de Entidades a Dtos de forma simple, para hacer esto se hará uso de MapStruct, para más información sobre la configuración de MapStruct Mapeo de Beans con MapStruct.

/**
 *
 */
package com.raidentrance.mapper;

import org.mapstruct.Mapper;
import com.raidentrance.dto.RoleDto;
import com.raidentrance.dto.UserDto;
import com.raidentrance.entities.Role;
import com.raidentrance.entities.User;

/**
 * @author raidentrance
 *
 */

@Mapper
public interface UserMapper {
	UserDto userEntityToUser(User entity);

	User userToUserEntity(UserDto dto);

	RoleDto roleEntityToRole(Role entity);

	Role roleToRoleEntity(RoleDto role);
}

7. Agregando Endpoints, El primer paso es asegurar que se cuenta con los endpoints tanto para “users” como para “roles”. Más adelante se detallarán los endpoints que contiene cada uno de ellos.

UserResource.java

/**
 *
 */
package com.raidentrance.resource;

import java.util.ArrayList;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.raidentrance.entities.User;
import com.raidentrance.repositories.UserRepository;
import jersey.repackaged.com.google.common.collect.Lists;

/**
 * @author raidentrance
 *
 */

@Component
@Path("/users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {

	......
}

RoleResource.java

/**
 *
 */
package com.raidentrance.resource;

import javax.ws.rs.Consumes;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

/**
 * @author raidentrance
 *
 */
@Component
@Path("/roles")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class RoleResource {
......
}

8. Registrando los servicios, Cómo se explicó en posts anteriores es necesario registrar cada servicio en Jersey para que funcione de forma correcta.

/**
 *
 */
package com.raidentrance.config;

import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;

import com.raidentrance.resource.RoleResource;
import com.raidentrance.resource.UserResource;

/**
 * @author raidentrance
 *
 */
@Component
public class JerseyConfig extends ResourceConfig {
	public JerseyConfig() {
		 register(UserResource.class);
		 register(RoleResource.class);
	}
}

9. Creando los assemblers , el siguiente paso es crear los assemblers, estos serán los responsables de 2 puntos transformar de entities a Dto’s y agregar los links de hateoas.

RoleAssembler.java

/**
 *
 */
package com.raidentrance.assembler;
 
import org.mapstruct.factory.Mappers;
import org.springframework.stereotype.Component;
import com.raidentrance.dto.RoleDto;
import com.raidentrance.entities.Role;
import com.raidentrance.mapper.UserMapper;
import com.raidentrance.resource.RoleResource;
 
/**
 * @author raidentrance
 *
 */
@Component
public class RoleAssembler extends JaxRsResourceAssemblerSupport<Role, RoleDto> {
 
    private UserMapper mapper = Mappers.getMapper(UserMapper.class);
 
    public RoleAssembler() {
        super(RoleResource.class, RoleDto.class);
    }
 
    @Override
    public RoleDto toResource(Role entity) {
        RoleDto role = createResourceWithId(entity.getIdRole(), entity);
        RoleDto result = mapper.roleEntityToRole(entity);
        result.add(role.getLinks());
        return result;
    }
}

UserAssembler.java

/**
 *
 */
package com.raidentrance.assembler;
 
import org.mapstruct.factory.Mappers;
import org.springframework.beans.factory.annotation.Autowired;
import com.raidentrance.dto.RoleDto;
import com.raidentrance.dto.UserDto;
import com.raidentrance.entities.User;
import com.raidentrance.mapper.UserMapper;
import com.raidentrance.resource.UserResource;
 
/**
 * @author raidentrance
 *
 */
@Component
public class UserAssembler extends JaxRsResourceAssemblerSupport<User, UserDto> {
    @Autowired
    private RoleAssembler assembler;
 
    private UserMapper mapper = Mappers.getMapper(UserMapper.class);
 
    public UserAssembler() {
        super(UserResource.class, UserDto.class);
    }
 
    @Override
    public UserDto toResource(User entity) {
        UserDto resource = createResourceWithId(entity.getIdUser(), entity);
        UserDto result = mapper.userEntityToUser(entity);
        RoleDto role = assembler.toResource(entity.getRole());
        result.add(resource.getLinks());
        result.setRole(role);
        return result;
    }
}

10. Agregando Repositorio para la entidad Role

/**
 *
 */
package com.raidentrance.repositories;
 
import org.springframework.data.repository.CrudRepository;
 
import com.raidentrance.entities.Role;
 
/**
 * @author raidentrance
 *
 */
public interface RoleRepository extends CrudRepository<Role, Integer>{
 
}

11. Detallando los endpoints necesarios , A continuación se muestran los endpoints que se utilizarán para exponer los recursos y para generar la navegación entre los servicios.

UserResource.java


/**
 *
 */
package com.raidentrance.resource;

import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.raidentrance.assembler.UserAssembler;
import com.raidentrance.entities.User;
import com.raidentrance.repositories.UserRepository;
import jersey.repackaged.com.google.common.collect.Lists;

/**
 * @author raidentrance
 *
 */

@Component
@Path("/users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {

	@Autowired
	private UserAssembler userAssembler;

	@Autowired
	private UserRepository userRepository;

	@GET
	public Response getUsers() {
		List<User> users = Lists.newArrayList(userRepository.findAll());
		return Response.ok(userAssembler.toResources(users)).build();
	}

	@GET
	@Path("/{idUser}")
	public Response getById(@PathParam("idUser") Integer idUser) {
		User requested = userRepository.findOne(idUser);
		return Response.ok(userAssembler.toResource(requested)).build();
	}

}

RoleResource.java

/**
 *
 */
package com.raidentrance.resource;

import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.raidentrance.assembler.RoleAssembler;
import com.raidentrance.entities.Role;
import com.raidentrance.repositories.RoleRepository;
import jersey.repackaged.com.google.common.collect.Lists;

/**
 * @author raidentrance
 *
 */
@Component
@Path("/roles")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class RoleResource {

	@Autowired
	private RoleRepository roleRepository;
	@Autowired
	private RoleAssembler assembler;

	@GET
	public Response getRoles() {
		List<Role> role = Lists.newArrayList(roleRepository.findAll());
		return Response.ok(assembler.toResources(role)).build();
	}

	@GET
	@Path("/{idRole}")
	public Response getById(@PathParam("idRole") Integer idRole) {
		Role requested = roleRepository.findOne(idRole);
		return Response.ok(assembler.toResource(requested)).build();
	}
}

12. Probando todo junto ,para ejecutar la aplicación el primer punto es generar el mapper de MapStruct utilizando el goal mvn generate-sources una vez generados los mappers es necesario compilar el proyecto y el proyecto queda listo para probarlo. Para esto se ejecutará la clase principal de la aplicación SprinBootSampleApplication, con esto se iniciará un contenedor.

Probando los recursos:

http://localhost:8080/users .

[
{
idUser: 1,
username: "raidentrance",
password: "superSecret",
role: {
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
},
links: [
{
rel: "self",
href: "http://localhost:8080/users/1"
}
]
},
{
idUser: 2,
username: "john",
password: "smith",
role: {
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
},
links: [
{
rel: "self",
href: "http://localhost:8080/users/2"
}
]
},
{
idUser: 3,
username: "juan",
password: "hola123",
role: {
idRole: 2,
name: "USER",
description: "Normal user",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/2"
}
]
},
links: [
{
rel: "self",
href: "http://localhost:8080/users/3"
}
]
}
]

http://localhost:8080/users/1 .

{
idUser: 1,
username: "raidentrance",
password: "superSecret",
role: {
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
},
links: [
{
rel: "self",
href: "http://localhost:8080/users/1"
}
]
}

http://localhost:8080/roles .

[
{
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
},
{
idRole: 2,
name: "USER",
description: "Normal user",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/2"
}
]
}
]

http://localhost:8080/roles/1 .

{
idRole: 1,
name: "ADMIN",
description: "Administrator",
links: [
{
rel: "self",
href: "http://localhost:8080/roles/1"
}
]
}

 

Puedes encontrar el código completo del ejemplo en el siguiente enlace:
https://github.com/raidentrance/spring-boot-example/tree/part3-adding-hateoas .

 

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com