Skip to content

2. El servidor Spring 4

En la arquitectura anterior, abordamos ahora la construcción del servicio web / JSON creado con el marco Spring 4. Lo escribiremos en varias etapas:

  • primero las capas [métier] y [DAO] (Data Access Object). Aquí utilizaremos Spring Data;
  • luego, el servicio web JSON sin autenticación. Aquí utilizaremos Spring MVC;
  • y luego añadiremos la parte de autenticación con Spring Security.

Comenzamos explicando la estructura de la base de datos que sustenta la aplicación.

2.1. La base de datos

La base de datos denominada en lo sucesivo [dbrdvmedecins] es una base de datos MySQL5 con las siguientes tablas:

  

Las citas se gestionan mediante las siguientes tablas:

  • [medecins]: contiene la lista de médicos de la consulta;
  • [clients]: contiene la lista de pacientes de la consulta;
  • [creneaux]: contiene los horarios de cada uno de los médicos;
  • [rv]: contiene la lista de citas de los médicos.

Las tablas [roles], [users] y [users_roles] son tablas relacionadas con la autenticación. Por el momento, no nos ocuparemos de ellas.

Las relaciones entre las tablas que gestionan las citas son las siguientes:

 
  • una franja horaria pertenece a un médico – un médico tiene 0 o más franjas horarias;
  • una cita reúne a un cliente y a un médico a través de un intervalo horario de este último;
  • un cliente tiene 0 o más citas;
  • a un intervalo de tiempo se le asocian 0 o más citas (en días diferentes).

2.1.1. La tabla [MEDECINS]

Contiene información sobre los médicos gestionados por la aplicación [RdvMedecins].

  • ID: número que identifica al médico - clave primaria de la tabla
  • VERSION: número que identifica el version de la línea en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la línea.
  • NOM: el apellido del médico
  • PRENOM: su nombre
  • TITRE: su tratamiento (Srta., Sra., Sr.)

2.1.2. La tabla [CLIENTS]

Los clients de los distintos médicos se registran en la tabla [CLIENTS]:

  • ID: n.º de identificación del cliente - clave primaria de la tabla
  • VERSION: número que identifica el version de la línea en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la línea.
  • NOM: el nombre del cliente
  • PRENOM: su nombre
  • TITRE: su tratamiento (Srta., Sra., Sr.)

2.1.3. La tabla [CRENEAUX]

Enumera los intervalos horarios en los que son posibles los RV:

  • ID: número que identifica la franja horaria - clave primaria de la tabla (línea 8)
  • VERSION: número que identifica el version de la línea en la tabla. Este número se incrementa en 1 cada vez que se realiza una modificación en la línea.
  • ID_MEDECIN: número que identifica al médico al que pertenece este intervalo horario – clave externa en la columna MEDECINS(ID).
  • HDEBUT: hora de inicio de la franja
  • MDEBUT: minutos de inicio del intervalo
  • HFIN: hora de fin de la franja
  • MFIN: minutos de fin de franja

La segunda línea de la tabla [CRENEAUX] (véase [1] más arriba) indica, por ejemplo, que la franja n.º 2 comienza a las 8:20 y termina a las 8:40 y pertenece al médico n.º 1 (Sra. Marie PELISSIER).

2.1.4. La tabla [RV]

Enumera los RV asignados a cada médico:

  • ID: número que identifica de forma única al RV – clave primaria
  • JOUR: día del RV
  • ID_CRENEAU: franja horaria del RV – clave externa en el campo [ID] de la tabla [CRENEAUX] – determina tanto la franja horaria como el médico correspondiente.
  • ID_CLIENT: n.º del cliente para el que se realiza la reserva – clave externa en el campo [ID] de la tabla [CLIENTS]

Esta tabla tiene una restricción de unicidad en la e e sobre los valores de las columnas unidas (JOUR, ID_CRENEAU):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

Si una fila de la tabla [RV] tiene el valor (JOUR1, ID_CRENEAU1) para las columnas (JOUR, ID_CRENEAU), este valor no puede aparecer en ningún otro sitio. De lo contrario, significaría que se han tomado dos RV al mismo tiempo para el mismo médico. Desde el punto de vista de la programación Java, el controlador JDBC de la base de datos lanza un SQLException cuando se produce este caso.

La línea de id igual a 3 (véase [1] más arriba) significa que se ha reservado un RV para la franja n.º 20 y el cliente n.º 4 el 23/08/2006. La tabla [CRENEAUX] nos indica que la franja n.º 20 corresponde al horario de 16:20 a 16:40 y pertenece al médico n.º 1 (Sra. Marie PELISSIER). La tabla [CLIENTS] nos indica que el cliente n.º 4 es la Srta. Brigitte BISTROU.

2.2. Introducción a Spring Data

Vamos a implementar la capa [DAO] del proyecto con Spring Data, una rama del ecosistema Spring.

En la página web de Spring hay numerosos tutoriales para iniciarse en Spring [http://spring.io/guides]. Vamos a utilizar uno de ellos para introducir Spring Data. Para ello, utilizamos Spring Tool Suite (STS).

  • en [1], importamos uno de los tutoriales de [spring.io/guides];
  • en [2], seleccionamos el tutorial [Accessing Data Jpa] que muestra cómo acceder a una base de datos con Spring Data;
  • en [3], se elige un proyecto configurado por Maven;
  • en [4], el tutorial puede presentarse de dos formas: [initial], que es un version vacío que se rellena siguiendo el tutorial, o [complete], que es el version final del tutorial. Elegimos este último;
  • en [5], podemos elegir visualizar el tutorial en un navegador;
  • en [6], el proyecto final.

2.2.1. La configuración de Maven del proyecto

Las dependencias de Maven del proyecto se configuran en el archivo [pom.xml]:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.0.2.RELEASE</version>
    </parent>

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

    <properties>
        <!-- utiliza UTF-8 para todo -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • líneas 5-9: definen un proyecto Maven padre. Es este el que define la mayor parte de las dependencias del proyecto. Pueden ser suficientes, en cuyo caso no se añaden más, o no, en cuyo caso se añaden las dependencias que faltan;
  • líneas 12-15: definen una dependencia de [spring-boot-starter-data-jpa]. Este artefacto contiene las clases de Spring Data;
  • líneas 16-19: definen una dependencia de SGBD y H2, que permiten crear y gestionar bases de datos en memoria.

Veamos las clases que aportan estas dependencias:

Son muchas:

  • algunas pertenecen al ecosistema Spring (las que empiezan por spring);
  • otras pertenecen al ecosistema Hibernate (hibernate, jboss), de las que aquí se utiliza la implementación JPA;
  • otras son bibliotecas de pruebas (junit, hamcrest);
  • otras son bibliotecas de registros (log4j, logback, slf4j);

Las vamos a conservar todas. Para una aplicación en producción, habría que conservar solo las que sean necesarias.

En la línea 26 del archivo [pom.xml] encontramos la línea:


<start-class>hello.Application</start-class>

Esta línea está relacionada con las siguientes:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Líneas 6-9: el complemento [spring-boot-maven-plugin] permite generar el ejecutable jar de la aplicación. La línea 26 del archivo [pom.xml] designa entonces la clase ejecutable de este jar.

2.2.2. La capa [JPA]

El acceso a la base de datos se realiza a través de una capa [JPA], Java Persistence API:

  

La aplicación es básica y gestiona clients [Customer]. La clase [Customer] forma parte de la capa [JPA] y es la siguiente:


package hello;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    protected Customer() {
    }

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }

}

Un cliente tiene un identificador [id], un nombre [firstName] y un apellido [lastName]. Cada instancia [Customer] representa una fila de una tabla de la base de datos.

  • línea 8: anotación JPA que hace que la persistencia de las instancias [Customer] (Create, Read, Update, Delete) vaya a ser gestionada por una implementación JPA. Según las dependencias de Maven, se observa que se utiliza la implementación JPA / Hibernate;
  • líneas 11-12: anotaciones JPA que asocian el campo [id] a la clave primaria de la tabla [Customer]. La línea 12 indica que la implementación JPA utilizará el método de generación de clave primaria propio del SGBD utilizado, en este caso H2;

No hay otras anotaciones para JPA. Por lo tanto, se utilizarán los valores por defecto:

  • la tabla de [Customer] llevará el nombre de la clase, es decir, [Customer];
  • las columnas de esta tabla llevarán el nombre de los campos de la clase: [id, firstName, lastName], teniendo en cuenta que no se distingue entre mayúsculas y minúsculas en el nombre de una columna de tabla;

Cabe señalar que en ningún momento se menciona la implementación JPA utilizada.

2.2.3. La capa [DAO]

  

La clase [CustomerRepository] implementa la capa [DAO]. Su código es el siguiente:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Por lo tanto, se trata de una interfaz y no de una clase (línea 7). Extiende la interfaz [CrudRepository], una interfaz de Spring Data (línea 5). Esta interfaz está parametrizada por dos tipos: el primero es el tipo de los elementos gestionados, en este caso el tipo [Customer]; el segundo es el tipo de la clave primaria de los elementos gestionados, en este caso un tipo [Long]. La interfaz [CrudRepository] es la siguiente:


package org.springframework.data.repository;

import java.io.Serializable;

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);

    <S extends T> Iterable<S> save(Iterable<S> entities);

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

    Iterable<T> findAll(Iterable<ID> ids);

    long count();

    void delete(ID id);

    void delete(T entity);

    void delete(Iterable<? extends T> entities);

    void deleteAll();
}

Esta interfaz define las operaciones CRUD (Crear – Leer – Actualizar – Eliminar) que se pueden realizar sobre un tipo JPA T:

  • línea 8: el método save permite persistir una entidad T en la base de datos. Hace que la entidad se persista con la clave primaria que le ha asignado SGBD. También permite actualizar una entidad T identificada por su clave primaria id. La elección de una u otra acción se realiza en función del valor de la clave primaria id: si este es nulo, se lleva a cabo la operación de persistencia; en caso contrario, se realiza la operación de actualización;
  • línea 10: lo mismo, pero para una lista de entidades;
  • línea 12: el método findOne permite recuperar una entidad T identificada por su clave primaria id;
  • línea 22: el método delete permite eliminar una entidad T identificada por su clave primaria id;
  • líneas 24-28: variantes del método [delete];
  • línea 16: el método [findAll] permite recuperar todas las entidades persistentes T;
  • línea 18: lo mismo, pero limitado a las entidades cuya lista de identificadores se ha pasado;

Volvamos a la interfaz [CustomerRepository]:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • la línea 9 permite recuperar un [Customer] por su nombre [lastName];

Y eso es todo para la capa [DAO]. No hay ninguna clase de implementación de la interfaz anterior. Esta se genera en tiempo de ejecución mediante [Spring Data]. Los métodos de la interfaz [CrudRepository] se implementan automáticamente. En cuanto a los métodos añadidos en la interfaz [CustomerRepository], depende. Volvamos a la definición de [Customer]:


    private long id;
    private String firstName;
private String lastName;

El método de la línea 9 se implementa automáticamente mediante [Spring Data] porque hace referencia al campo [lastName] (línea 3) de [Customer]. Cuando encuentra un método [findBySomething] en la interfaz que se va a implementar, Spring Data lo implementa mediante la siguiente consulta JPQL (Java Persistence Query Language):

select t from T t where t.something=:value

Por lo tanto, el tipo T debe tener un campo llamado [something]. Así, el método

List<Customer> findByLastName(String lastName);

se implementará mediante un código similar al siguiente:

return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()

donde [em] designa el contexto de persistencia JPA. Esto solo es posible si la clase [Customer] tiene un campo llamado [lastName], lo cual es el caso.

En conclusión, en los casos sencillos, Spring Data nos permite implementar la capa [DAO] con una simple interfaz.

2.2.4. La capa [console]

  

La clase [Application] es la siguiente:


package hello;

import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(Application.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // guarda un par de customers
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // recoger todos los customers
        Iterable<Customer> customers = repository.findAll();
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : customers) {
            System.out.println(customer);
        }
        System.out.println();

        // recoger un cliente individual por ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // obtener customers por last name
        List<Customer> bauers = repository.findByLastName("Bauer");
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : bauers) {
            System.out.println(bauer);
        }

        context.close();
    }

}
  • la línea 10: indica que la clase sirve para configurar Spring. Las versiones recientes de Spring pueden configurarse en Java en lugar de en XML. Ambos métodos pueden utilizarse simultáneamente. En el código de una clase que tiene la anotación [Configuration] normalmente se encuentran beans de Spring, es decir, definiciones de clases que deben instanciarse. Aquí no se define ningún bean. Cabe recordar que, cuando se trabaja con un SGBD, deben definirse varios beans de Spring:
    • un [EntityManagerFactory] que define la implementación JPA que se va a utilizar,
    • un [DataSource] que define la fuente de datos que se va a utilizar,
    • un [TransactionManager] que define el gestor de transacciones que se va a utilizar;

Aquí no se define ninguno de estos beans.

  • Línea 11: la anotación [EnableAutoConfiguration] es una anotación procedente del proyecto [Spring Boot] (líneas 5-6). Esta anotación solicita a Spring Boot, a través de la clase [SpringApplication] (línea 16), que configure la aplicación en función de las bibliotecas encontradas en su Classpath. Dado que las bibliotecas de Hibernate se encuentran en el Classpath, el bean [entityManagerFactory] se implementará con Hibernate. Dado que la biblioteca SGBD H2 se encuentra en el Classpath, el bean [dataSource] se implementará con H2. En el bean [dataSource], también hay que definir el usuario y su contraseña. Aquí Spring Boot utilizará el administrador por defecto de H2, que no tiene contraseña. Como la biblioteca [spring-tx] está en el Classpath, se utilizará el gestor de transacciones de Spring.

Además, se escaneará la carpeta en la que se encuentra la clase [Application] en busca de beans reconocidos implícitamente por Spring o definidos explícitamente mediante anotaciones de Spring. Así, se inspeccionarán las clases [Customer] y [CustomerRepository]. Dado que la primera tiene la anotación [@Entity], se catalogará como entidad que debe gestionar Hibernate. Dado que la segunda extiende la interfaz [CrudRepository], se registrará como bean de Spring.

Examinemos las líneas 16-17 del código:


ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
  • línea 1: se ejecuta el método estático [run] de la clase [SpringApplication] del proyecto Spring Boot. Su parámetro es la clase que tiene una anotación [Configuration] o [EnableAutoConfiguration]. A continuación, se llevará a cabo todo lo explicado anteriormente. El resultado es un contexto de aplicación Spring, es decir, un conjunto de beans gestionados por Spring;
  • línea 17: se solicita a este contexto Spring un bean que implemente la interfaz [CustomerRepository]. Aquí recuperamos la clase generada por Spring Data para implementar esta interfaz.

Las operaciones siguientes solo utilizan los métodos del bean que implementa la interfaz [CustomerRepository]. Cabe destacar, en la línea 50, que el contexto se cierra. Los resultados de la consola son los siguientes:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.0.2.RELEASE)

2014-06-05 16:23:13.877  INFO 11664 --- [           main] hello.Application                        : Iniciando la aplicación en Gportpers3 con PID 11664 (D:\Temp\wksSTS\gs-accessing-data-jpa-complete\target\classes iniciada por ST en D:\Temp\wksSTS\gs-accessing-data-jpa-complete)
2014-06-05 16:23:13.936  INFO 11664 --- [           main] s.c.a.AnnotationConfigApplicationContext : Actualizando org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: fecha de inicio [Thu Jun 05 16:23:13 CEST 2014]; raíz de la jerarquía de contexto
2014-06-05 16:23:15.424  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : Creación del contenedor JPA EntityManagerFactory para la unidad de persistencia «default»
2014-06-05 16:23:15.518  INFO 11664 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Procesando PersistenceUnitInfo [
    name: default
    ...]
2014-06-05 16:23:15.690  INFO 11664 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.1.Final}
2014-06-05 16:23:15.692  INFO 11664 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not encontrado
2014-06-05 16:23:15.694  INFO 11664 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Proveedor de código byte name : javassist
2014-06-05 16:23:15.988  INFO 11664 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
2014-06-05 16:23:16.078  INFO 11664 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Usando dialecto: org.hibernate.dialect.H2Dialect
2014-06-05 16:23:16.300  INFO 11664 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Usando ASTQueryTranslatorFactory
2014-06-05 16:23:16.613  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Ejecutando la exportación del esquema hbm2ddl
Hibernate: drop table customer if exists
Hibernate: create table customer (id bigint generated by default as identity, first_name varchar(255), last_name varchar(255), primary key (id))
2014-06-05 16:23:16.619  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Esquema de exportación completo
2014-06-05 16:23:17.074  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registrando beans para la exposición de JMX al inicio
2014-06-05 16:23:17.094  INFO 11664 --- [           main] hello.Application                        : Aplicación iniciada en 3,906 segundos (JVM ejecutándose durante 5,013)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: insert into customer (id, first_name, last_name) values (null, ?, ?)
Hibernate: select customer0_.id as id1_0_, customer0_.first_name as first_na2_0_, customer0_.last_name as last_nam3_0_ from customer customer0_
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Hibernate: select customer0_.id as id1_0_0_, customer0_.first_name as first_na2_0_0_, customer0_.last_name as last_nam3_0_0_ from customer customer0_ where customer0_.id=?
Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Hibernate: select customer0_.id as id1_0_, customer0_.first_name as first_na2_0_, customer0_.last_name as last_nam3_0_ from customer customer0_ where customer0_.last_name=?
Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2014-06-05 16:23:17.330  INFO 11664 --- [           main] s.c.a.AnnotationConfigApplicationContext : Cierre org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: fecha de inicio [Thu Jun 05 16:23:13 CEST 2014]; raíz de la jerarquía de contexto
2014-06-05 16:23:17.332  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Desregistrando beans expuestos a JMX al apagar
2014-06-05 16:23:17.333  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : Cerrando JPA EntityManagerFactory para la unidad de persistencia «default»
2014-06-05 16:23:17.334  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Ejecutando la exportación del esquema hbm2ddl
Hibernate: drop table customer if exists
2014-06-05 16:23:17.336  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Esquema de exportación completo
  • líneas 1-8: el logotipo del proyecto Spring Boot;
  • línea 9: se ejecuta la clase [hello.Application];
  • línea 10: [AnnotationConfigApplicationContext] es una clase que implementa la interfaz [ApplicationContext] de Spring. Se trata de un contenedor de beans;
  • línea 11: el bean [entityManagerFactory] se implementa con la clase [LocalContainerEntityManagerFactory], una clase de Spring;
  • línea 12: aparece [hibernate]. Se ha elegido esta implementación JPA;
  • línea 19: un dialecto de Hibernate es la variante SQL que se utilizará con SGBD. Aquí, el dialecto [H2Dialect] indica que Hibernate va a trabajar con SGBD y H2;
  • líneas 22-24: se crea la tabla [CUSTOMER]. Esto significa que Hibernate se ha configurado para generar las tablas a partir de las definiciones JPA; aquí, la definición JPA de la clase [Customer];
  • líneas 27-32: registros de Hibernate que muestran las inserciones de filas en la tabla [CUSTOMER]. Esto significa que Hibernate se ha configurado para generar registros;
  • líneas 35-39: los cinco registros clients insertados;
  • líneas 42-44: resultado del método [findOne] de la interfaz;
  • líneas 47-50: resultados del método [findByLastName];
  • líneas 51 y siguientes: registros del cierre del contexto Spring.

2.2.5. Configuración manual del proyecto Spring Data

Duplicamos el proyecto anterior en el proyecto [gs-accessing-data-jpa-2]:

  

En este nuevo proyecto, no vamos a basarnos en la configuración automática realizada por Spring Boot. La haremos manualmente. Esto puede resultar útil si las configuraciones predeterminadas no nos convencen.

En primer lugar, vamos a especificar las dependencias necesarias en el archivo [pom.xml]:


<dependencies>
        <!-- Spring Core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <!-- Transacciones de Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.5.2.RELEASE</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>1.0.2.RELEASE</version>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.4.Final</version>
        </dependency>
        <!-- H2 Base de datos -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.178</version>
        </dependency>
        <!-- Commons DBCP -->
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
            <version>1.6</version>
        </dependency>
    </dependencies>
  • líneas 3-17: las bibliotecas básicas de Spring;
  • líneas 19-28: las bibliotecas de Spring para gestionar las transacciones con una base de datos;
  • líneas 30-34: Spring Data utilizado para acceder a la base de datos;
  • líneas 36-40: Spring Boot para iniciar la aplicación;
  • líneas 48-52: SGBD H2;
  • líneas 54-63: las bases de datos suelen utilizarse con grupos de conexiones abiertas que evitan la apertura y el cierre repetidos de conexiones. En este caso, la implementación utilizada es la de [commons-dbcp];

También en [pom.xml], se modifica el nombre de la clase ejecutable:


    <properties>
...
        <start-class>demo.console.Main</start-class>
</properties>

En el nuevo proyecto, la entidad [Customer] y la interfaz [CustomerRepository] no cambian. Vamos a modificar la clase [Application], que se dividirá en dos clases:

  • [Config], que será la clase de configuración:
  • [Main], que será la clase ejecutable;
  

La clase ejecutable [Main] es la misma que la anterior, pero sin las anotaciones de configuración:


package demo.console;

import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;

import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;

public class Main {

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
...

        context.close();
    }

}
  • línea 12: la clase [Main] ya no tiene anotaciones de configuración;
  • línea 16: la aplicación se inicia con Spring Boot. El parámetro [Config.class] es la nueva clase de configuración del proyecto;

La clase [Config] que configura el proyecto es la siguiente:


package demo.config;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
    // la fuente de datos H2
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }

    // el proveedor JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("demo.entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Gestor de transacciones
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • línea 22: la anotación [@Configuration] convierte la clase [Config] en una clase de configuración de Spring;
  • línea 21: la anotación [@EnableJpaRepositories] permite designar las carpetas donde se encuentran las interfaces Spring Data y [CrudRepository]. Estas interfaces se convertirán en componentes Spring y estarán disponibles en su contexto;
  • línea 20: la anotación [@EnableTransactionManagement] indica que los métodos de las interfaces [CrudRepository] deben ejecutarse dentro de una transacción;
  • línea 19: la anotación [@EntityScan] permite especificar las carpetas en las que deben buscarse las entidades JPA. Aquí se ha puesto entre comentarios, porque esta información se ha proporcionado explícitamente en la línea 50. Esta anotación debería estar presente si se utiliza el modo [@EnableAutoConfiguration] y las entidades JPA no se encuentran en la misma carpeta que la clase de configuración;
  • línea 18: la anotación [@ComponentScan] permite enumerar las carpetas en las que se deben buscar los componentes Spring. Los componentes Spring son clases etiquetadas con anotaciones Spring como @Service, @Component, @Controller, etc. Aquí no hay más que las definidas dentro de la clase [Config], por lo que la anotación se ha comentado;
  • líneas 25-33: definen la fuente de datos, la base de datos H2. Es la anotación @Bean de la línea 25 la que convierte al objeto creado por este método en un componente gestionado por Spring. El nombre del método puede ser cualquiera. Sin embargo, debe llamarse [dataSource] si el EntityManagerFactory de la línea 47 no existe y se define mediante autoconfiguración;
  • línea 29: la base de datos se llamará [demo] y se generará en la carpeta del proyecto;
  • líneas 36-43: definen la implementación JPA utilizada, en este caso una implementación de Hibernate. El nombre del método puede ser cualquiera;
  • línea 39: sin registros SQL;
  • línea 30: la base de datos se creará si no existe;
  • líneas 46-54: definen el EntityManagerFactory que gestionará la persistencia JPA. El método debe llamarse obligatoriamente [entityManagerFactory];
  • línea 47: el método recibe dos parámetros del tipo de los dos beans definidos anteriormente. Estos serán construidos e inyectados por Spring como parámetros del método;
  • línea 49: establece la implementación JPA utilizada;
  • línea 50: establece las carpetas donde se encuentran las entidades JPA;
  • línea 51: establece la fuente de datos que se va a gestionar;
  • líneas 57-62: el gestor de transacciones. El método debe llamarse obligatoriamente [transactionManager]. Recibe como parámetro el bean de las líneas 46-54;
  • línea 60: el gestor de transacciones se asocia a EntityManagerFactory;

Los métodos anteriores se pueden definir en cualquier orden.

La ejecución del proyecto da los mismos resultados. Aparece un nuevo archivo en la carpeta del proyecto, el de la base de datos H2:

  

Por último, se puede prescindir de Spring Boot. Se crea una segunda clase ejecutable [Main2]:

  

La clase [Main2] tiene el siguiente código:


package demo.console;

import java.util.List;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;

public class Main2 {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
....

        context.close();
    }

}
  • línea 15: la clase de configuración [Config] ahora es utilizada por la clase Spring [AnnotationConfigApplicationContext]. En la línea 5 se puede ver que ya no hay dependencia de Spring Boot.

La ejecución da los mismos resultados que antes.

2.2.6. Creación de un archivo ejecutable

Para crear un archivo ejecutable del proyecto, se puede proceder de la siguiente manera:

  • en [1]: se crea una configuración de ejecución;
  • en [2]: del tipo [Java Application]
  • en [3]: designa el proyecto que se va a ejecutar (utilice el botón Browse);
  • en [4]: indica la clase que se va a ejecutar;
  • en [5]: el nombre de la configuración de ejecución; puede ser cualquiera;
  • en [6]: se exporta el proyecto;
  • en [7]: en forma de archivo ejecutable JAR;
  • en [8]: indica la ruta y el nombre del archivo ejecutable que se va a crear;
  • en [9]: el nombre de la configuración de ejecución creada en [5];

Una vez hecho esto, abrimos una consola en la carpeta que contiene el archivo ejecutable:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-2.jar

El archivo se ejecuta de la siguiente manera:


.....\dist>java -jar gs-accessing-data-jpa-2.jar

Los resultados obtenidos en la consola son los siguientes:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder para más detalles.
juin 12, 2014 9:48:38 AM org.hibernate.ejb.HibernatePersistence logDeprecation
WARN: HHH015016: Encountered a deprecated javax.persistence.spi.PersistenceProvider [org.hibernate.ejb.HibernatePersistence]; use [org.hibernate.jpa.HibernatePersistenceProvider] instead.
juin 12, 2014 9:48:38 AM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
        name: default
        ...]
juin 12, 2014 9:48:38 AM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.4.Final}
juin 12, 2014 9:48:38 AM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
juin 12, 2014 9:48:38 AM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
juin 12, 2014 9:48:39 AM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
juin 12, 2014 9:48:39 AM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
juin 12, 2014 9:48:39 AM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

2.2.7. Crear un nuevo proyecto Spring Data

Para crear un esqueleto de proyecto Spring Data, se puede proceder de la siguiente manera:

  • en [1], se crea un nuevo proyecto;
  • en [2]: de tipo [Spring Starter Project];
  • el proyecto generado será un proyecto Maven. En [3], se indica el nombre del grupo del proyecto;
  • en [4]: se indica el nombre del artefacto (un jar en este caso) que se creará al compilar el proyecto;
  • en [5]: se indica el paquete de la clase ejecutable que se va a crear en el proyecto;
  • en [6]: el nombre Eclipse del proyecto; puede ser cualquiera (no tiene por qué ser idéntico a [4]);
  • en [7]: se indica que se va a crear un proyecto con una capa [JPA]. Las dependencias necesarias para dicho proyecto se incluirán entonces en el archivo [pom.xml];
  • en [8]: el proyecto creado;

El archivo [pom.xml] integra las dependencias necesarias para un proyecto JPA:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.RELEASE</version>
        <relativePath/> <!-- buscar padre en el repositorio -->
    </parent>

    <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-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
  • líneas 9-12: las dependencias necesarias para JPA – incluirán [Spring Data];
  • líneas 13-17: las dependencias necesarias para las pruebas JUnit integradas con Spring;

La clase ejecutable [Application] no hace nada, pero está preconfigurada:


package istia.st;

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

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {

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

La clase de pruebas [ApplicationTests] no hace nada, pero está preconfigurada:


package istia.st;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {

    @Test
    public void contextLoads() {
    }

}
  • línea 9: la anotación [@SpringApplicationConfiguration] permite utilizar el archivo de configuración [Application]. De este modo, la clase de prueba se beneficiará de todos los beans que se definan en dicho archivo;
  • línea 8: la anotación [@RunWith] permite la integración de Spring con JUnit: la clase podrá ejecutarse como una prueba JUnit. [@RunWith] es una anotación JUnit (línea 4), mientras que la clase [SpringJUnit4ClassRunner] es una clase Spring (línea 6);

Ahora que tenemos un esqueleto de aplicación JPA, podemos completarlo para escribir el proyecto de la capa de persistencia del servidor de nuestra aplicación de gestión de citas.

2.3. El proyecto Eclipse del servidor

  

Los elementos principales del proyecto son los siguientes:

  • [pom.xml]: archivo de configuración Maven del proyecto;
  • [rdvmedecins.entities]: las entidades JPA;
  • [rdvmedecins.repositories]: las interfaces Spring Data de acceso a las entidades JPA;
  • [rdvmedecins.metier]: la capa [métier];
  • [rdvmedecins.domain]: las entidades manipuladas por la capa [métier];
  • [rdvmdecins.config]: las clases de configuración de la capa de persistencia;
  • [rdvmedecins.boot]: una aplicación de consola básica;

2.4. La configuración de Maven

El archivo [pom.xml] del proyecto es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring4.rdvmedecins</groupId>
    <artifactId>rdvmedecins-metier-dao</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.0.0.RELEASE</version>
    </parent>
    <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-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
    </dependencies>
    <properties>
        <!-- utiliza UTF-8 para todo -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>istia.st.spring.data.main.Application</start-class>
    </properties>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>http://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>http://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>
  • líneas 8-12: el proyecto se basa en el proyecto principal [spring-boot-starter-parent]. Para las dependencias ya presentes en el proyecto padre, no se especifica version. Se utilizará el version definido en el proyecto padre. Las demás dependencias se declaran de forma habitual;
  • líneas 14-17: para Spring Data;
  • líneas 18-22: para las pruebas JUnit;
  • líneas 23-26: controlador JDBC de SGBD MySQL5;
  • líneas 27-34: grupo de conexiones Commons DBCP;
  • líneas 35-38: biblioteca Jackson de gestión de JSON;
  • líneas 39-43: biblioteca de Google para la gestión de colecciones;

La versión 1.1.0.RC1 de [spring-boot-starter-parent] utiliza las siguientes versiones de las bibliotecas:

<activemq.version>5.9.1</activemq.version>
    <aspectj.version>1.8.0</aspectj.version>
    <codahale-metrics.version>3.0.2</codahale-metrics.version>
    <commons-beanutils.version>1.9.1</commons-beanutils.version>
    <commons-collections.version>3.2.1</commons-collections.version>
    <commons-dbcp.version>1.4</commons-dbcp.version>
    <commons-digester.version>2.1</commons-digester.version>
    <commons-pool.version>1.6</commons-pool.version>
    <commons-pool2.version>2.2</commons-pool2.version>
    <crashub.version>1.3.0-beta20</crashub.version>
    <flyway.version>3.0</flyway.version>
    <freemarker.version>2.3.20</freemarker.version>
    <gemfire.version>7.0.2</gemfire.version>
    <gradle.version>1.6</gradle.version>
    <groovy.version>2.3.2</groovy.version>
    <h2.version>1.3.175</h2.version>
    <hamcrest.version>1.3</hamcrest.version>
    <hibernate-entitymanager.version>4.3.1.Final</hibernate-entitymanager.version>
    <hibernate-jpa-api.version>1.0.1.Final</hibernate-jpa-api.version>
    <hibernate-validator.version>5.0.3.Final</hibernate-validator.version>
    <hibernate.version>4.3.1.Final</hibernate.version>
    <hikaricp.version>1.3.8</hikaricp.version>
    <hornetq.version>2.4.1.Final</hornetq.version>
    <hsqldb.version>2.3.2</hsqldb.version>
    <httpasyncclient.version>4.0.1</httpasyncclient.version>
    <httpclient.version>4.3.3</httpclient.version>
    <jackson.version>2.3.3</jackson.version>
    <java.version>1.6</java.version>
    <javassist.version>3.18.1-GA</javassist.version>
    <jedis.version>2.4.1</jedis.version>
    <jetty-jsp.version>2.2.0.v201112011158</jetty-jsp.version>
    <jetty.version>8.1.14.v20131031</jetty.version>
    <joda-time.version>2.3</joda-time.version>
    <jolokia.version>1.2.0</jolokia.version>
    <jstl.version>1.2</jstl.version>
    <junit.version>4.11</junit.version>
    <liquibase.version>3.0.8</liquibase.version>
    <log4j.version>1.2.17</log4j.version>
    <logback.version>1.1.2</logback.version>
    <mockito.version>1.9.5</mockito.version>
    <mongodb.version>2.12.1</mongodb.version>
    <mysql.version>5.1.30</mysql.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <reactor.version>1.1.1.RELEASE</reactor.version>
    <servlet-api.version>3.0.1</servlet-api.version>
    <slf4j.version>1.7.7</slf4j.version>
    <snakeyaml.version>1.13</snakeyaml.version>
    <solr.version>4.7.2</solr.version>
    <spock.version>0.7-groovy-2.0</spock.version>
    <spring-amqp.version>1.3.4.RELEASE</spring-amqp.version>
    <spring-batch.version>3.0.0.RELEASE</spring-batch.version>
    <spring-boot.version>1.1.0.RC1</spring-boot.version>
    <spring-data-releasetrain.version>Dijkstra-RELEASE</spring-data-releasetrain.version>
    <spring-hateoas.version>0.12.0.RELEASE</spring-hateoas.version>
    <spring-integration.version>4.0.2.RELEASE</spring-integration.version>
    <spring-loaded.version>1.2.0.RELEASE</spring-loaded.version>
    <spring-mobile.version>1.1.1.RELEASE</spring-mobile.version>
    <spring-security-jwt.version>1.0.2.RELEASE</spring-security-jwt.version>
    <spring-security.version>3.2.4.RELEASE</spring-security.version>
    <spring-social-facebook.version>1.1.1.RELEASE</spring-social-facebook.version>
    <spring-social-linkedin.version>1.0.1.RELEASE</spring-social-linkedin.version>
    <spring-social-twitter.version>1.1.0.RELEASE</spring-social-twitter.version>
    <spring-social.version>1.1.0.RELEASE</spring-social.version>
    <spring.version>4.0.5.RELEASE</spring.version>
    <thymeleaf-extras-springsecurity3.version>2.1.1.RELEASE</thymeleaf-extras-springsecurity3.version>
    <thymeleaf-layout-dialect.version>1.2.4</thymeleaf-layout-dialect.version>
    <thymeleaf.version>2.1.3.RELEASE</thymeleaf.version>
    <tomcat.version>7.0.54</tomcat.version>
    <velocity-tools.version>2.0</velocity-tools.version>
    <velocity.version>1.7</velocity.version>

2.5. Las entidades JPA

Las entidades JPA son los objetos que encapsularán las filas de las tablas de la base de datos.

  

La clase [AbstractEntity] es la clase padre de las entidades [Personne, Creneau, Rv]. Su definición es la siguiente:


package rdvmedecins.entities;

import java.io.Serializable;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

@MappedSuperclass
public class AbstractEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected Long id;
    @Version
    protected Long version;

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    // inicialización
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }

    @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id == other.id;
    }

    // getters y setters
    ..
}
  • línea 11: la anotación [@MappedSuperclass] indica que la clase anotada es padre de las entidades JPA y [@Entity];
  • líneas 15-17: definen la clave primaria [id] de cada entidad. Es la anotación [@Id] la que convierte el campo [id] en una clave primaria. La anotación [@GeneratedValue(strategy = GenerationType.AUTO)] indica que el valor de esta clave primaria es generado por SGBD y que no se impone ningún modo de generación;
  • líneas 18-19: definen el version de cada entidad. La implementación JPA incrementará este número de version cada vez que se modifique la entidad. Este número sirve para impedir la actualización simultánea de la entidad por parte de dos usuarios diferentes: dos usuarios, U1 y U2, leen la entidad E con un número de version igual a V1. U1 modifica E y guarda esta modificación en la base de datos: el n.º de version pasa entonces a V1+1. U2 modifica a su vez E y persiste esta modificación en la base de datos: recibirá una excepción porque tiene un version (V1) diferente al de la base de datos (V1+1);
  • líneas 29-33: el método [build] permite inicializar los dos campos de [AbstractEntity]. Este método hace que la referencia de la instancia [AbstractEntity] quede así inicializada;
  • líneas 36-44: se redefine el método [equals] de la clase: dos entidades se considerarán iguales si tienen el mismo nombre de clase y el mismo identificador id;

La entidad [Personne] es la clase padre de las entidades [Medecin] y [Client]:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
public class Personne extends AbstractEntity {
    private static final long serialVersionUID = 1L;
    // atributos de una persona
    @Column(length = 5)
    private String titre;
    @Column(length = 20)
    private String nom;
    @Column(length = 20)
    private String prenom;

    // constructor por defecto
    public Personne() {
    }

    // constructor con parámetros
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }

    // getters y setters
    ...
}
  • línea 6: la anotación [@MappedSuperclass] indica que la clase anotada es la clase principal de las entidades JPA y [@Entity];
  • líneas 10-15: una persona tiene un tratamiento (Srta.), un nombre (Jacqueline) y un apellido (Tatou). No se proporciona información sobre las columnas de la tabla. Por lo tanto, por defecto tendrán los mismos nombres que los campos;

La entidad [Medecin] es la siguiente:


package rdvmedecins.entities;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "medecins")
public class Medecin extends Personne {

    private static final long serialVersionUID = 1L;

    // constructor por defecto
    public Medecin() {
    }

    // constructor con parámetros
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }

}
  • línea 6: la clase es una entidad JPA;
  • línea 7: asociada a la tabla [MEDECINS] de la base de datos;
  • línea 8: la entidad [Medecin] deriva de la entidad [Personne];

Un médico se puede inicializar de la siguiente manera:

Medecin m=new Medecin("Mr","Paul","Tatou");

Si, además, se desea asignarle un identificador y un version, se podrá escribir:

Medecin m=new Medecin("Mr","Paul","Tatou").build(10,1);

donde el método [build] es el definido en [AbstractEntity].

La entidad [Client] es la siguiente:


package rdvmedecins.entities;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "clients")
public class Client extends Personne {

    private static final long serialVersionUID = 1L;

    // constructor por defecto
    public Client() {
    }

    // constructor con parámetros
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

    // identidad
    public String toString() {
        return String.format("Client[%s]", super.toString());
    }

}
  • línea 6: la clase es una entidad JPA;
  • línea 7: asociada a la tabla [CLIENTS] de la base de datos;
  • línea 8: la entidad [Client] deriva de la entidad [Personne];

La entidad [Creneau] es la siguiente:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {

    private static final long serialVersionUID = 1L;
    // características de una franja horaria de RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // un horario está vinculado a un médico
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // clave externa
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;

    // constructor por defecto
    public Creneau() {
    }

    // constructor con parámetros
    public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
        this.medecin = medecin;
        this.hdebut = hdebut;
        this.mdebut = mdebut;
        this.hfin = hfin;
        this.mfin = mfin;
    }

    // toString
    public String toString() {
        return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
    }

    // clave externa
    public long getIdMedecin() {
        return idMedecin;
    }

    // setters - getters
    ...
}
  • línea 10: la clase es una entidad JPA;
  • línea 11: asociada a la tabla [CRENEAUX] de la base de datos;
  • línea 12: la entidad [Creneau] deriva de laentidad [AbstractEntity] y, por lo tanto, hereda el identificador [id] y el de version [version];
  • línea 16: hora de inicio de la franja horaria (14);
  • línea 17: minutos de inicio del intervalo (20);
  • línea 18: hora de finalización del intervalo (14);
  • línea 19: minutos de finalización de la franja horaria (40);
  • líneas 22-24: el médico propietario de la franja horaria. La tabla [CRENEAUX] tiene una clave externa en la tabla [MEDECINS]. Esta relación se materializa en las líneas 22-24;
  • línea 22: la anotación [@ManyToOne] indica una relación de varios (franjas horarias) a uno (médico). El atributo [fetch=FetchType.LAZY] indica que, cuando se solicita una entidad [Creneau] al contexto de persistencia y esta debe buscarse en la base de datos, la entidad [Medecin] no se devuelve junto con ella. La ventaja de este modo es que la entidad [Medecin] solo se busca si el desarrollador lo solicita. De este modo, se ahorra memoria y se gana en rendimiento;
  • línea 23: indica el nombre de la columna de clave externa en la tabla [CRENEAUX];
  • líneas 27-28: la clave externa de la tabla [MEDECINS];
  • línea 27: la columna [ID_MEDECIN] ya se ha utilizado en la línea 23. Esto significa que puede modificarse de dos formas diferentes, lo que no admite la norma JPA. Por lo tanto, se añaden los atributos [insertable = false, updatable = false], lo que hace que la columna solo se pueda leer;

La entidad [Rv] es la siguiente:


package rdvmedecins.entities;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // características de un Rv
    @Temporal(TemporalType.DATE)
    private Date jour;

    // un rv está vinculado a un cliente
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // un rv está vinculado a una franja horaria
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

    // claves externas
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;

    // fabricante por defecto
    public Rv() {
    }

    // con parámetros
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }

    // toString
    public String toString() {
        return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
    }

    // claves externas
    public long getIdCreneau() {
        return idCreneau;
    }

    public long getIdClient() {
        return idClient;
    }

    // getters y setters
...
}
  • línea 14: la clase es una entidad JPA;
  • línea 15: asociada a la tabla [RV] de la base de datos;
  • línea 16: la entidad [Rv] deriva de laentidad [AbstractEntity] y, por lo tanto, hereda el identificador [id] y el de version [version];
  • línea 21: la fecha de la cita;
  • línea 20: el tipo [Date] de Java contiene tanto una fecha como una hora. Aquí se especifica que solo se utiliza la fecha;
  • líneas 24-26: el cliente para el que se ha concertado esta cita. La tabla [RV] tiene una clave externa en la tabla [CLIENTS]. Esta relación se materializa en las líneas 24-26;
  • líneas 29-31: el intervalo horario de la cita. La tabla [RV] tiene una clave externa en la tabla [CRENEAUX]. Esta relación se materializa en las líneas 29-31;
  • líneas 34-35: la clave externa [idClient];
  • líneas 36-37: la clave externa [idCreneau];

2.6. La capa [DAO]

Vamos a implementar la capa [DAO] con Spring Data:

  

La capa [DAO] se implementa con cuatro interfaces Spring Data:

  • [ClientRepository]: da acceso a las entidades JPA y [Client];
  • [CreneauRepository]: da acceso a las entidades JPA y [Creneau];
  • [MedecinRepository]: da acceso a las entidades JPA y [Medecin];
  • [RvRepository]: da acceso a las entidades JPA y [Rv];

La interfaz [MedecinRepository] es la siguiente:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Medecin;

public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
  • línea 7: la interfaz [MedecinRepository] se limita a heredar los métodos de la interfaz [CrudRepository] sin añadir otros;

La interfaz [ClientRepository] es la siguiente:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Client;

public interface ClientRepository extends CrudRepository<Client, Long> {
}
  • línea 7: la interfaz [ClientRepository] se limita a heredar los métodos de la interfaz [CrudRepository] sin añadir otros;

La interfaz [CreneauRepository] es la siguiente:


package rdvmedecins.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Creneau;

public interface CreneauRepository extends CrudRepository<Creneau, Long> {
    // lista de franjas horarias de un médico
    @Query("select c from Creneau c where c.medecin.id=?1")
    Iterable<Creneau> getAllCreneaux(long idMedecin);
}
  • línea 8: la interfaz [CreneauRepository] hereda los métodos de la interfaz [CrudRepository];
  • líneas 10-11: el método [getAllCreneaux] permite obtener los horarios disponibles de un médico;
  • línea 11: el parámetro es el identificador del médico. El resultado es una lista de franjas horarias en forma de un objeto [Iterable<Creneau>];
  • línea 10: la anotación [@Query] permite especificar la consulta JPQL (Java Persistence Query Language) que implementa el método. El parámetro [?1] se sustituirá por el parámetro [idMedecin] del método;

La interfaz [RvRepository] es la siguiente:


package rdvmedecins.repositories;

import java.util.Date;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Rv;

public interface RvRepository extends CrudRepository<Rv, Long> {

    @Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
    Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
  • línea 10: la interfaz [RvRepository] hereda los métodos de la interfaz [CrudRepository];
  • líneas 12-13: el método [getRvMedecinJour] permite obtener las citas de un médico para un día determinado;
  • línea 13: los parámetros son el identificador del médico y el día. El resultado es una lista de citas en forma de un objeto [Iterable<Rv>];
  • línea 12: la anotación [@Query] permite especificar la consulta JPQL que implementa el método. El parámetro [?1] se sustituirá por el parámetro [idMedecin] del método y el parámetro [?2] se sustituirá por el parámetro [jour] del método. No basta con la siguiente consulta JPQL:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

ya que los campos de la clase Rv, de los tipos [Client] y [Creneau], se obtienen en modo [FetchType.LAZY], lo que significa que deben solicitarse explícitamente para poder obtenerlos. Esto se hace en la consulta JPQL con la sintaxis [left join fetch entité], que solicita que se realice una unión con la tabla a la que apunta la clave externa para recuperar la entidad a la que se apunta;

2.7. La capa [métier]

  
  • [IMetier] es la interfaz de la capa [métier] y [Metier] son su implementación;
  • [AgendaMedecinJour] y [CreneauMedecinJour] son dos entidades de negocio;

2.7.1. Las entidades

La entidad [CreneauMedecinJour] asocia un intervalo de tiempo y la posible cita concertada en dicho intervalo:


package rdvmedecins.domain;

import java.io.Serializable;

import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;

public class CreneauMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // campos
    private Creneau creneau;
    private Rv rv;

    // constructores
    public CreneauMedecinJour() {

    }

    public CreneauMedecinJour(Creneau creneau, Rv rv) {
        this.creneau=creneau;
        this.rv=rv;
    }

    // toString
    @Override
    public String toString() {
        return String.format("[%s %s]", creneau, rv);
    }

    // getters y setters
...
}
  • línea 12: el intervalo horario;
  • línea 13: la posible cita – null en caso contrario;

La entidad [AgendaMedecinJour] es el agenda de un médico para un día determinado, es decir, la lista de sus citas:


package rdvmedecins.domain;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;

import rdvmedecins.entities.Medecin;

public class AgendaMedecinJour implements Serializable {

    private static final long serialVersionUID = 1L;
    // campos
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

    // constructores
    public AgendaMedecinJour() {

    }

    public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
        this.medecin = medecin;
        this.jour = jour;
        this.creneauxMedecinJour = creneauxMedecinJour;
    }

    public String toString() {
        StringBuffer str = new StringBuffer("");
        for (CreneauMedecinJour cr : creneauxMedecinJour) {
            str.append(" ");
            str.append(cr.toString());
        }
        return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
    }

    // getters y setters
...
}
  • línea 13: el médico;
  • línea 14: el día en el agenda;
  • línea 15: sus franjas horarias con o sin cita;

2.7.2. El servicio

La interfaz de la capa [métier] es la siguiente:


package rdvmedecins.metier;

import java.util.Date;
import java.util.List;

import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;

public interface IMetier {

    // lista de clients
    public List<Client> getAllClients();

    // lista de médicos
    public List<Medecin> getAllMedecins();

    // lista de franjas horarias de un médico
    public List<Creneau> getAllCreneaux(long idMedecin);

    // lista de Rv de un médico, en un día determinado
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour);

    // buscar un cliente identificado por su id
    public Client getClientById(long id);

    // buscar un cliente identificado por su id
    public Medecin getMedecinById(long id);

    // buscar un Rv identificado por su id
    public Rv getRvById(long id);

    // buscar una franja horaria identificada por su id
    public Creneau getCreneauById(long id);

    // añadir un RV
    public Rv ajouterRv(Date jour, Creneau créneau, Client client);

    // eliminar un RV
    public void supprimerRv(Rv rv);

    // profesión
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);

}

Los comentarios explican la función de cada uno de los métodos.

La implementación de la interfaz [IMetier] es la siguiente clase [Metier]:


package rdvmedecins.metier;

import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;

import com.google.common.collect.Lists;

@Service("métier")
public class Metier implements IMetier {

    // repositorios
    @Autowired
    private MedecinRepository medecinRepository;
    @Autowired
    private ClientRepository clientRepository;
    @Autowired
    private CreneauRepository creneauRepository;
    @Autowired
    private RvRepository rvRepository;

    // implementación de interfaz
    @Override
    public List<Client> getAllClients() {
        return Lists.newArrayList(clientRepository.findAll());
    }

    @Override
    public List<Medecin> getAllMedecins() {
        return Lists.newArrayList(medecinRepository.findAll());
    }

    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
    }

    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
    }

    @Override
    public Client getClientById(long id) {
        return clientRepository.findOne(id);
    }

    @Override
    public Medecin getMedecinById(long id) {
        return medecinRepository.findOne(id);
    }

    @Override
    public Rv getRvById(long id) {
        return rvRepository.findOne(id);
    }

    @Override
    public Creneau getCreneauById(long id) {
        return creneauRepository.findOne(id);
    }

    @Override
    public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
        return rvRepository.save(new Rv(jour, client, créneau));
    }

    @Override
    public void supprimerRv(Rv rv) {
        rvRepository.delete(rv.getId());
    }

    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
    ...
    }

}
  • línea 24: la anotación [@Service] es una anotación de Spring que convierte a la clase anotada en un componente gestionado por Spring. Se puede asignar o no un nombre a un componente. Este se denomina [métier];
  • línea 25: la clase [Metier] implementa la interfaz [IMetier];
  • línea 28: la anotación [@Autowired] es una anotación de Spring. El valor del campo así anotado será inicializado (inyectado) por Spring con la referencia de un componente de Spring del tipo o del nombre especificados. En este caso, la anotación [@Autowired] no especifica ningún nombre. Por lo tanto, se realizará una inyección por tipo;
  • línea 29: el campo [medecinRepository] se inicializará con la referencia de un componente Spring de tipo [MedecinRepository]. Será la referencia de la clase generada por Spring Data para implementar la interfaz [MedecinRepository] que ya hemos presentado;
  • líneas 30-35: este proceso se repite para las otras tres interfaces estudiadas;
  • líneas 39-41: implementación del método [getAllClients];
  • línea 40: utilizamos el método [findAll] de la interfaz [ClientRepository]. Este método devuelve un tipo [Iterable<Client>] que transformamos en [List<Client>] con el método estático [Lists.newArrayList]. La clase [Lists] está definida en la biblioteca Google Guava. En [pom.xml] se ha importado esta dependencia:

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
  • líneas 38-86: los métodos de la interfaz [IMetier] se implementan con la ayuda de las clases de la capa [DAO];

Solo el método de la línea 88 es específico de la capa [métier]. Se ha incluido aquí porque realiza un procesamiento específico del negocio que va más allá de un simple acceso a los datos. Sin este método, no habría habido motivo para crear una capa [métier]. El método [getAgendaMedecinJour] es el siguiente:


public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        // lista de franjas horarias del médico
        List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
        // lista de reservas de ese mismo médico para ese mismo día
        List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
        // se crea un diccionario a partir de los Rv tomados
        Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
        for (Rv resa : reservations) {
            hReservations.put(resa.getCreneau().getId(), resa);
        }
        // se crea el agenda para el día solicitado
        AgendaMedecinJour agenda = new AgendaMedecinJour();
        // el médico
        agenda.setMedecin(getMedecinById(idMedecin));
        // el día
        agenda.setJour(jour);
        // los intervalos de reserva
        CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
        agenda.setCreneauxMedecinJour(creneauxMedecinJour);
        // rellenar los intervalos de reserva
        for (int i = 0; i < creneauxHoraires.size(); i++) {
            // línea i agenda
            creneauxMedecinJour[i] = new CreneauMedecinJour();
            // franja horaria
            Creneau créneau = creneauxHoraires.get(i);
            long idCreneau = créneau.getId();
            creneauxMedecinJour[i].setCreneau(créneau);
            // ¿El intervalo está libre o reservado?
            if (hReservations.containsKey(idCreneau)) {
                // el horario está ocupado - se anota la reserva
                Rv resa = hReservations.get(idCreneau);
                creneauxMedecinJour[i].setRv(resa);
            }
        }
        // se devuelve el resultado
        return agenda;
    }

Se invita al lector a leer los comentarios. El algoritmo es el siguiente:

  • se recuperan todas las franjas horarias del médico indicado;
  • se recuperan todas sus citas para el día indicado;
  • con esta información, se puede determinar si un horario está libre u ocupado;

2.8. La configuración del proyecto

  

La clase [DomainAndPersitenceConfig] configura todo el proyecto:


package rdvmedecins.config;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {

    // la fuente de datos MySQL
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        return dataSource;
    }

    // el proveedor JPA - no es necesario si se aceptan los valores predeterminados utilizados por Spring Boot
    // aquí lo definimos para activar/desactivar los registros SQL
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory y TransactionManager se definen con valores predeterminados por Spring Boot

}
  • líneas 45: no vamos a definir los beans [EntityManagerFactory] y [TransactionManager]. Para ello, nos basaremos en la anotación [@EnableAutoConfiguration] de Spring Boot (línea 17);
  • líneas 24-32: definen la fuente de datos MySQL5. Se trata de un bean que, por lo general, Spring Boot no puede deducir;
  • líneas 36-43: también configuramos la implementación JPA para establecer el atributo [showSql] de Hibernate en falso (línea 39). Por defecto, está en verdadero;
  • por el momento, los únicos componentes gestionados por Spring son los beans de las líneas 25 y 37, además de los beans [EntityManagerFactory] y [TransactionManager] mediante autoconfiguración. Tenemos que añadir los beans de las capas [métier] y [DAO];
  • la línea 16 añade al contexto de Spring las interfaces del paquete [rdvmdecins.repositories] que heredan de la interfaz [CrudRepository];
  • la línea 18 añade al contexto de Spring todas las clases del paquete [rdvmedecins] y sus descendientes que tengan una anotación Spring. En el paquete [rdvmdecins.metier], se encontrará la clase [Metier] con su anotación [@Service] y se añadirá al contexto de Spring;
  • línea 45: Spring Boot definirá por defecto un bean [entityManagerFactory]. Hay que indicar a este bean dónde se encuentran las entidades JPA que debe gestionar. Esto se hace en la línea 19;
  • línea 20: indica que los métodos de las interfaces que heredan de la interfaz [CrudRepository] deben ejecutarse dentro de una transacción;

2.9. Las pruebas de la capa [métier]

  

La clase [rdvmedecins.tests.Metier] es una clase de prueba Spring / JUnit 4:


package rdvmedecins.tests;

import java.text.ParseException;
import java.util.Date;
import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;

@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {

    @Autowired
    private IMetier métier;

    @Test
    public void test1(){
        // visualización clients
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // visualización de médicos
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // visualización de franjas horarias de un médico
        Medecin médecin = medecins.get(0);
        List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
        display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
        // lista de Rv de un médico, en un día determinado
        Date jour = new Date();
        display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // añadir un RV
        Rv rv = null;
        Creneau créneau = creneaux.get(2);
        Client client = clients.get(0);
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        rv = métier.ajouterRv(jour, créneau, client);
        // verificación
        Rv rv2 = métier.getRvById(rv.getId());
        Assert.assertEquals(rv, rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // Añadir un RV en el mismo horario del mismo día
        // debe provocar una excepción
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        Boolean erreur = false;
        try {
            rv = métier.ajouterRv(jour, créneau, client);
            System.out.println("Rv ajouté");
        } catch (Exception ex) {
            Throwable th = ex;
            while (th != null) {
                System.out.println(ex.getMessage());
                th = th.getCause();
            }
            // se anota el error
            erreur = true;
        }
        // se comprueba que se ha producido un error
        Assert.assertTrue(erreur);
        // lista de RV
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // visualización de agenda
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // eliminar un RV
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // verificación
        rv2 = métier.getRvById(rv.getId());
        Assert.assertNull(rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
    }

    // método de utilidad: muestra los elementos de una colección
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }

}
  • línea 22: la anotación [@SpringApplicationConfiguration] permite utilizar el archivo de configuración [DomainAndPersistenceConfig] estudiado anteriormente. De este modo, la clase de prueba se beneficia de todos los beans definidos por este archivo;
  • línea 23: la anotación [@RunWith] permite la integración de Spring con JUnit: la clase podrá ejecutarse como una prueba JUnit. [@RunWith] es una anotación JUnit (línea 9), mientras que la clase [SpringJUnit4ClassRunner] es una clase Spring (línea 12);
  • líneas 26-27: inyección en la clase de prueba de una referencia a la capa [métier];
  • muchas pruebas son simples pruebas visuales:
    • líneas 32-33: lista de clients;
    • líneas 35-36: lista de médicos;
    • líneas 39-40: lista de franjas horarias de un médico;
    • línea 43: lista de citas de un médico;
  • línea 50: añadir una nueva cita. El método [ajouterRv] devuelve la cita con información adicional, su clave primaria id;
  • línea 53: se utiliza esta clave primaria para buscar la cita en la base de datos;
  • línea 54: se comprueba que la cita buscada y la cita encontrada sean la misma. Recordemos que el método [equals] de la entidad [Rv] ha sido redefinido: dos citas son iguales si tienen el mismo id. En este caso, esto nos indica que la cita añadida se ha introducido correctamente en la base de datos;
  • líneas 61-73: se intenta añadir por segunda vez la misma cita. Esto debe ser rechazado por el SGBD, ya que existe una restricción de unicidad:

CREATE TABLE IF NOT EXISTS `rv` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `JOUR` date NOT NULL,
  `ID_CLIENT` bigint(20) NOT NULL,
  `ID_CRENEAU` bigint(20) NOT NULL,
  `VERSION` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`ID`),
  UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
  KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
  KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;

La línea 8 anterior indica que la combinación [JOUR, ID_CRENEAU] debe ser única, lo que impide introducir dos citas el mismo día en la misma franja horaria.

  • línea 73: se comprueba que se ha producido una excepción;
  • línea 77: se solicita el agenda del médico para el que se acaba de añadir una cita;
  • línea 79: se comprueba que la cita añadida aparece efectivamente en su agenda;
  • línea 82: se elimina la cita añadida;
  • línea 84: se busca en la base de datos la cita eliminada;
  • línea 85: se comprueba que se ha recuperado un puntero null, lo que indica que la cita buscada no existe;

La ejecución de la prueba se realiza con éxito:

 

2.10. El programa de consola

  

El programa de consola es básico. Muestra cómo recuperar una clave externa:


package rdvmedecins.boot;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;

import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;

public class Boot {
    // el arranque
    public static void main(String[] args) {
        // preparamos la configuración
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // la iniciamos
        ConfigurableApplicationContext context = app.run(args);
        // función
        IMetier métier = context.getBean(IMetier.class);
        try {
            // añadir un RV
            Date jour = new Date();
            System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
            Client client = (Client) new Client().build(1L, 1L);
            Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
            Rv rv = métier.ajouterRv(jour, créneau, client);
            System.out.println(String.format("Rv ajouté = %s", rv));
            // verificación
            créneau = métier.getCreneauById(1L);
            long idMedecin = créneau.getIdMedecin();
            display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
        } catch (Exception ex) {
            System.out.println("Exception : " + ex.getCause());
        }
        // cierre del contexto Spring
        context.close();
    }

    // método de utilidad: muestra los elementos de una colección
    private static <T> void display(String message, Iterable<T> elements) {
        System.out.println(message);
        for (T element : elements) {
            System.out.println(element);
        }
    }

}

El programa añade una cita y, a continuación, comprueba que se ha añadido.

  • línea 19: la clase [SpringApplication] utilizará la clase de configuración [DomainAndPersistenceConfig];
  • línea 20: supresión de los registros de inicio de la aplicación;
  • línea 22: se ejecuta la clase [SpringApplication]. Devuelve un contexto Spring, es decir, la lista de beans registrados;
  • línea 24: se recupera una referencia al bean que implementa la interfaz [IMetier]. Se trata, por tanto, de una referencia a la capa [métier];
  • líneas 27-31: se añade una nueva cita para hoy, para el cliente n.º 1 en la franja horaria n.º 1. El cliente y la franja horaria se han creado de la nada para mostrar que solo se utilizan los identificadores. Aquí se ha inicializado la version, pero se podría haber puesto cualquier cosa. Aquí no se utiliza;
  • línea 34: queremos saber quién es el médico asignado a la franja horaria n.º 1. Para ello, tenemos que consultar la base de datos y buscar la franja horaria n.º 1. Como estamos en el modo [FetchType.LAZY], el médico no aparece junto con la franja horaria. Sin embargo, nos hemos asegurado de incluir un campo [idMedecin] en la entidad [Creneau] para recuperar la clave primaria del médico;
  • línea 35: recuperamos la clave primaria del médico;
  • línea 36: se muestra la lista de citas del médico;

Los resultados de la consola son los siguientes:

1
2
3
4
Ajout d'un Rv le [10/06/2014] dans le créneau 1 pour le client 1
Rv ajouté = Rv[113, Tue Jun 10 16:51:01 CEST 2014, 1, 1]
Liste des rendez-vous
Rv[113, 2014-06-10, 1, 1]

2.11. Introducción a Spring MVC

Ahora abordamos la construcción de la capa web. Esta se compone principalmente de métodos que procesan URL precisos y responden con una línea de texto en formato JSON (Javascript Object Notation). Esta capa web es una interfaz web que a veces se denomina «API web». Vamos a implementar esta interfaz con Spring MVC, otra rama del ecosistema Spring. Comenzamos por estudiar una de las guías que se encuentran en [http://spring.io].

2.11.1. El proyecto de demostración

  • en [1], importamos una de las guías de Spring;
  • en [2], elegimos el ejemplo [Rest Service];
  • en [3], elegimos el proyecto Maven;
  • en [4], tomamos la version final de la guía;
  • en [5], validamos;
  • en [6], el proyecto importado;

Los servicios web accesibles a través de URL estándar y que proporcionan texto JSON suelen denominarse servicios REST (REpresentational State Transfer). En este documento, me limitaré a llamar al servicio que vamos a construir un servicio web / JSON. Se dice que un servicio es Restful si cumple ciertas reglas. No he intentado cumplirlas.

Examinemos ahora el proyecto importado, en primer lugar su configuración de Maven.

2.11.2. Configuración de Maven

El archivo [pom.xml] es el siguiente:


<?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.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <url>http://repo.spring.io/release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <url>http://repo.spring.io/release</url>
        </pluginRepository>
    </pluginRepositories>
</project>
  • líneas 10-14: al igual que en el proyecto [Spring Data], encontramos el proyecto padre [Spring Boot];
  • líneas 17-20: el artefacto [spring-boot-starter-web] incluye las bibliotecas necesarias para un proyecto Spring MVC. En concreto, incluye un servidor Tomcat integrado. Es en este servidor donde se ejecutará la aplicación;
  • líneas 21-24: la biblioteca Jackson gestiona la transformación de un objeto Java en una cadena y viceversa;

Las bibliotecas que incluye esta configuración son muy numerosas:

Arriba se ven los tres archivos del servidor Tomcat.

2.11.3. La arquitectura de un servicio Spring REST

Spring MVC implementa el modelo de arquitectura denominado MVC (Modelo – Vista – Controlador) de la siguiente manera:

El procesamiento de una solicitud de un cliente se lleva a cabo de la siguiente manera:

  1. solicitud: las URL solicitadas tienen el formato http://máquina:puerto/contexto/Acción/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] es la clase de Spring que procesa las URL entrantes. Ella «enruta» la URL hacia la acción que debe procesarla. Estas acciones son métodos de clases específicas denominadas [Contrôleurs]. La C de MVC es aquí la cadena [Dispatcher Servlet, Contrôleur, Action]. Si no se ha configurado ninguna acción para procesar el URL entrante, el servlet [Dispatcher Servlet] responderá que no se ha encontrado el URL solicitado (error 404 NOT FOUND);
  1. procesamiento
  • la acción seleccionada puede utilizar los parámetros parami que le ha transmitido el servlet [Dispatcher Servlet]. Estos pueden proceder de varias fuentes:
    • la ruta [/param1/param2/...] del URL,
    • de los parámetros [p1=v1&p2=v2] del URL,
    • de los parámetros enviados por el navegador con su solicitud;
  • en el procesamiento de la solicitud del usuario, la acción puede necesitar la capa [metier] [2b]. Una vez procesada la solicitud del cliente, esta puede generar diversas respuestas. Un ejemplo clásico es:
    • una página de error si la solicitud no se ha podido procesar correctamente
    • una página de confirmación en caso contrario
  • la acción solicita que se muestre una vista determinada [3]. Esta vista mostrará datos que se denominan el modelo de la vista. Es la M de MVC. La acción creará este modelo M [2c] y solicitará que se muestre una vista V [3];
  1. respuesta: la vista V seleccionada utiliza la plantilla M creada por la acción para inicializar las partes dinámicas de la respuesta HTML que debe enviar al cliente y, a continuación, envía dicha respuesta.

Para un servicio web / JSON, la arquitectura anterior se modifica ligeramente:

  • en [4a], el modelo, que es una clase Java, se transforma en una cadena JSON mediante una biblioteca JSON;
  • en [4b], esta cadena JSON se envía al navegador;

2.11.4. El controlador C

  

La aplicación importada tiene el siguiente controlador:


package hello;

import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @RequestMapping("/greeting")
    public @ResponseBody
    Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }
}
  • línea 9: la anotación [@Controller] convierte la clase [GreetingController] en un controlador Spring, es decir, que sus métodos se registran para procesar URL;
  • línea 15: la anotación [@RequestMapping] indica el URL que procesa el método, en este caso el URL [/greeting]. Veremos más adelante que este URL se puede configurar y que es posible recuperar estos parámetros;
  • línea 16: la anotación [@ResponseBody] indica que el método no genera una plantilla para una vista (JSP, JSF, Thymeleaf, ...) que se enviará posteriormente al navegador del cliente, sino que genera por sí misma la respuesta enviada al navegador. En este caso, genera un objeto de tipo [Greeting] (línea 18). Aunque aquí no se vea, este objeto se transformará primero en JSON antes de enviarse al navegador. Es la presencia de una biblioteca JSON en las dependencias del proyecto lo que hace que Spring Boot, mediante autoconfiguración, configure el proyecto de esta manera;
  • línea 17: el método [greeting] tiene un parámetro [String name]. La anotación [@RequestParam(value = "name", required = false, defaultValue = "World"] indica que este parámetro debe inicializarse con un parámetro denominado [name](@RequestParam(value = "name"). Este puede ser el parámetro de un GET o de un POST. Este parámetro no es obligatorio (required = false). En este último caso, el parámetro [name] del método se inicializará con el valor [World] (defaultValue = «World»).

2.11.5. El modelo M

El modelo M generado por el método anterior es el siguiente objeto [Greeting]:

  

package hello;

public class Greeting {

    private final long id;
    private final String content;

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }
}

La transformación JSON de este objeto creará la cadena de caracteres {"id":n,"content":"texto"}. Al final, la cadena JSON generada por el método del controlador tendrá la forma:

{"id":2,"content":"Hello, World!"}

o

{"id":2,"content":"Hello, John!"}

2.11.6. Configuración del proyecto

  

El proyecto se configura mediante la siguiente clase [Application]:


package hello;

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

@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • línea 11: curiosamente, esta clase se ejecuta con un método [main] propio de las aplicaciones de consola. Así es. La clase [SpringApplication] de la línea 12 iniciará el servidor Tomcat presente en las dependencias e implementará el servicio REST en él;
  • línea 4: vemos que la clase [SpringApplication] pertenece al proyecto [Spring Boot];
  • línea 12: el primer parámetro es la clase que configura el proyecto, el segundo los posibles parámetros;
  • línea 8: la anotación [@EnableAutoConfiguration] solicita a Spring Boot que realice la configuración del proyecto;
  • línea 7: la anotación [@ComponentScan] hace que se explore la carpeta que contiene la clase [Application] para buscar los componentes de Spring. Se encontrará uno, la clase [GreetingController], que tiene la anotación [@Controller], lo que la convierte en un componente Spring;

2.11.7. Ejecución del proyecto

Ejecutemos el proyecto:

 

Se obtienen los siguientes registros de consola:

____ _ __ _ _

 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.0.RELEASE)

2014-06-11 14:31:36.435  INFO 11744 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 11744 (D:\Temp\wksSTS\gs-rest-service-complete\target\classes started by ST in D:\Temp\wksSTS\gs-rest-service-complete)
2014-06-11 14:31:36.473  INFO 11744 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7684af0b: startup date [Wed Jun 11 14:31:36 CEST 2014]; root of context hierarchy
2014-06-11 14:31:36.966  INFO 11744 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-06-11 14:31:37.760  INFO 11744 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-06-11 14:31:37.955  INFO 11744 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-06-11 14:31:37.956  INFO 11744 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.54
2014-06-11 14:31:38.053  INFO 11744 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-06-11 14:31:38.054  INFO 11744 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1584 ms
2014-06-11 14:31:38.596  INFO 11744 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-06-11 14:31:38.598  INFO 11744 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-06-11 14:31:38.919  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-11 14:31:39.125  INFO 11744 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-06-11 14:31:39.129  INFO 11744 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-06-11 14:31:39.130  INFO 11744 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-06-11 14:31:39.160  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler de tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-11 14:31:39.160  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler de tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-11 14:31:39.448  INFO 11744 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-06-11 14:31:39.490  INFO 11744 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-06-11 14:31:39.492  INFO 11744 --- [           main] hello.Application                        : Started Application in 3.45 seconds (JVM running for 3.93)
  • línea 12: el servidor Tomcat se inicia en el puerto 8080 (línea 11);
  • línea 16: el servlet [DispatcherServlet] está presente;
  • línea 19: se ha detectado el método [GreetingController.greeting];

Para probar la aplicación web, se solicita el URL [http://localhost:8080/greeting]:

 

Se recibe correctamente la cadena esperada JSON. Puede resultar interesante ver los encabezados HTTP enviados por el servidor. Para ello, utilizaremos la extensión de Chrome llamada [Advanced Rest Client] (véanse los anexos):

  • en [1], el URL solicitado;
  • en [2], se utiliza el método GET;
  • en [3], la respuesta JSON;
  • en [4], el servidor indicó que enviaba una respuesta en formato JSON;
  • en [5], se solicita el mismo URL, pero esta vez con un POST;
  • en [7], la información se envía al servidor en forma de [urlencoded];
  • en [6], el parámetro name con su valor;
  • en [8], el navegador indica al servidor que le envía información [urlencoded];
  • en [9], la respuesta JSON del servidor;

2.11.8. Creación de un archivo ejecutable

Es posible crear un archivo ejecutable fuera de Eclipse. La configuración necesaria se encuentra en el archivo [pom.xml]:


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
</build>
  • las líneas 9-12 definen el complemento que creará el archivo ejecutable;
  • la línea 3 define la clase ejecutable del proyecto;

Se procede de la siguiente manera:

  • en [1]: se ejecuta un objetivo de Maven;
  • en [2]: hay dos objetivos (goals): [clean] para eliminar la carpeta [target] del proyecto Maven, y [package] para regenerarla;
  • en [3]: la carpeta [target] generada se guardará en esta carpeta;
  • en [4]: se genera el archivo de destino;

En los registros que aparecen en la consola, es importante que aparezca el complemento [spring-boot-maven-plugin]. Es este el que genera el archivo ejecutable.

[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---

Con una consola, nos situamos en la carpeta generada:

1
2
3
4
5
6
7
8
9
D:\Temp\wksSTS\gs-rest-service-complete\target>dir
 ...
11/06/2014  15:30    <DIR>          classes
11/06/2014  15:30    <DIR>          generated-sources
11/06/2014  15:30        11 073 572 gs-rest-service-0.1.0.jar
11/06/2014  15:30             3 690 gs-rest-service-0.1.0.jar.original
11/06/2014  15:30    <DIR>          maven-archiver
11/06/2014  15:30    <DIR>          maven-status
...
  • línea 5: el archivo generado;

Este archivo se ejecuta de la siguiente manera:

D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.0.RELEASE)

2014-06-11 15:32:47.088  INFO 4972 --- [           main] hello.Application
                  : Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...

Ahora que la aplicación web se ha iniciado, se puede acceder a ella con un navegador:

 

2.11.9. Implementar la aplicación en un servidor Tomcat

Aunque Spring Boot resulta muy práctico en modo de desarrollo, es probable que una aplicación en producción se implemente en un servidor Tomcat real. A continuación se explica cómo hacerlo:

Modifica el archivo [pom.xml] de la siguiente manera:


<?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.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>
....
</project>

Los cambios deben realizarse en dos lugares:

  • línea 9: hay que indicar que se va a generar un archivo WAR (Web ARchive);
  • líneas 26-30: hay que añadir una dependencia del artefacto [spring-boot-starter-tomcat]. Este artefacto incluye todas las clases de Tomcat en las dependencias del proyecto;
  • línea 29: este artefacto es [provided], es decir, que los archivos correspondientes no se incluirán en el WAR generado. De hecho, estos archivos se encontrarán en el servidor Tomcat en el que se ejecutará la aplicación;

Además, hay que configurar la aplicación web. A falta del archivo [web.xml], esto se hace con una clase que hereda de [SpringBootServletInitializer]:

  

La clase [ApplicationInitializer] es la siguiente:


package hello;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class ApplicationInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

}
  • línea 6: la clase [ApplicationInitializer] extiende la clase [SpringBootServletInitializer];
  • línea 9: el método [configure] se redefine (línea 8);
  • línea 10: se proporciona la clase que configura el proyecto;

Para ejecutar el proyecto, se puede proceder de la siguiente manera:

  • en [1], se ejecuta el proyecto en uno de los servidores registrados en IDE Eclipse;
  • en [2], se selecciona [tc Server Developer], que está presente por defecto. Se trata de una variante de Tomcat;

Una vez hecho esto, se puede solicitar el URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] en un navegador:

 

Ahora sabemos cómo generar un archivo WAR. A continuación, seguiremos trabajando con Spring Boot y su archivo ejecutable jar.

2.11.10. Crear un nuevo proyecto web

Para crear un nuevo proyecto web, se puede proceder de la siguiente manera:

  • en [1]: Archivo / Nuevo / Proyecto Spring Starter
  • en [2]: seleccionar [Web]. No se seleccionan bibliotecas de vistas porque en un servicio web / JSON no hay vistas;
  • el proyecto creado será un proyecto Maven. En [3], se introduce el grupo del artefacto Maven que se va a crear; en [4], el nombre del artefacto;
  • en [5], se introduce el nombre de un paquete donde Spring colocará la clase de configuración del proyecto;
  • en [6], se le da un nombre al proyecto Eclipse, que puede ser diferente de [4];
 

2.12. La capa [web]

  

Vamos a construir la capa web en varias etapas:

  • paso 1: una capa web operativa sin autenticación;
  • paso 2: implementación de la autenticación con Spring Security;
  • paso 3: implementación de CORS [Cross-origin resource sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]. El cliente de nuestro servicio web será un cliente web Angular que no pertenecerá necesariamente al mismo dominio que nuestro servicio web. Por defecto, no podrá acceder a él a menos que el servicio web se lo permita. Veremos cómo;

2.12.1. Configuración de Maven

El archivo [pom.xml] del proyecto es el siguiente:


<modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring4.mvc</groupId>
    <artifactId>rdvmedecins-webapi-v1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rdvmedecins-webapi-v1</name>
    <description>Gestion de RV Médecins</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.0.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>istia.st.spring4.rdvmedecins</groupId>
            <artifactId>rdvmedecins-metier-dao</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
  • líneas 7-11: el proyecto Maven principal;
  • líneas 13-16: las dependencias para un proyecto Spring MVC;
  • líneas 17-21: las dependencias del proyecto de capas [métier, DAO, JPA];

2.12.2. La interfaz del servicio web

  • en [1], arriba, el navegador solo puede solicitar un número limitado de URL con una sintaxis precisa;
  • en [4], recibe una respuesta JSON;

Las respuestas de nuestro servicio web tendrán todas el mismo formato, correspondiente a la transformación JSON de un objeto de tipo [Reponse] como el siguiente:


package rdvmedecins.web.models;

public class Reponse {

    // ----------------- propiedades
    // estado de la operación
    private int status;
    // la respuesta JSON
    private Object data;

    // ---------------constructores
    public Reponse() {
    }

    public Reponse(int status, Object data) {
        this.status = status;
        this.data = data;
    }

    // métodos
    public void incrStatusBy(int increment) {
        status += increment;
    }

    // ----------------------getters y setters
...
}
  • línea 7: código de error de la respuesta 0: OK, en caso contrario: KO;
  • línea 9: el cuerpo de la respuesta;

A continuación, presentamos las capturas de pantalla que ilustran la interfaz del servicio web / JSON:

Lista de todos los pacientes de la consulta médica [/getAllClients]

Lista de todos los médicos de la consulta médica [/getAllMedecins]

Lista de franjas horarias de un médico [/getAllCreneaux/{idMedecin}]

Lista de citas de un médico [/getRvMedecinJour/{idMedecin}/{aaaa-mm-dd}

Agenda de un médico [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]

Para añadir o eliminar una cita utilizamos la extensión de Chrome [Advanced Rest Client], ya que estas operaciones se realizan con un POST.

Añadir una cita [/ajouterRv]

  • en [0], el URL del servicio web;
  • en [1], se utiliza el método POST;
  • en [2], el texto JSON de la información transmitida al servicio web en forma de {día, idClient, idCreneau};
  • en [3], el cliente indica al servicio web que le envía información en formato JSON;

La respuesta es entonces la siguiente:

  • en [4]: el cliente envía el encabezado indicando que los datos que envía están en formato JSON;
  • en [5]: el servicio web responde que él también envía JSON;
  • en [6]: la respuesta JSON del servicio web. El campo [data] contiene la forma JSON de la cita añadida;

Se puede comprobar la presencia de la nueva cita:

Eliminar una cita [/supprimerRv]

  • en [1], el URL del servicio web;
  • en [2], se utiliza el método POST;
  • en [3], el texto JSON de la información transmitida al servicio web en forma de {idRv};
  • en [4], el cliente indica al servicio web que le envía información JSON;

La respuesta es entonces la siguiente:

  • en [5]: el campo [status] es 0, lo que indica que la operación se ha realizado correctamente;

Se puede comprobar la eliminación de la cita:

En la imagen anterior, la cita del paciente [Mme GERMAN] ya no aparece.

El servicio web también permite recuperar entidades a través de su identificador:

Todas estas URL son procesadas por el controlador [RdvMedecinsController] que presentamos a continuación.

2.12.3. Estructura del controlador [RdvMedecinsController]

  

El controlador [RdvMedecinsController] es el siguiente:


package rdvmedecins.web.controllers;

import java.text.ParseException;
...

@RestController
public class RdvMedecinsController {

    @Autowired
    private ApplicationModel application;
    private List<String> messages;

    @PostConstruct
    public void init() {
        // mensajes de error de la aplicación
        messages = application.getMessages();
    }

    // lista de médicos
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
...
    }

    // lista de clients
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients() {
...
    }

    // lista de franjas horarias de un médico
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
    }

    // lista de citas de un médico
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
            @PathVariable("jour") String jour) {
...
    }

    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
    public Reponse getClientById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
    public Reponse getMedecinById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
    public Reponse getRvById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
    public Reponse getCreneauById(@PathVariable("id") long id) {
...
    }

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
...
    }

    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
...
    }

    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(
            @PathVariable("idMedecin") long idMedecin,
            @PathVariable("jour") String jour) {
...
    }
}
  • línea 6: la anotación [@RestController] convierte la clase [RdvMedecinsController] en un controlador Spring. Además, también implica que los métodos que procesan los URL generarán una respuesta que se transformará automáticamente en JSON;
  • líneas 9-10: Spring inyectará aquí un objeto de tipo [ApplicationModel];
  • línea 13: la anotación [@PostConstruct] marca un método que se ejecutará justo después de la instanciación de la clase. Cuando este se ejecuta, los objetos inyectados por Spring están disponibles;
  • todos los métodos devuelven un objeto de tipo [Reponse] como se muestra a continuación:

package rdvmedecins.web.models;

public class Reponse {

    // ----------------- propiedades
    // estado de la operación
    private int status;
    // la respuesta
    private Object data;
...
}

Este objeto se serializa en JSON antes de enviarse al navegador del cliente;

  • línea 20: la anotación [@RequestMapping] establece las condiciones de llamada del método. Aquí, el método procesa una solicitud GET del URL [/getAllMedecins]. Si este URL fuera solicitado por un POST, sería rechazado y Spring MVC enviaría un código de error HTTP al cliente web;
  • línea 32: el URL se configura mediante {idMedecin}. Este parámetro se recupera con la anotación [@PathVariable] de la línea 33;
  • línea 33: el único parámetro [long idMedecin] recibe su valor del parámetro {idMedecin} de URL [@PathVariable("idMedecin")]. El parámetro en URL y el del método pueden tener nombres diferentes. Cabe señalar aquí que [@PathVariable("idMedecin")] es de tipo String (todo el URL es un String), mientras que el parámetro [long idMedecin] es de tipo [long]. El cambio de tipo se realiza automáticamente. Se devuelve un código de error HTTP si este cambio de tipo falla;
  • línea 65: la anotación [@RequestBody] designa el cuerpo de la consulta. En una solicitud GET, casi nunca hay cuerpo (pero es posible incluir uno). En una solicitud POST, suele haberlo (pero es posible no incluirlo). Para URL [ajouterRv], el cliente web envía en su POST la siguiente cadena JSON:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

La sintaxis [@RequestBody PostAjouterRv post] (línea 65) , sumada al hecho de que el método espera el JSON [consumes = "application/json; charset=UTF-8"] de la línea 64, hará que la cadena JSON enviada por el cliente web se deserialice en un objeto de tipo [PostAjouter]. Este es el siguiente:


package rdvmedecins.web.models;

public class PostAjouterRv {

    // datos de post
    private String jour;
    private long idClient;
    private long idCreneau;

    // getters y setters
    ...
}

Aquí también se producirán automáticamente los cambios de tipo necesarios;

  • en las líneas 69-70, encontramos un mecanismo similar para URL [/supprimerRv]. La cadena JSON enviada es la siguiente:
{"idRv":116}

y el tipo [PostSupprimerRv] es el siguiente:


package rdvmedecins.web.models;

public class PostSupprimerRv {

    // datos de post
    private long idRv;

    // getters y setters
    ...
}

2.12.4. Las plantillas del servicio web

  

Ya hemos presentado los modelos [Reponse, PostAjouterRv, PostSupprimerRv]. El modelo [ApplicationModel] es el siguiente:


package rdvmedecins.web.models;

import java.util.Date;
...

@Component
public class ApplicationModel implements IMetier {

    // la capa [métier]
    @Autowired
    private IMetier métier;

    // datos procedentes de la capa [métier]
    private List<Medecin> médecins;
    private List<Client> clients;
    // mensajes de error
   private List<String> messages;

    @PostConstruct
    public void init() {
        // se recuperan los médicos y los clients
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }

    // getter
    public List<String> getMessages() {
        return messages;
    }

    // ------------------------- interfaz de capa [métier]
    @Override
    public List<Client> getAllClients() {
        return clients;
    }

    @Override
    public List<Medecin> getAllMedecins() {
        return médecins;
    }

    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return métier.getAllCreneaux(idMedecin);
    }

    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return métier.getRvMedecinJour(idMedecin, jour);
    }

    @Override
    public Client getClientById(long id) {
        return métier.getClientById(id);
    }

    @Override
    public Medecin getMedecinById(long id) {
        return métier.getMedecinById(id);
    }

    @Override
    public Rv getRvById(long id) {
        return métier.getRvById(id);
    }

    @Override
    public Creneau getCreneauById(long id) {
        return métier.getCreneauById(id);
    }

    @Override
    public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
        return métier.ajouterRv(jour, creneau, client);
    }

    @Override
    public void supprimerRv(Rv rv) {
        métier.supprimerRv(rv);
    }

    @Override
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        return métier.getAgendaMedecinJour(idMedecin, jour);
    }

}
  • línea 6: la anotación [@Component] convierte la clase [ApplicationModel] en un componente Spring. Al igual que todos los componentes Spring vistos hasta ahora (a excepción de @Controller), solo se instanciará un único objeto de este tipo (singleton);
  • línea 7: la clase [ApplicationModel] implementa la interfaz [IMetier];
  • líneas 10-11: Spring inyecta una referencia en la capa [métier];
  • línea 19: la anotación [@PostConstruct] hace que el método [init] se ejecute justo después de la instanciación de la clase [ApplicationModel];
  • líneas 23-24: se recuperan las listas de médicos y de clients de la capa [métier];
  • línea 26: si se produce una excepción, se almacenan los mensajes de la pila de excepciones en el campo de la línea 17;

La clase [ApplicationModel] nos servirá para dos cosas:

  • como caché para almacenar las listas de médicos y pacientes (clients);
  • como interfaz única para los controladores;

La arquitectura de la capa web evoluciona de la siguiente manera:

  • en [2b], los métodos del controlador o controladores se comunican con el singleton [ApplicationModel];

Esta estrategia aporta flexibilidad en la gestión de la caché. Actualmente, los horarios de los médicos no se almacenan en caché. Para hacerlo, basta con modificar la clase [ApplicationModel]. Esto no tiene ningún impacto en el controlador, que seguirá utilizando el método [List<Creneau> getAllCreneaux(long idMedecin)] como lo hacía anteriormente. Lo que se modificará es la implementación de este método en [ApplicationModel].

2.12.5. La clase Static

La clase [Static] agrupa un conjunto de métodos estáticos de utilidad que no tienen un carácter «de negocio» ni «web»:

  

Su código es el siguiente:


package rdvmedecins.web.helpers;

import java.text.SimpleDateFormat;
...

public class Static {

    public Static() {
    }

    // lista de mensajes de error de una excepción
    public static List<String> getErreursForException(Exception exception) {
        // se recupera la lista de mensajes de error de la excepción
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }

    // mapeadores Object --> Map
    // --------------------------------------------------------
....
}
  • línea 12: el método [Static.getErreursForException] que se ha utilizado (línea 8 más abajo) en el método [init] de la clase [ApplicationModel]:

    @PostConstruct
    public void init() {
        // se recuperan los médicos y los clients
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
}

El método crea un objeto [List<String>] con los mensajes de error [exception.getMessage()] de una excepción [exception] y de las que esta contiene [exception.getCause()].

La clase [Static] contiene otros métodos de utilidad sobre los que volveremos cuando los encontremos.

A continuación, detallaremos el procesamiento de URL del servicio web. En este procesamiento intervienen tres clases principales:

  • el controlador [RdvMedecinsController];
  • la clase de métodos de utilidad [Static];
  • la clase de caché [ApplicationModel];
  

2.12.6. El método [init] del controlador

El controlador [RdvMedecinsController] (véase el apartado 2.12.3) tiene un método [init] que se ejecuta justo después de su instanciación:


    @Autowired
    private ApplicationModel application;
    private List<String> messages;

    @PostConstruct
    public void init() {
        // mensajes de error de la aplicación
        messages = application.getMessages();
}
  • línea 8: los mensajes de error almacenados en la aplicación caché [ApplicationModel] se guardan localmente en el campo de la línea 3. Esto permitirá a los métodos saber si la aplicación se ha inicializado correctamente.

2.12.7. La URL [/getAllMedecins]

El URL [/getAllMedecins] es procesado por el siguiente método del controlador [RdvMedecinsController]:


    // lista de médicos
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
        // estado de la aplicación
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // lista de médicos
        try {
            return new Reponse(0, application.getAllMedecins());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}
  • línea 5: se comprueba si la aplicación se ha inicializado correctamente (mensajes==null). Si no es así, se devuelve una respuesta con status=-1 y data=mensajes;
  • línea 10: en caso contrario, se devuelve la lista de médicos con un status igual a 0. El método [application.getAllMedecins()] no lanza una excepción, ya que se limita a devolver una lista que se encuentra en la caché. No obstante, mantendremos esta gestión de excepciones por si los médicos ya no se almacenaran en la caché;

Aún no hemos ilustrado el caso en el que la aplicación se haya inicializado incorrectamente. Detengamos SGBD MySQL5, iniciemos el servicio web y luego solicitemos URL [/getAllMedecins]:

Image

Efectivamente, se produce un error. En un contexto normal, se obtiene la siguiente vista:

2.12.8. El URL [/getAllClients]

El URL [/getAllClients] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


    // lista de clients
    @RequestMapping(value = "/getAllClients")
    public Reponse getAllClients() {
        // estado de la solicitud
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // lista de clients
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}

Es análogo al método [getAllMedecins] ya estudiado. Los resultados obtenidos son los siguientes:

2.12.9. El URL [/getAllCreneaux/{idMedecin}]

El URL [/getAllCreneaux/{idMedecin}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


// lista de franjas horarias de un médico
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
        // estado de la solicitud
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // se recupera el médico
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // horarios del médico
        List<Creneau> créneaux = null;
        try {
            créneaux = application.getAllCreneaux(médecin.getId());
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // se devuelve la respuesta
        return new Reponse(0, Static.getListMapForCreneaux(créneaux));
    }
  • línea 9: se solicita al médico identificado por el parámetro [id] un método local:

    private Reponse getMedecin(long id) {
        // se recupera el médico
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // ¿Médico existente?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, médecin);
}

Se vuelve de este método con un status en [0,1,2]. Volvamos al código del método [getAllCreneaux]:

  • líneas 10-12: si status!=0, se devuelve la respuesta inmediatamente;
  • línea 13: se recupera el médico;
  • línea 17: se recuperan los horarios de este médico;
  • línea 22: se envía como respuesta un objeto [Static.getListMapForCreneaux(créneaux)];

Recordemos la definición de la clase [Creneau]:


@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {

    private static final long serialVersionUID = 1L;
    // características de una franja horaria de RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // un horario está vinculado a un médico
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // clave externa
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • línea 13: se busca al médico en modo [FetchType.LAZY];

Recordemos la consulta JPQL que implementa el método [getAllCreneaux] en la capa [DAO]:


@Query("select c from Creneau c where c.medecin.id=?1")

La notación [c.medecin.id] fuerza la unión entre las tablas [CRENEAUX] y [MEDECINS]. Por lo tanto, la consulta devuelve todas las franjas horarias del médico con el médico en cada una de ellas. Al serializar estas franjas horarias en JSON, aparece la cadena JSON del médico en cada una de ellas. Esto es innecesario. Por lo tanto, en lugar de serializar un objeto [Creneau], serializaremos un objeto [Map] en el que solo incluiremos los campos deseados.

Volvamos al código estudiado inicialmente:


// se devuelve la respuesta
return new Reponse(0, Static.getListMapForCreneaux(créneaux));

El método [Static.getListMapForCreneaux] es el siguiente:


    // List<Creneau> --> List<Map>
    public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
        // lista de diccionarios <String,Object>
        List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
        for (Creneau créneau : créneaux) {
            liste.add(Static.getMapForCreneau(créneau));
        }
        // se devuelve la lista
        return liste;
}

y el método [Static.getMapForCreneau] es el siguiente:


    // Creneau --> Map
    public static Map<String, Object> getMapForCreneau(Creneau créneau) {
        // ¿Hay algo que hacer?
        if (créneau == null) {
            return null;
        }
        // diccionario <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("id", créneau.getId());
        hash.put("hDebut", créneau.getHdebut());
        hash.put("mDebut", créneau.getMdebut());
        hash.put("hFin", créneau.getHfin());
        hash.put("mFin", créneau.getMfin());
        // se devuelve el diccionario
        return hash;
}
  • línea 8: se crea un diccionario;
  • líneas 9-13: se añaden los campos que se desean conservar en la cadena JSON. El campo [medecin] no aparece;
  • línea 15: se devuelve este diccionario;

Los resultados obtenidos son los siguientes:

o bien estos si el intervalo no existe:

o estos en caso de error al acceder a la base de datos:

2.12.10. El URL [/getRvMedecinJour/{idMedecin}/{jour}]

URL [/getRvMedecinJour/{idMedecin}/{jour}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


// lista de citas de un médico
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // estado de la aplicación
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // se comprueba la fecha
        Date jourAgenda = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false);
        try {
            jourAgenda = sdf.parse(jour);
        } catch (ParseException e) {
            return new Reponse(3, null);
        }
        // se recupera el médico
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // lista de sus citas
        List<Rv> rvs = null;
        try {
            rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
        } catch (Exception e1) {
            return new Reponse(4, Static.getErreursForException(e1));
        }
        // se devuelve la respuesta
        return new Reponse(0, Static.getListMapForRvs(rvs));
}
  • línea 31: se devuelve un objeto List<Map<String,Object>> en lugar de un objeto List<Rv>. Recordemos la definición de la clase [Rv]:

@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // características de un Rv
    @Temporal(TemporalType.DATE)
    private Date jour;

    // un rv está vinculado a un cliente
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // un rv está vinculado a una franja horaria
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

    // claves externas
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;

...

}
  • línea 11: se busca el cliente con el modo [FetchType.LAZY];
  • línea 18: se busca la franja horaria con el modo [FetchType.LAZY];

Recordemos la consulta JPQL que busca las citas:


@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")

Se realizan uniones explícitas para recuperar los campos [client] y [creneau]. Por otra parte, debido a la unión [cr.medecin.id=?1], también tendremos el médico. Por lo tanto, el médico aparecerá en la cadena JSON de cada cita. Sin embargo, esta información duplicada es, además, innecesaria. Volvamos al código del método:

  • línea 31: construimos nosotros mismos el diccionario que se va a serializar en JSON;

El diccionario creado para una cita es el siguiente:


    // Rv --> Mapa
    public static Map<String, Object> getMapForRv(Rv rv) {
        // ¿hay algo que hacer?
        if (rv == null) {
            return null;
        }
        // diccionario <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("id", rv.getId());
        hash.put("client", rv.getClient());
        hash.put("creneau", getMapForCreneau(rv.getCreneau()));
        // se devuelve el diccionario
        return hash;
}
  • línea 11: retomamos el diccionario del objeto [Creneau] que hemos presentado anteriormente;

Los resultados obtenidos son los siguientes:

o también estos con un día incorrecto:

o estos otros con un médico incorrecto:

2.12.11. El URL [/getAgendaMedecinJour/{idMedecin}/{jour}]

El URL [/getAgendaMedecinJour/{idMedecin}/{jour}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // estado de la aplicación
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // se comprueba la fecha
        Date jourAgenda = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false);
        try {
            jourAgenda = sdf.parse(jour);
        } catch (ParseException e) {
            return new Reponse(3, new String[] { String.format("jour [%s] invalide", jour) });
        }
        // se recupera el médico
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // se recupera su agenda
        AgendaMedecinJour agenda = null;
        try {
            agenda = application.getAgendaMedecinJour(médecin.getId(), jourAgenda);
        } catch (Exception e1) {
            return new Reponse(4, Static.getErreursForException(e1));
        }
        // ok
        return new Reponse(0, Static.getMapForAgendaMedecinJour(agenda));
    }
}
  • línea 30, se devuelve un objeto de tipo List<Map<String,Object>.

El método [Static.getMapForAgendaMedecinJour] es el siguiente:


    // AgendaMedecinJour --> Mapa
    public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
        // ¿Hay que hacer algo?
        if (agenda == null) {
            return null;
        }
        // diccionario <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("medecin", agenda.getMedecin());
        hash.put("jour", new SimpleDateFormat("yyyy-MM-dd").format(agenda.getJour()));
        List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
        for (CreneauMedecinJour créneau : agenda.getCreneauxMedecinJour()) {
            créneaux.add(getMapForCreneauMedecinJour(créneau));
        }
        hash.put("creneauxMedecin", créneaux);
        // se devuelve el diccionario
        return hash;
}

El diccionario construido tiene tres campos:

  • [medecin]: el médico propietario del agenda. Se ha conservado esta información porque solo aparece una vez, mientras que en los casos anteriores se repetía en cada cadena JSON;
  • [jour]: el día del agenda;
  • [creneauxMedecin]: la lista de franjas horarias del médico con una posible cita en esa franja;

El método [getMapForCreneauMedecinJour] utilizado en la línea 13 es el siguiente:


    // CreneauMedecinJour --> mapa
    public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
        // ¿Hay que hacer algo?
        if (créneau == null) {
            return null;
        }
        // diccionario <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
        hash.put("rv", getMapForRv(créneau.getRv()));
        // se devuelve el diccionario
        return hash;
}
  • líneas 9-10: se utilizan los diccionarios ya estudiados para los tipos [Creneau] y [Rv], que por lo tanto no incluyen el objeto [Medecin];

Los resultados obtenidos son los siguientes:

o bien estos si el día es incorrecto:

o estos si el número del médico no es válido:

2.12.12. El URL [/getMedecinById/{id}]

El URL [/getMedecinById/{id}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
    public Reponse getMedecinById(@PathVariable("id") long id) {
        // estado de la aplicación
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // se busca al médico
        return getMedecin(id);
}

En la línea 8, el método [getMedecin] es el siguiente:


    private Reponse getMedecin(long id) {
        // se recupera el médico
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // ¿Existe el médico?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, médecin);
}

Los resultados obtenidos son los siguientes:

o bien estos si el número del médico es incorrecto:

2.12.13. El URL [/getClientById/{id}]

El URL [/getClientById/{id}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
    public Reponse getClientById(@PathVariable("id") long id) {
        // estado de la solicitud
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // se recupera el cliente
        return getClient(id);
}

Línea 8, el método [getClient] es el siguiente:


    private Reponse getClient(long id) {
        // se recupera el cliente
        Client client = null;
        try {
            client = application.getClientById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // ¿Cliente existente?
        if (client == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, client);
}

Los resultados obtenidos son los siguientes:

o bien estos si el número de cliente es incorrecto:

2.12.14. El URL [/getCreneauById/{id}]

El URL [/getCreneauById/{id}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
    public Reponse getCreneauById(@PathVariable("id") long id) {
        // estado de la aplicación
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // se recupera la franja horaria
        Reponse réponse = getCreneau(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
        }
        // resultado
        return réponse;
}

Línea 8, el método [getCreneau] es el siguiente:


    private Reponse getCreneau(long id) {
        // se recupera la franja horaria
        Creneau créneau = null;
        try {
            créneau = application.getCreneauById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // ¿franja horaria existente?
        if (créneau == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, créneau);
}

Los resultados obtenidos son los siguientes:

o estos si el número de franja horaria es incorrecto:

2.12.15. El URL [/getRvById/{id}]

El URL [/getRvById/{id}] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
    public Reponse getRvById(@PathVariable("id") long id) {
        // estado de la aplicación
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // se recupera el rv
        Reponse réponse = getRv(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
        }
        // resultado
        return réponse;
}

Línea 8, el método [getRv] es el siguiente:


    private Reponse getRv(long id) {
        // se recupera el Rv
        Rv rv = null;
        try {
            rv = application.getRvById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // ¿Existe Rv?
        if (rv == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, rv);
}

Línea 10, el método [Static.getMapForRv2] es el siguiente:


// Rv --> Mapa
    public static Map<String, Object> getMapForRv2(Rv rv) {
        // ¿Hay que hacer algo?
        if (rv == null) {
            return null;
        }
        // diccionario <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("id", rv.getId());
        hash.put("idClient", rv.getIdClient());
        hash.put("idCreneau", rv.getIdCreneau());
        // se devuelve el diccionario
        return hash;
    }

Los resultados obtenidos son los siguientes:

o bien estos si el número de cita es incorrecto:

2.12.16. El URL [/ajouterRv]

El URL [/ajouterRv] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
        // estado de la aplicación
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // se recuperan los valores enviados
        String jour = post.getJour();
        long idCreneau = post.getIdCreneau();
        long idClient = post.getIdClient();
        // se comprueba la fecha
        Date jourAgenda = null;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false);
        try {
            jourAgenda = sdf.parse(jour);
        } catch (ParseException e) {
            return new Reponse(6, null);
        }
        // se recupera la franja horaria
        Reponse réponse = getCreneau(idCreneau);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Creneau créneau = (Creneau) réponse.getData();
        // se recupera el cliente
        réponse = getClient(idClient);
        if (réponse.getStatus() != 0) {
            réponse.incrStatusBy(2);
            return réponse;
        }
        Client client = (Client) réponse.getData();
        // se añade el Rv
        Rv rv = null;
        try {
            rv = application.ajouterRv(jourAgenda, créneau, client);
        } catch (Exception e1) {
            return new Reponse(5, Static.getErreursForException(e1));
        }
        // se devuelve la respuesta
        return new Reponse(0, Static.getMapForRv(rv));
    }

No hay nada aquí que no se haya visto ya. En la línea 41, se devuelve la cita que se ha añadido en la línea 36.

Los resultados obtenidos se ven así con el cliente [Advanced Rest Client]:

o bien a esto si, por ejemplo, se introduce un número de franja horaria inexistente:

2.12.17. El URL [/supprimerRv]

El URL [/supprimerRv] se procesa mediante el siguiente método del controlador [RdvMedecinsController]:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
        // estado de la aplicación
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // se recuperan los valores enviados
        long idRv = post.getIdRv();
        // se recupera el rv
        Reponse réponse = getRv(idRv);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        // eliminación del rv
        try {
            application.supprimerRv(idRv);
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // ok
        return new Reponse(0, null);
    }

Los resultados obtenidos por son los siguientes:

o bien estos si el n.º de la cita no existe:

Ya hemos terminado con el controlador. Ahora veremos cómo configurar el proyecto.

2.12.18. Configuración del servicio web

  

La clase de configuración [AppConfig] es la siguiente:


package rdvmedecins.web.config;

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

import rdvmedecins.config.DomainAndPersistenceConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {

}
  • línea 9: se activa el modo [AutoConfiguration] para que Spring Boot pueda configurar el proyecto en función de los archivos que encuentre en la ruta de clases del proyecto;
  • línea 10: se solicita que los componentes de Spring se busquen en el paquete [rdvmedecins.web] y sus descendientes. Así es como se encontrarán los componentes:
    • [@RestController RdvMedecinsController] en el paquete [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] en el paquete [rdvmedecins.web.models];
  • línea 11: se importa la clase [DomainAndPersistenceConfig] que configura el proyecto [rdvmedecins-metier-dao] para tener acceso a los beans de este proyecto;

2.12.19. La clase ejecutable del servicio web

  

La clase [Boot] es la siguiente:


package rdvmedecins.web.boot;

import org.springframework.boot.SpringApplication;

import rdvmedecins.web.config.AppConfig;

public class Boot {

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

En la línea 10, se ejecuta el método estático [SpringApplication.run] con la clase de configuración del proyecto [AppConfig] como primer parámetro. Este método llevará a cabo la autoconfiguración del proyecto, iniciará el servidor Tomcat integrado en las dependencias y desplegará en él el controlador [RdvMedecinsController].

Los registros de ejecución son los siguientes:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.0.0.RELEASE)

2014-06-12 17:30:41.261  INFO 9388 --- [           main] rdvmedecins.web.boot.Boot                : Starting Boot on Gportpers3 with PID 9388 (D:\data\istia-1314\polys\istia\angularjs-spring4\dvp\rdvmedecins-webapi\target\classes started by ST)
2014-06-12 17:30:41.306  INFO 9388 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@a1e932e: startup date [Thu Jun 12 17:30:41 CEST 2014]; root of context hierarchy
2014-06-12 17:30:42.058  INFO 9388 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'org.springframework.boot.autoconfigure.AutoConfigurationPackages': replacing [Generic bean: class [org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null] with [Generic bean: class [org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null]
2014-06-12 17:30:42.866  INFO 9388 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$fd7a7b18] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2014-06-12 17:30:42.900  INFO 9388 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2014-06-12 17:30:42.915  INFO 9388 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2014-06-12 17:30:42.920  INFO 9388 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2014-06-12 17:30:43.164  INFO 9388 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-06-12 17:30:43.403  INFO 9388 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-06-12 17:30:43.403  INFO 9388 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.52
2014-06-12 17:30:43.582  INFO 9388 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-06-12 17:30:43.582  INFO 9388 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2279 ms
2014-06-12 17:30:44.117  INFO 9388 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-06-12 17:30:44.119  INFO 9388 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-06-12 17:30:44.662  INFO 9388 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2014-06-12 17:30:44.707  INFO 9388 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2014-06-12 17:30:44.839  INFO 9388 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.1.Final}
2014-06-12 17:30:44.842  INFO 9388 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2014-06-12 17:30:44.844  INFO 9388 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2014-06-12 17:30:45.189  INFO 9388 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
2014-06-12 17:30:45.616  INFO 9388 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2014-06-12 17:30:45.783  INFO 9388 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2014-06-12 17:30:46.729  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:46.825  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String)
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long)
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String)
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllMedecins],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins()
2014-06-12 17:30:46.826  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getMedecinById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCreneauById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getClientById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getRvById/{id}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long)
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllClients],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.getAllClients()
2014-06-12 17:30:46.827  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/ajouterRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv)
2014-06-12 17:30:46.828  INFO 9388 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/supprimerRv],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public rdvmedecins.web.models.Reponse rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv)
2014-06-12 17:30:46.851  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] al controlador de tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:46.851  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler de tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:47.131  INFO 9388 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-06-12 17:30:47.169  INFO 9388 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-06-12 17:30:47.170  INFO 9388 --- [           main] rdvmedecins.web.boot.Boot                : Started Boot in 6.302 seconds (JVM running for 6.906)
2014-06-12 17:30:55.520  INFO 9388 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-06-12 17:30:55.520  INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2014-06-12 17:30:55.538  INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 18 ms
  • línea 17: se inicia el servidor Tomcat;
  • líneas 23-31: se inicializan las capas [métier, DAO, JPA];
  • línea 34: se ha detectado el método que procesa URL [/getRvMedecinJour/{idMedecin}/{jour}]. Este proceso de detección de los métodos del controlador se repite hasta la línea 44;
  • línea 52: el servlet de Spring MVC [DispatcherServlet] está listo para responder a las solicitudes web de clients;

Ahora disponemos de un servicio web operativo al que se puede acceder mediante un cliente web. A continuación, abordaremos la seguridad de este servicio: queremos que solo determinadas personas puedan gestionar las citas de los médicos. Para ello, utilizaremos el marco Spring Security, una rama del ecosistema Spring.

2.13. Introducción a Spring Security

Vamos a importar de nuevo una guía de Spring siguiendo los pasos 1 a 3 que se indican a continuación:

  

El proyecto se compone de los siguientes elementos:

  • en la carpeta [templates], se encuentran las páginas HTML del proyecto;
  • [Application]: es la clase ejecutable del proyecto;
  • [MvcConfig]: es la clase de configuración de Spring MVC;
  • [WebSecurityConfig]: es la clase de configuración de Spring Security;

2.13.1. Configuración de Maven

El proyecto [3] es un proyecto Maven. Examinemos su archivo [pom.xml] para conocer sus dependencias:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.1.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
</dependencies>
  • líneas 1-5: el proyecto es un proyecto Spring Boot;
  • líneas 8-11: dependencia del framework [Thymeleaf], que permite crear páginas HTML dinámicas. Este framework puede sustituir a las páginas JSP (Java Server Pages) que, hasta hace poco, eran el framework de vistas predeterminado de Spring MVC;
  • líneas 12-15: dependencia del marco Spring Security;

2.13.2. Las vistas Thymeleaf

  

La vista [home.html] es la siguiente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>

    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • Los atributos [th:xx] son atributos de Thymeleaf. Thymeleaf los interpreta antes de que la página HTML se envíe al cliente. Este no los ve;
  • línea 12: el atributo [th:href="@{/hello}"] generará el atributo [href] de la etiqueta <a>. El valor [@{/hello}] generará la ruta [<context>/hello], donde [context] es el contexto de la aplicación web;

El código HTML generado es el siguiente:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>
    <p>
        Click <a href="/hello">here</a> to see a greeting.
    </p>
</body>
</html>
  • línea 10: el contexto de la aplicación es la raíz /;

La vista [hello.html] es la siguiente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • línea 9: El atributo [th:inline="text"] generará el texto de la etiqueta <h1>. Este texto contiene una expresión $ que debe evaluarse. El elemento [[${#httpServletRequest.remoteUser}]] es el valor del atributo [RemoteUser] de la consulta HTTP actual. Es el nombre del usuario conectado;
  • línea 10: un formulario HTML. El atributo [th:action="@{/logout}"] generará el atributo [action] de la etiqueta [form]. El valor [@{/logout}] generará la ruta [<context>/logout], donde [context] es el contexto de la aplicación web;

El código HTML generado es el siguiente:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1>Hello user!</h1>
    <form method="post" action="/logout">
        <input type="submit" value="Sign Out" />
    <input type="hidden" name="_csrf" value="c60cf557-1f3b-415f-a628-39380de7b69a" /></form>
</body>
</html>
  • línea 8: la traducción de Hello [[${#httpServletRequest.remoteUser}]]!;
  • línea 9: la traducción de @{/logout};
  • línea 11: un campo oculto llamado (atributo name) _csrf;

La última vista [login.html] es la siguiente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • línea 9: el atributo [th:if="${param.error}"] hace que la etiqueta <div> solo se genere si el URL que muestra la página de inicio de sesión contiene el parámetro [error] (http://context/login?error);
  • línea 10: el atributo [th:if="${param.logout}"] hace que la etiqueta <div> solo se genere si el URL que muestra la página de inicio de sesión contiene el parámetro [logout] (http://context/login?logout);
  • líneas 11-23: un formulario HTML;
  • línea 11: el formulario se enviará a URL [<context>/login], donde <context> es el contexto de la aplicación web;
  • línea 13: un campo de entrada denominado [username];
  • línea 17: un campo de entrada denominado [password];

El código HTML generado es el siguiente:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>

    <form method="post" action="/login">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    <input type="hidden" name="_csrf" value="c60cf557-1f3b-415f-a628-39380de7b69a" /></form>
</body>
</html>

Cabe destacar que, en la línea 21, Thymeleaf ha añadido un campo oculto denominado [_csrf].

2.13.3. Configuración de Spring MVC

  

La clase [MvcConfig] configura el framework Spring MVC:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}
  • línea 7: la anotación [@Configuration] convierte la clase [MvcConfig] en una clase de configuración;
  • línea 8: la clase [MvcConfig] extiende la clase [WebMvcConfigurerAdapter] para redefinir algunos de sus métodos;
  • línea 10: redefinición de un método de la clase padre;
  • líneas 11-16: el método [addViewControllers] permite asociar URL a vistas HTML. Se realizan las siguientes asociaciones:
URL
vista
/, /home
/templates/home.html
/hello
/plantillas/hola.html
/login
/templates/login.html

El sufijo [html] y la carpeta [templates] son los valores predeterminados que utiliza Thymeleaf. Se pueden cambiar mediante la configuración. La carpeta [templates] debe estar en la raíz de la ruta de clases del proyecto:

Por encima de [1], las carpetas [main] y [resources] son ambas carpetas de origen (source folders). Esto implica que su contenido estará en la raíz del Classpath del proyecto. Por lo tanto, en [2], las carpetas [hello] y [templates] estarán en la raíz del Classpath.

2.13.4. Configuración de Spring Security

  

La clase [WebSecurityConfig] configura el marco Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • línea 9: la anotación [@Configuration] convierte la clase [WebSecurityConfig] en una clase de configuración;
  • línea 10: la anotación [@EnableWebSecurity] convierte la clase [WebSecurityConfig] en una clase de configuración de Spring Security;
  • línea 11: la clase [WebSecurity] extiende la clase [WebSecurityConfigurerAdapter] para redefinir algunos de sus métodos;
  • línea 12: redefinición de un método de la clase padre;
  • líneas 13-16: el método [configure(HttpSecurity http)] se redefine para establecer los derechos de acceso a los distintos URL de la aplicación;
  • línea 14: el método [http.authorizeRequests()] permite asociar URL a derechos de acceso. Se realizan las siguientes asociaciones:
URL
regla
código
/, /home
acceso sin autenticación

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Acceso solo con autenticación
http.anyRequest().authenticated();
  • línea 15: define el método de autenticación. La autenticación se realiza a través de un formulario de URL [/login] accesible para todos [http.formLogin().loginPage("/login").permitAll()]. El cierre de sesión (logout) también es accesible para todos.
  • líneas 19-21: redefinen el método [configure(AuthenticationManagerBuilder auth)] que gestiona a los usuarios;
  • línea 20: la autenticación se realiza con usuarios definidos de forma «fija» [auth.inMemoryAuthentication()]. Aquí se define un usuario con el nombre de usuario [user], la contraseña [password] y el rol [USER]. Se pueden conceder los mismos derechos a los usuarios que tengan el mismo rol;

2.13.5. Clase ejecutable

  

La clase [Application] es la siguiente:


package hello;

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

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

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

}
  • línea 8: la anotación [@EnableAutoConfiguration] solicita a Spring Boot (línea 3) que realice la configuración que el desarrollador no habrá hecho explícitamente;
  • línea 9: convierte la clase [Application] en una clase de configuración de Spring;
  • línea 10: solicita el escaneo de la carpeta de la clase [Application] para buscar componentes de Spring. Las dos clases [MvcConfig] y [WebSecurityConfig] serán detectadas de este modo, ya que tienen la anotación [@Configuration];
  • línea 13: el método [main] de la clase ejecutable;
  • línea 14: se ejecuta el método estático [SpringApplication.run] con la clase de configuración [Application] como parámetro. Ya nos hemos encontrado con este proceso y sabemos que se iniciará el servidor Tomcat integrado en las dependencias Maven del proyecto y que el proyecto se desplegará en él. Hemos visto que cuatro URL eran gestionadas por [/, /home, /login, /hello] y que algunas estaban protegidas por derechos de acceso.

2.13.6. Pruebas de la aplicación

Comencemos solicitando el URL [/], que es uno de los cuatro URL aceptados. Está asociado a la vista [/templates/home.html]:

 

La URL solicitada [/] es accesible para todos. Por eso la hemos obtenido. El enlace [here] es el siguiente:

Click <a href="/hello">here</a> to see a greeting.

El URL [/hello] se solicitará al hacer clic en el enlace. Este está protegido:

URL
regla
código
/, /home
acceso sin autenticación

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Acceso solo con autenticación
http.anyRequest().authenticated();

Es necesario estar autenticado para obtenerlo. Spring Security redirigirá entonces el navegador del cliente a la página de autenticación. Según la configuración vista, se trata de la página de URL [/login]. Esta es accesible para todos:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

Así que obtenemos [1]:

El código fuente de la página obtenida es el siguiente:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • En la línea 7 aparece un campo oculto que no está en la página [login.html] original. Lo ha añadido Thymeleaf. Este código, denominado CSRF (Cross Site Request Forgery), tiene como objetivo eliminar una vulnerabilidad de seguridad. Este token debe reenviarse a Spring Security junto con la autenticación para que esta última sea aceptada;

Recordamos que Spring Security solo reconoce al usuario user/password. Si introducimos cualquier otra cosa en [2], obtenemos la misma página con un mensaje de error en [3]. Spring Security ha redirigido el navegador a URL [http://localhost:8080/login?error]. La presencia del parámetro [error] ha activado la visualización de la etiqueta:


<div th:if="${param.error}">Invalid username and password.</div>

Ahora, introduzcamos los valores esperados para usuario/contraseña [4]:

  • en [4], nos identificamos;
  • en [5], Spring Security nos redirige a URL [/hello], ya que es URL lo que solicitábamos cuando fuimos redirigidos a la página de inicio de sesión. La identidad del usuario se mostró en la siguiente línea de [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

La página [5] muestra el siguiente formulario:


    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
</form>

Al hacer clic en el botón [Sign Out], se ejecutará un POST en el URL [/logout]. Este, al igual que el URL y el [/login], es accesible para todos:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

En nuestra asociación URL / vistas, no hemos definido nada para URL [/logout]. ¿Qué va a pasar? Probemos:

  • en [6], hacemos clic en el botón [Sign Out];
  • en [7], vemos que hemos sido redirigidos a URL [http://localhost:8080/login?logout]. Ha sido Spring Security quien ha solicitado esta redirección. La presencia del parámetro [logout] en URL ha hecho que se muestre la siguiente línea de la vista:

<div th:if="${param.logout}">You have been logged out.</div>

2.13.7. Conclusión

En el ejemplo anterior, podríamos haber escrito primero la aplicación web y luego haberla protegido. Spring Security no es intrusivo. Se puede implementar la seguridad de una aplicación web ya escrita. Además, hemos descubierto lo siguiente:

  • es posible definir una página de autenticación;
  • la autenticación debe ir acompañada del token CSRF emitido por Spring Security;
  • si la autenticación falla, se redirige a la página de autenticación con un parámetro adicional error en el URL;
  • si la autenticación se realiza correctamente, se redirige a la página solicitada en el momento de la autenticación. Si se solicita directamente la página de autenticación sin pasar por una página intermedia, Spring Security nos redirige a la URL [/] (este caso no se ha presentado);
  • nos desconectamos solicitando la URL [/logout] con una POST. Spring Security nos redirige entonces a la página de autenticación con el parámetro logout en el URL;

Todas estas conclusiones se basan en los comportamientos predeterminados de Spring Security. Estos comportamientos se pueden modificar mediante la configuración, redefiniendo ciertos métodos de la clase [WebSecurityConfigurerAdapter].

El tutorial anterior nos será de poca ayuda en lo que viene a continuación. De hecho, vamos a utilizar:

  • una base de datos para almacenar los usuarios, sus contraseñas y sus roles;
  • una autenticación por encabezado HTTP;

Hay muy pocos tutoriales sobre lo que queremos hacer aquí. La solución que se va a proponer es una recopilación de códigos encontrados aquí y allá.

2.14. Configuración de la seguridad en el servicio web de citas

2.14.1. La base de datos

La base de datos [rdvmedecins] evoluciona para tener en cuenta a los usuarios, sus contraseñas y sus roles. Aparecen tres nuevas tablas:

Image

Tabla [USERS]: los usuarios

  • ID: clave primaria;
  • VERSION: columna de control de versiones de la línea;
  • IDENTITY: una identidad descriptiva del usuario;
  • LOGIN: el nombre de usuario;
  • PASSWORD: su contraseña;

En la tabla USERS, las contraseñas no se almacenan en texto claro:

 

El algoritmo que cifra las contraseñas es el algoritmo BCRYPT.

Tabla [ROLES]: los roles

  • ID: clave primaria;
  • VERSION: columna de control de versiones de la fila;
  • NAME: nombre del rol. Por defecto, Spring Security espera nombres con el formato ROLE_XX, por ejemplo, ROLE_ADMIN o ROLE_GUEST;
 

Tabla [USERS_ROLES]: tabla de unión USERS / ROLES

Un usuario puede tener varios roles, y un rol puede agrupar a varios usuarios. Se trata de una relación muchos a muchos representada por la tabla [USERS_ROLES].

  • ID: clave primaria;
  • VERSION: columna de control de versiones de la fila;
  • USER_ID: identificador de un usuario;
  • ROLE_ID: identificador de un rol;
 

Dado que modificamos la base de datos, deben modificarse todas las capas del proyecto [métier, DAO, JPA]:

2.14.2. El nuevo proyecto Eclipse de [métier, DAO, JPA]

Duplicamos el proyecto inicial [rdvmedecins-metier-dao] en [rdvmedecins-metier-dao-v2]:

  • en [1]: el nuevo proyecto;
  • en [2]: las modificaciones introducidas para tener en cuenta la seguridad se han reunido en un único paquete [rdvmedecins.security]. Estos nuevos elementos pertenecen a las capas [JPA] y [DAO], pero por simplicidad los he reunido en un mismo paquete.

2.14.3. Las nuevas entidades [JPA]

La capa JPA define tres nuevas entidades:

  

La clase [User] es la imagen de la tabla [USERS]:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // propiedades
    private String identity;
    private String login;
    private String password;

    // fabricante
    public User() {
    }

    public User(String identity, String login, String password) {
        this.identity = identity;
        this.login = login;
        this.password = password;
    }

    // identidad
    @Override
    public String toString() {
        return String.format("User[%s,%s,%s]", identity, login, password);
    }

    // getters y setters
....
}
  • línea 9: la clase amplía la clase [AbstractEntity] ya utilizada para las demás entidades;
  • líneas 13-15: no se especifica ningún nombre para las columnas porque tienen el mismo nombre que los campos asociados a ellas;

La clase [Role] es el reflejo de la tabla [ROLES]:


package rdvmedecins.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {

    private static final long serialVersionUID = 1L;

    // propiedades
    private String name;

    // constructores
    public Role() {
    }

    public Role(String name) {
        this.name = name;
    }

    // identidad
    @Override
    public String toString() {
        return String.format("Role[%s]", name);
    }

    // getters y setters
...
}

La clase [UserRole] es la imagen de la tabla [USERS_ROLES]:


package rdvmedecins.entities;

import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {

    private static final long serialVersionUID = 1L;

    // un UserRole hace referencia a un User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // un UserRole hace referencia a un Role
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // getters y setters
...
}
  • líneas 15-17: representan la clave externa de la tabla [USERS_ROLES] hacia la tabla [USERS];
  • líneas 19-21: representan la clave externa de la tabla [USERS_ROLES] hacia la tabla [ROLES];

2.14.4. Modificaciones de la capa [DAO]

La capa [DAO] se amplía con tres nuevos [Repository]:

  

La interfaz [UserRepository] gestiona el acceso a las entidades [User]:


package rdvmedecins.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;

public interface UserRepository extends CrudRepository<User, Long> {

    // lista de roles de un usuario identificado por su id
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // lista de roles de un usuario identificado por su nombre de usuario y contraseña
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // búsqueda de un usuario mediante su nombre de usuario
    User findUserByLogin(String login);
}
  • línea 9: la interfaz [UserRepository] amplía la interfaz [CrudRepository] de Spring Data (línea 4);
  • líneas 12-13: el método [getRoles(User user)] permite obtener todos los roles de un usuario identificado por su [id]
  • líneas 16-17: lo mismo, pero para un usuario identificado por su nombre de usuario y contraseña;

La interfaz [RoleRepository] gestiona los accesos a las entidades [Role]:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // búsqueda de un rol mediante su nombre
    Role findRoleByName(String name);

}
  • línea 5: la interfaz [RoleRepository] amplía la interfaz [CrudRepository];
  • línea 8: se puede buscar un rol por su nombre;

La interfaz [userRoleRepository] gestiona el acceso a las entidades [UserRole]:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • línea 5: la interfaz [UserRoleRepository] se limita a ampliar la interfaz [CrudRepository] sin añadirle nuevos métodos;

2.14.5. Las clases de gestión de usuarios y roles

  

Spring Security exige la creación de una clase que implemente la siguiente interfaz [UsersDetail]:

 

Esta interfaz se implementa aquí mediante la clase [AppUserDetails]:


package rdvmedecins.security;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // propiedades
    private User user;
    private UserRepository userRepository;

    // constructores
    public AppUserDetails() {
    }

    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }

    // -------------------------interfaz
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLogin();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    // getters y setters
    ...
}
  • línea 10: la clase [AppUserDetails] implementa la interfaz [UserDetails];
  • líneas 15-16: la clase encapsula un usuario (línea 15) y el repositorio que permite obtener los detalles de dicho usuario (línea 16);
  • líneas 22-25: el constructor que instancia la clase con un usuario y su repositorio;
  • líneas 28-35: implementación del método [getAuthorities] de la interfaz [UserDetails]. Debe construir una colección de elementos de tipo [GrantedAuthority] o derivado. Aquí utilizamos el tipo derivado [SimpleGrantedAuthority] (línea 32) que encapsula el nombre de uno de los roles del usuario de la línea 15;
  • líneas 31-33: se recorre la lista de roles del usuario de la línea 15 para construir una lista de elementos de tipo [SimpleGrantedAuthority];
  • líneas 38-40: implementan el método [getPassword] de la interfaz [UserDetails]. Se devuelve la contraseña del usuario de la línea 15;
  • líneas 38-40: implementan el método [getUserName] de la interfaz [UserDetails]. Se devuelve el nombre de usuario de la línea 15;
  • líneas 47-50: la cuenta del usuario nunca caduca;
  • líneas 52-55: la cuenta del usuario nunca se bloquea;
  • líneas 57-60: las credenciales del usuario nunca caducan;
  • líneas 62-65: la cuenta del usuario siempre está activa;

Spring Security también exige la existencia de una clase que implemente la interfaz [AppUserDetailsService]:

 

Esta interfaz está implementada por la siguiente clase [AppUserDetails]:


package rdvmedecins.security;

import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // ¿Se busca al usuario por su nombre de usuario?
        User user = userRepository.findUserByLogin(login);
        // ¿Encontrado?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // se muestran los datos del usuario
        return new AppUserDetails(user, userRepository);
    }

}
  • línea 9: la clase será un componente Spring, por lo que estará disponible en su contexto;
  • líneas 12-13: el componente [UserRepository] se inyectará aquí;
  • líneas 16-25: implementación del método [loadUserByUsername] de la interfaz [UserDetailsService] (línea 10). El parámetro es el nombre de usuario;
  • línea 18: se busca al usuario mediante su nombre de usuario;
  • líneas 20-22: si no se encuentra, se lanza una excepción;
  • línea 24: se crea y se representa un objeto [AppUserDetails]. Es del tipo [UserDetails] (línea 16);

2.14.6. Pruebas de la capa [DAO]

  

En primer lugar, creamos una clase ejecutable [CreateUser] capaz de crear un usuario con un rol:


package rdvmedecins.security;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;

import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;

public class CreateUser {

    public static void main(String[] args) {
        // sintaxis: nombre de usuario contraseña roleName

        // se necesitan tres parámetros
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // se recuperan los parámetros
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // contexto Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // ¿existe ya el rol?
        Role role = roleRepository.findRoleByName(roleName);
        // si no existe, se crea
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // ¿existe ya el usuario?
        User user = userRepository.findUserByLogin(login);
        // si no existe, lo creamos
        if (user == null) {
            // se aplica el hash a la contraseña con bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // guardamos el usuario
            user = userRepository.save(new User(login, login, crypt));
            // se crea la relación con el rol
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // el usuario ya existe: ¿tiene el rol solicitado?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // si no se encuentra, se crea la relación con el rol
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // cierre del contexto Spring
        context.close();
    }

}
  • línea 17: la clase espera tres argumentos que definen a un usuario: su nombre de usuario, su contraseña y su rol;
  • líneas 25-27: se recuperan los tres parámetros;
  • línea 29: el contexto Spring se construye a partir de la clase de configuración [DomainAndPersistenceConfig]. Esta clase ya existía en el proyecto anterior. Debe modificarse de la siguiente manera:

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
  • línea 1: hay que indicar que ahora hay componentes [Repository] en el paquete [rdvmedecins.security];
  • línea 4: hay que indicar que ahora hay entidades JPA en el paquete [rdvmedecins.security];

Volvamos al código de creación de un usuario:

  • líneas 30-32: recuperamos las referencias de los tres [Repository] que nos pueden ser útiles para crear el usuario;
  • línea 34: se comprueba si el rol ya existe;
  • líneas 36-38: si no es así, lo creamos en la base de datos. Tendrá un nombre del tipo [ROLE_XX];
  • línea 40: se comprueba si el nombre de usuario ya existe;
  • líneas 42-49: si el nombre de usuario no existe, se crea en la base de datos;
  • línea 44: se cifra la contraseña. Aquí se utiliza la clase [BCrypt] de Spring Security (línea 4). Por lo tanto, se necesitan los archivos de este framework. El archivo [pom.xml] incluye una nueva dependencia:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • línea 46: el usuario se almacena en la base de datos;
  • línea 48: así como la relación que lo vincula a su rol;
  • líneas 51-57: caso en el que el inicio de sesión ya existe; entonces se comprueba si entre sus roles se encuentra ya el rol que se le quiere asignar;
  • líneas 59-61: si no se ha encontrado el rol buscado, se crea una línea en la tabla [USERS_ROLES] para vincular al usuario con su rol;
  • no se ha previsto ninguna protección contra posibles excepciones. Se trata de una clase de apoyo para crear rápidamente un usuario con un rol.

Al ejecutar la clase con los argumentos [x x guest], se obtienen en la base de datos los siguientes resultados:

Tabla [USERS]

 

Tabla [ROLES]

 

Tabla [USERS_ROLES]

 

Consideremos ahora la segunda clase [UsersTest], que es una prueba de JUnit:

  

package rdvmedecins.security;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import rdvmedecins.config.DomainAndPersistenceConfig;

import com.google.common.collect.Lists;

@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    @Test
    public void findAllUsersWithTheirRoles() {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(user);
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }

    @Test
    public void findUserByLogin() {
        // se recupera el usuario [admin]
        User user = userRepository.findUserByLogin("admin");
        // se comprueba que su contraseña es [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // se comprueba el rol de admin / admin
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        // se recupera el usuario [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // se comprueba que su contraseña es [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // se comprueba el rol de admin / admin
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // método de utilidad: muestra los elementos de una colección
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • líneas 27-34: prueba visual. Se muestran todos los usuarios con sus roles;
  • líneas 36-46: se comprueba que el usuario [admin] tiene la contraseña [admin] y el rol [ROLE_ADMIN] utilizando el repositorio [UserRepository];
  • línea 41: [admin] es la contraseña en texto claro. En la base, está cifrada según el algoritmo BCrypt. El método [ BCrypt.checkpw] permite verificar que la contraseña en texto claro, una vez cifrada, es efectivamente igual a la que figura en la base;
  • líneas 48-59: se comprueba que el usuario [admin] tiene la contraseña [admin] y el rol [ROLE_ADMIN] utilizando el servicio [appUserDetailsService];

La ejecución de las pruebas se realiza correctamente con los siguientes registros:

User[guest,guest,$2a$10$Gzyp54mvkgMH0SPQkXo.Zeu.DvJ/Ql50PRXLf2FkolMTs7fr6A2J2]
Roles :
Role[ROLE_GUEST]
User[admin,admin,$2a$10$m79V6MKt9GPDdpjSulyqReqUioqYwXy8ollt/.ia15FhX2fym3AE6]
Roles :
Role[ROLE_ADMIN]
User[user,user,$2a$10$ph5y/1H89YC11oGVLB49fON.dZwnu44bAOKMK1FFl//xjAvsr/Ese]
Roles :
Role[ROLE_USER]
User[x,x,$2a$10$dAKd2SuQplR1iFhoBUUFs.XiA0lYxNqOmrkv97Gbr5KBoHzEi/5HG]
Roles :
Role[ROLE_GUEST]

2.14.7. Conclusión provisional

La incorporación de las clases necesarias para Spring Security se ha podido realizar con pocas modificaciones del proyecto original. Recordemos cuáles son:

  • adición de una dependencia de Spring Security en el archivo [pom.xml];
  • creación de tres tablas adicionales en la base de datos;
  • creación de entidades JPA y de componentes Spring en el paquete [rdvmedecins.security];

Este caso tan favorable se debe a que las tres tablas añadidas a la base de datos son independientes de las tablas existentes. Incluso se podrían haber colocado en una base de datos separada. Esto fue posible porque se decidió que un usuario tenía una existencia independiente de los médicos y de clients. Si estos últimos hubieran sido usuarios potenciales, habría sido necesario crear vínculos entre la tabla [USERS] y las tablas [MEDECINS] y [CLIENTS]. Esto habría tenido entonces un impacto importante en el proyecto existente.

2.14.8. El proyecto Eclipse de la capa [web]

El proyecto [rdvmedecins-webapi] anterior se duplica en el proyecto [rdvmedecins-webapi-v2] [1]:

Las únicas modificaciones deben realizarse en el paquete [rdvmedecins.web.config], donde hay que configurar Spring Security. Ya nos hemos encontrado con una clase de configuración de Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

Seguiremos el mismo procedimiento:

  • línea 11: definir una clase que extienda la clase [WebSecurityConfigurerAdapter];
  • línea 13: definir un método [configure(HttpSecurity http)] que defina los derechos de acceso a los diferentes URL del servicio web;
  • línea 19: definir un método [configure(AuthenticationManagerBuilder auth)] que define los usuarios y sus roles;

La configuración de Spring Security se realiza mediante la clase [SecurityConfig]:


package rdvmedecins.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpMethod;
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 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import rdvmedecins.security.AppUserDetailsService;

@EnableAutoConfiguration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // la autenticación la realiza el bean [appUserDetailsService]
        // la contraseña se cifra mediante el algoritmo de hash Bcrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // la contraseña se transmite mediante el encabezado Authorization: Basic xxxx
        http.httpBasic();
        // solo el rol ADMIN puede utilizar la aplicación
        http.authorizeRequests() //
                .antMatchers("/", "/**") // todos los URL
                .hasRole("ADMIN");
    }
}
  • líneas 14-15: se han retomado las anotaciones del ejemplo;
  • líneas 17-18: se inyecta la clase [AppUserDetails], que da acceso a los usuarios de la aplicación;
  • líneas 20-21: el método [configure(HttpSecurity http)] define a los usuarios y sus roles. Recibe como parámetro un tipo [AuthenticationManagerBuilder]. Este parámetro se enriquece con dos datos:
    • una referencia al servicio [appUserDetailsService] de la línea 18 que da acceso a los usuarios registrados. Cabe señalar aquí que no se indica que estén registrados en una base de datos. Por lo tanto, podrían estar en una caché, proporcionados por un servicio web, etc.
    • el tipo de cifrado utilizado para la contraseña. Recordemos aquí que hemos utilizado el algoritmo BCrypt;
  • líneas 27-40: el método [configure(HttpSecurity http)] define los derechos de acceso a los URL del servicio web;
  • línea 30: en el proyecto de introducción vimos que, por defecto, Spring Security gestionaba un token CSRF (Cross Site Request Forgery) que el usuario que quisiera autenticarse debía enviar al servidor. Aquí este mecanismo está desactivado;
  • línea 32: se activa el modo de autenticación mediante el encabezado HTTP. El cliente deberá enviar el siguiente encabezado HTTP:
Authorization:Basic code

donde «code» es la codificación de la cadena «login:password» mediante el algoritmo Base64. Por ejemplo, la codificación Base64 de la cadena admin:admin es YWRtaW46YWRtaW4=. Por lo tanto, el usuario con nombre de usuario [admin] y contraseña [admin] enviará el siguiente encabezado HTTP para autenticarse:

Authorization:Basic YWRtaW46YWRtaW4=
  • líneas 34-36: indican que todos los URL del servicio web son accesibles para los usuarios que tengan el rol [ROLE_ADMIN]. Esto significa que un usuario que no tenga este rol no puede acceder al servicio web;

La clase [AppConfig], que configura toda la aplicación, evoluciona de la siguiente manera:

  

package rdvmedecins.web.config;

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

import rdvmedecins.config.DomainAndPersistenceConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {

}
  • La modificación se produce en la línea 11: se indica que ahora hay dos archivos de configuración que se deben utilizar: [DomainAndPersistenceConfig] y [SecurityConfig].

2.14.9. Pruebas del servicio web

Vamos a probar el servicio web con el cliente Chrome [Advanced Rest Client]. Tendremos que especificar el encabezado de autenticación HTTP:

Authorization:Basic code

donde [code] es el código Base64 de la cadena [login:password]. Para generar este código, se puede utilizar el siguiente programa:

  

package rdvmedecins.helpers;

import org.springframework.security.crypto.codec.Base64;

public class Base64Encoder {

    public static void main(String[] args) {
        // se esperan dos argumentos: nombre de usuario y contraseña
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // se recuperan los dos argumentos
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // se codifica la cadena
        byte[] data = Base64.encode(chaîne.getBytes());
        // se muestra su codificación Base64
        System.out.println(new String(data));
    }

}

Si ejecutamos este programa con los dos argumentos [admin admin]:

  

obtenemos el siguiente resultado:

YWRtaW46YWRtaW4=

Ahora que sabemos cómo generar el encabezado de autenticación HTTP, iniciamos el servicio web, que ahora es seguro. A continuación, con el cliente Chrome [Advanced Rest Client], solicitamos la lista de todos los médicos:

  • en [1], solicitamos el URL de los médicos;
  • en [2], con un método GET;
  • en [3], proporcionamos el encabezado HTTP de la autenticación. El código [YWRtaW46YWRtaW4=] es la codificación Base64 de la cadena [admin:admin];
  • en [4], enviamos el comando HTTP;

La respuesta del servidor es la siguiente:

  • en [1], el encabezado de autenticación HTTP;
  • en [2], el servidor devuelve una respuesta JSON;
  • en [3], la lista de médicos.

Intentemos ahora una solicitud HTTP con un encabezado de autenticación incorrecto. La respuesta es entonces la siguiente:

  • en [1] y [3]: el encabezado de autenticación HTTP;
  • en [2]: la respuesta del servicio web;

Ahora probemos con el usuario user / user. Existe, pero no tiene acceso al servicio web. Si ejecutamos el programa de codificación Base64 con los dos argumentos [user user]:

  

obtenemos el siguiente resultado:

dXNlcjp1c2Vy
  • en [1] y [3]: el encabezado de autenticación HTTP;
  • en [2]: la respuesta del servicio web. Es diferente de la anterior, que era [401 Unauthorized]. En esta ocasión, el usuario se ha autenticado correctamente, pero no tiene los permisos suficientes para acceder a URL;

2.15. Conclusion

Recordemos la arquitectura global de nuestra aplicación cliente/servidor:

Ahora ya tenemos operativo un servicio web seguro. Veremos que habrá que modificarlo debido a problemas que surgirán al construir el cliente Angular JS. Pero esperaremos a encontrarnos con el problema para resolverlo. Ahora vamos a construir el cliente Angular que ofrecerá una interfaz web para gestionar las citas de los médicos.