Skip to content

2. El servidor Spring 4

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

  • primero las capas [business] y [DAO] (objeto de acceso a datos). Aquí utilizaremos Spring Data;
  • luego, el servicio web JSON sin autenticación. Aquí utilizaremos Spring MVC;
  • luego añadiremos el componente de autenticación utilizando Spring Security.

Comenzaremos explicando la estructura de la base de datos subyacente a la aplicación.

2.1. La base de datos

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

  

Las citas se gestionan mediante las siguientes tablas:

  • [doctors]: contiene la lista de médicos de la consulta;
  • [clientes]: contiene la lista de pacientes de la consulta;
  • [slots]: contiene los horarios disponibles de cada médico;
  • [rv]: contiene la lista de citas de los médicos.

Las tablas [roles], [users] y [users_roles] están relacionadas con la autenticación. Por ahora, 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 conecta tanto a un cliente como a un médico a través del intervalo de tiempo del médico;
  • un cliente tiene 0 o más citas;
  • un intervalo de tiempo está asociado a 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 de identificación del médico; clave principal de la tabla
  • VERSION: Número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza un cambio en la fila.
  • LAST_NAME: el apellido del médico
  • FIRST_NAME: el nombre del médico
  • TITLE: su tratamiento (Sra., Sra., Sr.)

2.1.2. La tabla [CLIENTS]

Los clientes de los distintos médicos se almacenan en la tabla [CLIENTS]:

  • ID: número de identificación del cliente; clave principal de la tabla
  • VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza un cambio en la fila.
  • APELLIDOS: los apellidos del cliente
  • NOMBRE: el nombre del cliente
  • TÍTULO: su tratamiento (Sra., Sra., Sr.)

2.1.3. La tabla [SLOTS]

En ella se indican los horarios en los que hay citas disponibles:

  • ID: número de identificación del intervalo de tiempo; clave principal de la tabla (fila 8)
  • VERSION: número que identifica la versión de la fila en la tabla. Este número se incrementa en 1 cada vez que se realiza un cambio en la fila.
  • DOCTOR_ID: Número de identificación del médico al que pertenece este intervalo de tiempo; clave externa en la columna DOCTORS(ID).
  • START_TIME: Hora de inicio de la franja horaria
  • MSTART: Minutos de inicio del intervalo de tiempo
  • HFIN: hora de finalización del intervalo
  • MFIN: minutos de finalización de la franja horaria

La segunda fila de la tabla [SLOTS] (véase [1] más arriba) indica, por ejemplo, que el intervalo n.º 2 comienza a las 8:20 a. m. y termina a las 8:40 a. m., y que pertenece a la doctora n.º 1 (Sra. Marie PELISSIER).

2.1.4. La tabla [RV]

En ella se enumeran las citas reservadas para cada médico:

  • ID: identificador único de la cita – clave principal
  • DAY: día de la cita
  • SLOT_ID: franja horaria de la cita – clave externa en el campo [ID] de la tabla [SLOTS] – determina tanto la franja horaria como el médico asignado.
  • CLIENT_ID: ID 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 los valores de las columnas unidas (DÍA, ID_FRANJA):

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

Si una fila de la tabla [RV] tiene el valor (DAY1, SLOT_ID1) para las columnas (DAY, SLOT_ID), este valor no puede aparecer en ningún otro lugar. De lo contrario, esto significaría que se han reservado dos citas a la misma hora para el mismo médico. Desde el punto de vista de la programación Java, el controlador JDBC de la base de datos lanza una excepción SQLException cuando esto ocurre.

La fila con ID igual a 3 (véase [1] más arriba) significa que se reservó una cita para el slot n.º 20 y el cliente n.º 4 el 23/08/2006. La tabla [SLOTS] nos indica que el slot n.º 20 corresponde al intervalo de tiempo de 16:20 a 16:40 y pertenece a la doctora n.º 1 (Sra. Marie PELISSIER). La tabla [CLIENTS] nos indica que el cliente n.º 4 es la Sra. Brigitte BISTROU.

2.2. Introducción a Spring Data

Implementaremos la capa [DAO] del proyecto utilizando Spring Data, una rama del ecosistema Spring.

La página web de Spring ofrece numerosos tutoriales para iniciarse en Spring [http://spring.io/guides]. Utilizaremos uno de ellos para presentar Spring Data. Para ello, utilizaremos Spring Tool Suite (STS).

  • En [1], importamos uno de los tutoriales de [spring.io/guides];
  • En [2], seleccionamos el tutorial [Acceso a datos JPA], que muestra cómo acceder a una base de datos utilizando Spring Data;
  • En [3], seleccionamos un proyecto configurado por Maven;
  • en [4], el tutorial está disponible en dos formatos: [initial], que es una versión vacía que se rellena siguiendo el tutorial, o [complete], que es la versión final del tutorial. Elegimos esta última;
  • En [5], puedes elegir ver 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>
        <!-- use UTF-8 for everything -->
        <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 principal. Este proyecto define la mayoría de las dependencias del proyecto. Pueden ser suficientes, en cuyo caso no se añaden dependencias adicionales, o pueden no serlo, 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 del SGBD H2, que permite crear y gestionar bases de datos en memoria.

Veamos las clases que proporcionan estas dependencias:

Hay muchos:

  • algunas pertenecen al ecosistema Spring (las que empiezan por «spring»);
  • otras pertenecen al ecosistema de Hibernate (hibernate, jboss), cuya implementación de JPA estamos utilizando aquí;
  • otras son bibliotecas de pruebas (JUnit, Hamcrest);
  • otras son bibliotecas de registro (log4j, logback, slf4j);

Vamos a quedarnos con todas. Para una aplicación de producción, solo deberíamos quedarnos con 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á vinculada a las siguientes líneas:


<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 [spring-boot-maven-plugin] permite generar el archivo JAR ejecutable de la aplicación. La línea 26 del archivo [pom.xml] especifica la clase ejecutable de este JAR.

2.2.2. La capa [JPA]

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

  

La aplicación es básica y gestiona entidades [Cliente]. La clase [Cliente] forma parte de la capa [JPA] y tiene la siguiente estructura:


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 ID [id], un nombre [firstName] y un apellido [lastName]. Cada instancia de [Customer] representa una fila en una tabla de la base de datos.

  • línea 8: anotación JPA que garantiza que la persistencia de las instancias [Customer] (Crear, Leer, Actualizar, Eliminar) será gestionada por una implementación de JPA. Según las dependencias de Maven, podemos ver que se está utilizando la implementación de JPA/Hibernate;
  • Líneas 11-12: Anotaciones JPA que asocian el campo [id] con la clave principal de la tabla [Customer]. La línea 12 indica que la implementación de JPA utilizará el método de generación de claves principales específico del SGBD que se esté utilizando, en este caso H2;

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

  • la tabla [Customer] recibirá el nombre de la clase, es decir, [Customer];
  • las columnas de esta tabla recibirá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 los nombres de las columnas de la tabla;

Tenga en cuenta que la implementación de JPA utilizada nunca recibe un nombre.

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 principal 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 en un tipo JPA T:

  • Línea 8: El método `save` se utiliza para persistir una entidad `T` en la base de datos. Persiste la entidad utilizando la clave primaria que le ha asignado el SGBD. También permite actualizar una entidad `T` identificada por su clave primaria `id`. La elección entre estas dos acciones depende del valor de la clave primaria `id`: si es nulo, se realiza la operación de persistencia; en caso contrario, se realiza la operación de actualización;
  • línea 10: igual que arriba, pero para una lista de entidades;
  • línea 12: el método `findOne` recupera 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: variaciones del método [delete];
  • línea 16: el método [findAll] recupera todas las entidades T persistidas;
  • línea 18: igual que arriba, pero limitado a las entidades para las que se ha proporcionado una lista de identificadores;

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 te permite recuperar un [Cliente] por su [apellido];

Y eso es todo en cuanto a la capa [DAO]. No hay ninguna clase de implementación para la interfaz anterior. La genera [Spring Data] en tiempo de ejecución. Los métodos de la interfaz [CrudRepository] se implementan automáticamente. En cuanto a los métodos añadidos a 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 es implementado automáticamente por [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 utilizando 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á con un código similar al siguiente:

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

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

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

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);
 
        // save a couple of 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"));
 
        // fetch all 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();
 
        // fetch an individual customer by ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();
 
        // fetch customers by 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();
    }
 
}
  • Línea 10: indica que la clase se utiliza 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 anotada con [Configuration], normalmente encontramos beans de Spring, es decir, definiciones de clases que deben instanciarse. Aquí no se define ningún bean. Es importante señalar que, al trabajar con un SGBD, deben definirse varios beans de Spring:
    • un [EntityManagerFactory] que define la implementación de 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 tipos de datos.

  • Línea 11: La anotación [EnableAutoConfiguration] es una anotación del proyecto [Spring Boot] (líneas 5-6). Esta anotación indica a Spring Boot, a través de la clase [SpringApplication] (línea 16), que configure la aplicación basándose en las bibliotecas que se encuentran en su ruta de clases. Dado que las bibliotecas de Hibernate están en la ruta de clases, el bean [entityManagerFactory] se implementará utilizando Hibernate. Dado que la biblioteca del SGBD H2 está en la ruta de clases, el bean [dataSource] se implementará utilizando H2. En el bean [dataSource], también debemos definir el nombre de usuario y la contraseña. Aquí, Spring Boot utilizará el administrador predeterminado de H2, que no tiene contraseña. Dado que la biblioteca [spring-tx] se encuentra en la ruta de clases, se utilizará el gestor de transacciones de Spring.

Además, se analizará el directorio que contiene la clase [Application] en busca de beans reconocidos implícitamente por Spring o definidos explícitamente mediante anotaciones de Spring. Por lo tanto, se inspeccionarán las clases [Customer] y [CustomerRepository]. Dado que la primera tiene la anotación [@Entity], se catalogará como una entidad que gestionará Hibernate. Dado que la segunda extiende la interfaz [CrudRepository], se registrará como un 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: solicitamos un bean que implemente la interfaz [CustomerRepository] desde este contexto de Spring. Aquí, recuperamos la clase generada por Spring Data para implementar esta interfaz.

Las siguientes operaciones simplemente utilizan los métodos del bean que implementa la interfaz [CustomerRepository]. Obsérvese en la línea 50 que se cierra el contexto. La salida de la consola es la siguiente:

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

2014-06-05 16:23:13.877  INFO 11664 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 11664 (D:\Temp\wksSTS\gs-accessing-data-jpa-complete\target\classes started by ST in D:\Temp\wksSTS\gs-accessing-data-jpa-complete)
2014-06-05 16:23:13.936  INFO 11664 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: startup date [Thu Jun 05 16:23:13 CEST 2014]; root of context hierarchy
2014-06-05 16:23:15.424  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2014-06-05 16:23:15.518  INFO 11664 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing 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 found
2014-06-05 16:23:15.694  INFO 11664 --- [ main] org.hibernate.cfg.Environment : HHH000021: Bytecode provider 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: Using dialect: org.hibernate.dialect.H2Dialect
2014-06-05 16:23:16.300  INFO 11664 --- [ main] o.h.h.i.ast.ASTQueryTranslatorFactory : HHH000397: Using ASTQueryTranslatorFactory
2014-06-05 16:23:16.613  INFO 11664 --- [ main] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000227: Running hbm2ddl schema export
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: Schema export complete
2014-06-05 16:23:17.074  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-06-05 16:23:17.094  INFO 11664 --- [           main] hello.Application                        : Started Application in 3.906 seconds (JVM running for 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 : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: startup date [Thu Jun 05 16:23:13 CEST 2014]; root of context hierarchy
2014-06-05 16:23:17.332  INFO 11664 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
2014-06-05 16:23:17.333  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2014-06-05 16:23:17.334  INFO 11664 --- [ main] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000227: Running hbm2ddl schema export
Hibernate: drop table customer if exists
2014-06-05 16:23:17.336  INFO 11664 --- [ main] org.hibernate.tool.hbm2ddl.SchemaExport : HHH000230: Schema export complete
  • 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. Es un contenedor de beans;
  • línea 11: el bean [entityManagerFactory] se implementa utilizando la clase [LocalContainerEntityManagerFactory], una clase de Spring;
  • línea 12: aparece [hibernate]. Esta es la implementación de JPA que se ha elegido;
  • línea 19: un dialecto de Hibernate es la variante de SQL que se utilizará con el SGBD. Aquí, el dialecto [H2Dialect] indica que Hibernate funcionará con el SGBD H2;
  • líneas 22-24: se crea la tabla [CUSTOMER]. Esto significa que Hibernate se ha configurado para generar tablas a partir de definiciones JPA, en este caso la definición JPA de la clase [Customer];
  • líneas 27–32: registros de Hibernate que muestran la inserción de filas en la tabla [CUSTOMER]. Esto significa que Hibernate se ha configurado para generar registros;
  • líneas 35-39: los cinco clientes 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 de 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 utilizaremos la configuración automática que ofrece Spring Boot. Lo configuraremos manualmente. Esto puede resultar útil si las configuraciones predeterminadas no se ajustan a nuestras necesidades.

En primer lugar, especificaremos 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>
        <!-- Spring transactions -->
        <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 Database -->
        <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: bibliotecas principales de Spring;
  • líneas 19–28: bibliotecas de Spring para gestionar transacciones de bases 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: el SGBD H2;
  • líneas 54–63: Las bases de datos suelen utilizarse con grupos de conexiones abiertas, lo que evita tener que abrir y cerrar conexiones repetidamente. En este caso, la implementación utilizada es la de [commons-dbcp];

Sin salir de [pom.xml], cambiamos 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] permanecen sin cambios. Modificaremos 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 antes, 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 ninguna anotación 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 {
    // h2 data source
    @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;
    }
 
    // the provider 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();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • línea 22: la anotación [@Configuration] convierte a la clase [Config] en una clase de configuración de Spring;
  • línea 21: la anotación [@EnableJpaRepositories] especifica los directorios donde se encuentran las interfaces [CrudRepository] de Spring Data. Estas interfaces se convertirán en componentes de 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] especifica los directorios en los que se deben buscar las entidades JPA. Aquí se ha comentado porque esta información se proporcionó explícitamente en la línea 50. Esta anotación debe estar presente si se utiliza el modo [@EnableAutoConfiguration] y las entidades JPA no se encuentran en el mismo directorio que la clase de configuración;
  • línea 18: la anotación [@ComponentScan] permite enumerar los directorios en los que se deben buscar los componentes de Spring. Los componentes de Spring son clases etiquetadas con anotaciones de Spring como @Service, @Component, @Controller, etc. Aquí no hay otros aparte de los definidos 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 aquí puede ser cualquiera. Sin embargo, debe llamarse [dataSource] si la EntityManagerFactory de la línea 47 no está presente y se define mediante la configuración automática;
  • 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 de JPA utilizada, en este caso una implementación de Hibernate. El nombre del método aquí 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 [entityManagerFactory];
  • línea 47: el método recibe dos parámetros de los tipos 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 de JPA que se va a utilizar;
  • línea 50: especifica los directorios donde se pueden encontrar 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 [transactionManager]. Recibe el bean de las líneas 46–54 como parámetro;
  • línea 60: el gestor de transacciones se asocia con EntityManagerFactory;

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

Al ejecutar el proyecto se obtienen los mismos resultados. Aparece un nuevo archivo en la carpeta del proyecto, el archivo de la base de datos H2:

  

Por fin podemos prescindir de Spring Boot. Creamos 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]. Como se ve en la línea 5, ya no hay ninguna dependencia de Spring Boot.

La ejecución arroja los mismos resultados que antes.

2.2.6. Creación de un archivo ejecutable

Para crear un archivo ejecutable del proyecto, proceda de la siguiente manera:

  • en [1]: cree una configuración de tiempo de ejecución;
  • en [2]: de tipo [Aplicación Java]
  • en [3]: especifica el proyecto que se va a ejecutar (utiliza el botón Examinar);
  • en [4]: especifica la clase que se va a ejecutar;
  • en [5]: el nombre de la configuración de ejecución; puede ser cualquiera;
  • en [6]: exporta el proyecto;
  • en [7]: como archivo JAR ejecutable;
  • en [8]: especifica 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, abre 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 que se muestran 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 for further details.
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 una plantilla de proyecto de Spring Data, sigue estos pasos:

  • En [1], crea un nuevo proyecto;
  • en [2]: selecciona [Spring Starter Project];
  • El proyecto generado será un proyecto Maven. En [3], especifique el nombre del grupo del proyecto;
  • En [4], especifique el nombre del artefacto (un archivo JAR en este caso) que se creará al compilar el proyecto;
  • en [5]: especifique el paquete de la clase ejecutable que se creará en el proyecto;
  • en [6]: el nombre del proyecto en Eclipse; puede ser cualquiera (no tiene por qué ser el mismo que en [4]);
  • en [7]: especifique que está creando 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] incluye 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/> <!-- lookup parent from repository -->
    </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: dependencias necesarias para JPA; incluirá [Spring Data];
  • líneas 13–17: 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 prueba [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 definidos en este archivo;
  • línea 8: la anotación [@RunWith] permite la integración de Spring con JUnit: la clase se puede ejecutar como una prueba JUnit. [@RunWith] es una anotación de JUnit (línea 4), mientras que la clase [SpringJUnit4ClassRunner] es una clase de Spring (línea 6);

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

2.3. El proyecto de servidor de Eclipse

  

Los principales componentes del proyecto son los siguientes:

  • [pom.xml]: el archivo de configuración de Maven del proyecto;
  • [rdvmedecins.entities]: las entidades JPA;
  • [rdvmedecins.repositories]: interfaces de Spring Data para acceder a las entidades JPA;
  • [rdvmedecins.metier]: la capa [de negocio];
  • [rdvmedecins.domain]: las entidades gestionadas por la capa [de negocio];
  • [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>
        <!-- use UTF-8 for everything -->
        <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 depende del proyecto padre [spring-boot-starter-parent]. Para las dependencias que ya están presentes en el proyecto padre, no se especifica ninguna versión. Se utilizará la versión definida en el proyecto padre. Las demás dependencias se declaran como de costumbre;
  • líneas 14–17: para Spring Data;
  • líneas 18–22: para las pruebas JUnit;
  • líneas 23–26: controlador JDBC para el SGBD MySQL5;
  • líneas 27–34: grupo de conexiones Commons DBCP;
  • líneas 35-38: biblioteca Jackson para el manejo de JSON;
  • líneas 39–43: biblioteca Google Collections;

La versión 1.1.0.RC1 de [spring-boot-starter-parent] utiliza las siguientes versiones de 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. Entidades JPA

Las entidades JPA son los objetos que encapsulan las filas de las tablas de la base de datos.

  

La clase [AbstractEntity] es la clase padre de las entidades [Person, Slot, Appointment]. 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;
    }
 
    // initialization
    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 and setters
    ..
}
  • línea 11: la anotación [@MappedSuperclass] indica que la clase anotada es una clase padre de las entidades JPA [@Entity];
  • Líneas 15-17: definen la clave principal [id] para cada entidad. Es la anotación [@Id] la que convierte el campo [id] en una clave principal. La anotación [@GeneratedValue(strategy = GenerationType.AUTO)] indica que el valor de esta clave principal lo genera el SGBD y que no se impone ningún modo de generación;
  • Líneas 18-19: definen la versión de cada entidad. La implementación de JPA incrementará este número de versión cada vez que se modifique la entidad. Este número se utiliza para evitar actualizaciones simultáneas de la entidad por parte de dos usuarios diferentes: dos usuarios, U1 y U2, leen la entidad E con un número de versión igual a V1. U1 modifica E y persiste este cambio en la base de datos: el número de versión cambia entonces a V1+1. U2 modifica a su vez E y persiste este cambio en la base de datos: recibirá una excepción porque su versión (V1) difiere de la de la base de datos (V1+1);
  • líneas 29–33: el método [build] inicializa los dos campos de [AbstractEntity]. Este método devuelve una referencia a la instancia de [AbstractEntity] así inicializada;
  • líneas 36-44: se redefine el método [equals] de la clase: dos entidades se consideran iguales si tienen el mismo nombre de clase y el mismo identificador id;

La entidad [Person] es la clase padre de las entidades [Doctor] 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;
    // attributes of a person
    @Column(length = 5)
    private String titre;
    @Column(length = 20)
    private String nom;
    @Column(length = 20)
    private String prenom;
 
    // default builder
    public Personne() {
    }
 
    // builder with parameters
    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 and setters
    ...
}
  • línea 6: la anotación [@MappedSuperclass] indica que la clase anotada es una clase padre de entidades JPA [@Entity];
  • líneas 10-15: una persona tiene un tratamiento (Sra.), un nombre (Jacqueline) y un apellido (Tatou). No se proporciona información sobre las columnas de la tabla. 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;
 
    // default builder
    public Medecin() {
    }
 
    // builder with parameters
    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 [DOCTORS] de la base de datos;
  • línea 8: la entidad [Doctor] deriva de la entidad [Person];

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

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

Si además queremos asignarle un ID y una versión, podemos 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;
 
    // default builder
    public Client() {
    }
 
    // builder with parameters
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }
 
    // identity
    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 [Person];

La entidad [TimeSlot] 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;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
 
    // default builder
    public Creneau() {
    }
 
    // builder with parameters
    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);
    }
 
    // foreign key
    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 la entidad [AbstractEntity] y, por lo tanto, hereda los campos [id] y [version];
  • línea 16: hora de inicio del intervalo (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 representa en las líneas 22-24;
  • Línea 22: La anotación [@ManyToOne] indica una relación de muchos a uno (franjas horarias) a uno (médico). El atributo [fetch=FetchType.LAZY] especifica que, cuando se solicita una entidad [Creneau] desde el contexto de persistencia y debe recuperarse de la base de datos, la entidad [Medecin] no se recupera junto con ella. La ventaja de este modo es que la entidad [Doctor] solo se recupera si el desarrollador la solicita. Esto ahorra memoria y mejora el rendimiento;
  • línea 23: especifica 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 se puede modificar de dos formas diferentes, lo cual no está permitido por el estándar JPA. Por lo tanto, añadimos los atributos [insertable = false, updatable = false], lo que significa que la columna solo se puede 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;
 
    // characteristics of an Rv
    @Temporal(TemporalType.DATE)
    private Date jour;
 
    // an appointment is linked to a customer
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;
 
    // an appointment is linked to a time slot
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;
 
    // foreign keys
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;
 
    // default builder
    public Rv() {
    }
 
    // with parameters
    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);
    }
 
    // foreign keys
    public long getIdCreneau() {
        return idCreneau;
    }
 
    public long getIdClient() {
        return idClient;
    }
 
    // getters and 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 la entidad [AbstractEntity] y, por lo tanto, hereda los campos [id] y [version];
  • línea 21: la fecha de la cita;
  • línea 20: el tipo Java [Date] contiene tanto una fecha como una hora. Aquí especificamos 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 representa en las líneas 24-26;
  • líneas 29-31: el intervalo de tiempo de la cita. La tabla [RV] tiene una clave externa en la tabla [CRENEAUX]. Esta relación se representa en las líneas 29-31;
  • filas 34-35: la clave externa [idClient];
  • filas 36-37: la clave externa [idCreneau];

2.6. La capa [DAO]

Implementaremos la capa [DAO] utilizando Spring Data:

  

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

  • [ClientRepository]: proporciona acceso a las entidades JPA [Client];
  • [CreneauRepository]: proporciona acceso a las entidades JPA [Creneau];
  • [MedecinRepository]: proporciona acceso a las entidades JPA [Medecin];
  • [RvRepository]: proporciona acceso a las entidades JPA [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] simplemente hereda los métodos de la interfaz [CrudRepository] sin añadir ninguno más;

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] simplemente hereda los métodos de la interfaz [CrudRepository] sin añadir ninguno más;

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> {
    // list of physician slots
    @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] recupera los horarios disponibles de un médico;
  • línea 11: el parámetro es el ID 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] se utiliza para especificar la consulta JPQL (Java Persistence Query Language) que implementa el método. El parámetro [?1] será sustituido 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] recupera las citas de un médico para un día determinado;
  • Línea 13: Los parámetros son el ID 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. La siguiente consulta JPQL no es suficiente:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

porque los campos de la clase Rv, de tipos [Client] y [Creneau], se recuperan en modo [FetchType.LAZY], lo que significa que deben solicitarse explícitamente para obtenerlos. Esto se hace en la consulta JPQL utilizando la sintaxis [left join fetch entity], que requiere que se realice una unión con la tabla a la que hace referencia la clave externa para recuperar la entidad referenciada;

2.7. La capa [business]

  
  • [IMetier] es la interfaz de la capa [business] y [Metier] es su implementación;
  • [DoctorDailySchedule] y [DoctorDailySlot] son dos entidades de negocio;

2.7.1. Las entidades

La entidad [DoctorTimeSlot] asocia un intervalo de tiempo con cualquier cita reservada dentro de ese 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;
    // fields
    private Creneau creneau;
    private Rv rv;
 
    // manufacturers
    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 and setters
...
}
  • línea 12: la franja horaria;
  • línea 13: la cita, si la hay; en caso contrario, nulo;

La entidad [AgendaMedecinJour] es la 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;
    // fields
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;
 
    // manufacturers
    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 and setters
...
}
  • línea 13: el médico;
  • línea 14: el día del calendario;
  • línea 15: sus franjas horarias disponibles, con o sin cita previa;

2.7.2. El servicio

La interfaz de la capa [de negocio] 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 {
 
    // customer list
    public List<Client> getAllClients();
 
    // list of doctors
    public List<Medecin> getAllMedecins();
 
    // list of physician slots
    public List<Creneau> getAllCreneaux(long idMedecin);
 
    // list of doctor's appointments on a given day
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
 
    // find a customer identified by its id
    public Client getClientById(long id);
 
    // find a customer identified by its id
    public Medecin getMedecinById(long id);
 
    // find an Rv identified by its id
    public Rv getRvById(long id);
 
    // find a time slot identified by its id
    public Creneau getCreneauById(long id);
 
    // add a RV to the list
    public Rv ajouterRv(Date jour, Creneau créneau, Client client);
 
    // delete a RV
    public void supprimerRv(Rv rv);
 
    // job
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
 
}

Los comentarios explican la función de cada método.

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 {
 
    // repositories
    @Autowired
    private MedecinRepository medecinRepository;
    @Autowired
    private ClientRepository clientRepository;
    @Autowired
    private CreneauRepository creneauRepository;
    @Autowired
    private RvRepository rvRepository;
 
    // interface implementation
    @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 al componente. Este se llama [business];
  • 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 anotado de esta manera será inicializado (inyectado) por Spring con la referencia a un componente de Spring del tipo o nombre especificado. Aquí, la anotación [@Autowired] no especifica un nombre. Por lo tanto, se realizará una inyección basada en el tipo;
  • línea 29: el campo [medecinRepository] se inicializará con la referencia a un componente de Spring de tipo [MedecinRepository]. Esta será la referencia a 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 mencionadas;
  • 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 convertimos a una [List<Client>] utilizando 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 utilizando clases de la capa [DAO];

Solo el método de la línea 88 es específico de la capa [business]. Se ha colocado aquí porque ejecuta una lógica de negocio que va más allá del simple acceso a datos. Sin este método, no habría razón para crear una capa [business]. El método [getAgendaMedecinJour] es el siguiente:


public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        // list of doctor's time slots
        List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
        // list of bookings for the same doctor on the same day
        List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
        // a dictionary is created from the Rvs taken
        Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
        for (Rv resa : reservations) {
            hReservations.put(resa.getCreneau().getId(), resa);
        }
        // create the agenda for the requested day
        AgendaMedecinJour agenda = new AgendaMedecinJour();
        // the doctor
        agenda.setMedecin(getMedecinById(idMedecin));
        // the day
        agenda.setJour(jour);
        // reservation slots
        CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
        agenda.setCreneauxMedecinJour(creneauxMedecinJour);
        // filling reservation slots
        for (int i = 0; i < creneauxHoraires.size(); i++) {
            // line i agenda
            creneauxMedecinJour[i] = new CreneauMedecinJour();
            // time slot
            Creneau créneau = creneauxHoraires.get(i);
            long idCreneau = créneau.getId();
            creneauxMedecinJour[i].setCreneau(créneau);
            // is the slot free or reserved?
            if (hReservations.containsKey(idCreneau)) {
                // the slot is occupied - we note the resa
                Rv resa = hReservations.get(idCreneau);
                creneauxMedecinJour[i].setRv(resa);
            }
        }
        // we return the result
        return agenda;
    }

Se recomienda a los lectores que lean los comentarios. El algoritmo es el siguiente:

  • recuperar todos los horarios disponibles para el médico especificado;
  • recuperar todas sus citas para el día especificado;
  • con esta información, podemos determinar si un intervalo de tiempo está disponible o reservado;

2.8. Configuración del proyecto

  

La clase [DomainAndPersistenceConfig] 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 {
 
    // the MySQL data source
    @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;
    }
 
    // provider JPA - not required if you're happy with the default values used by Spring boot
    // here we define it to enable / disable logs SQL
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }
 
    // the EntityManagerFactory and TransactionManager are defined with default values by Spring boot
 
}
  • Línea 45: No definiremos los beans [EntityManagerFactory] y [TransactionManager]. En su lugar, utilizaremos la anotación [@EnableAutoConfiguration] de Spring Boot (línea 17);
  • Líneas 24–32: Definimos la fuente de datos MySQL 5. Se trata de un bean que Spring Boot no suele poder configurar automáticamente;
  • Líneas 36–43: También configuramos la implementación de JPA para establecer el atributo [showSql] de Hibernate en false (línea 39). Por defecto, está establecido en true;
  • Por ahora, los únicos componentes gestionados por Spring son los beans de las líneas 25 y 37, además de los beans [EntityManagerFactory] y [TransactionManager] a través de la configuración automática. Necesitamos añadir los beans de las capas [business] 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 subclases que tengan una anotación de Spring. En el paquete [rdvmdecins.metier], se buscará 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]. Debemos indicar a este bean dónde se encuentran las entidades JPA que debe gestionar. La línea 19 hace esto;
  • Línea 20: especifica que los métodos de las interfaces que heredan de la interfaz [CrudRepository] deben ejecutarse dentro de una transacción;

2.9. Pruebas para la capa [business]

  

La clase [rdvmedecins.tests.Metier] es una clase de pruebas de 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(){
        // customer display
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // physician display
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // display doctor's slots
        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);
        // list of doctor's appointments on a given day
        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));
        // add a 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);
        // check
        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));
        // add a RV in the same slot on the same day
        // must trigger an exception
        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();
            }
            // we note the error
            erreur = true;
        }
        // check for errors
        Assert.assertTrue(erreur);
        // RV list
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // calendar display
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // delete a RV
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // check
        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));
    }
 
    // utility method - displays items in a collection
    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] mencionado anteriormente. De este modo, la clase de prueba se beneficia de todos los beans definidos en este archivo;
  • línea 23: la anotación [@RunWith] permite la integración de Spring con JUnit: la clase se puede ejecutar como una prueba JUnit. [@RunWith] es una anotación de JUnit (línea 9), mientras que la clase [SpringJUnit4ClassRunner] es una clase de Spring (línea 12);
  • Líneas 26-27: Inyección de una referencia a la capa [business] en la clase de prueba;
  • Muchas pruebas son simplemente pruebas visuales:
    • líneas 32-33: lista de clientes;
    • 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 [addAppt] devuelve la cita con información adicional, su clave primaria id;
  • línea 53: esta clave primaria se utiliza para buscar la cita en la base de datos;
  • línea 54: verificamos que la cita que se está buscando y la cita encontrada son la misma. Recordemos que el método [equals] de la entidad [Rv] se ha redefinido: dos citas son iguales si tienen el mismo id. Aquí, esto nos muestra que la cita añadida se ha insertado efectivamente en la base de datos;
  • Líneas 61–73: Intentamos añadir la misma cita por segunda vez. El SGBD debería rechazarla porque 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 especifica que la combinación [DAY, SLOT_ID] debe ser única, lo que evita que se programen dos citas en el mismo intervalo de tiempo del mismo día.

  • línea 73: verificamos que efectivamente se ha producido una excepción;
  • línea 77: recuperamos el calendario del médico para el que acabamos de añadir una cita;
  • línea 79: verificamos que la cita añadida se encuentra efectivamente en su agenda;
  • línea 82: eliminamos la cita añadida;
  • línea 84: recuperamos la cita eliminada de la base de datos;
  • línea 85: comprobamos que hemos recuperado un puntero nulo, lo que indica que la cita que buscábamos no existe;

La prueba se ejecuta correctamente:

 

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 {
    // the boot
    public static void main(String[] args) {
        // prepare the configuration
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // launch it
        ConfigurableApplicationContext context = app.run(args);
        // business
        IMetier métier = context.getBean(IMetier.class);
        try {
            // add a RV to the list
            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));
            // check
            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());
        }
        // closing the Spring context
        context.close();
    }
 
    // utility method - displays items in a collection
    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]. Por lo tanto, se trata de una referencia a la capa [business];
  • líneas 27-31: añadir una nueva cita para hoy, para el cliente n.º 1 en la franja horaria n.º 1. El cliente y la franja horaria se crearon desde cero para demostrar que solo se utilizan identificadores. Inicializamos la versión aquí, pero podríamos haber utilizado cualquier valor. No se utiliza aquí;
  • línea 34: queremos saber qué médico tiene el espacio n.º 1. Para ello, necesitamos consultar la base de datos para el espacio n.º 1. Como estamos en modo [FetchType.LAZY], el médico no se devuelve junto con el espacio. Sin embargo, nos aseguramos 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: mostramos la lista de citas del médico;

La salida de la consola es la siguiente:

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 abordaremos la construcción de la capa web. Esta capa consiste principalmente en métodos que gestionan URL específicas y responden con una línea de texto en formato JSON (JavaScript Object Notation). Esta capa web es una interfaz web a la que a veces se hace referencia como API web. Implementaremos esta interfaz utilizando Spring MVC, otro componente del ecosistema Spring. Comenzaremos revisando 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], seleccionamos el ejemplo [Rest Service];
  • en [3], seleccionamos el proyecto Maven;
  • en [4], seleccionamos la versión final de la guía;
  • en [5], confirmamos;
  • en [6], el proyecto importado;

Los servicios web accesibles a través de URL estándar que devuelven texto JSON suelen denominarse servicios REST (REpresentational State Transfer). En este documento, me referiré simplemente al servicio que vamos a crear como un servicio web/JSON. Se dice que un servicio es RESTful si sigue ciertas reglas. No he intentado cumplir estas reglas.

Examinemos ahora el proyecto importado, comenzando por 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], está presente el proyecto principal [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. La aplicación se ejecutará en este servidor;
  • líneas 21–24: La biblioteca Jackson gestiona JSON: convierte un objeto Java en una cadena JSON y viceversa;

Esta configuración incluye un gran número de bibliotecas:

Arriba vemos los tres archivos del servidor Tomcat.

2.11.3. La arquitectura de un servicio REST de Spring

Spring MVC implementa el patrón arquitectónico MVC (Modelo-Vista-Controlador) de la siguiente manera:

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

  1. solicitud: las URL solicitadas tienen el formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... El [Dispatcher Servlet] es la clase de Spring que gestiona las URL entrantes. «Enruta» la URL a la acción que debe gestionarla. Estas acciones son métodos de clases específicas denominadas [Controladores]. La C de MVC aquí es la cadena [Servlet Dispatcher, Controlador, Acción]. Si no se ha configurado ninguna acción para gestionar la URL entrante, el [Servlet Dispatcher] responderá que no se ha encontrado la URL solicitada (error 404 NOT FOUND);
  1. el procesamiento
  • la acción seleccionada puede utilizar los parámetros que el [Servlet Dispatcher] le ha pasado. Estos pueden provenir de varias fuentes:
    • la ruta [/param1/param2/...] de la URL,
    • los parámetros de la URL [p1=v1&p2=v2],
    • de los parámetros enviados por el navegador con su solicitud;
  • al procesar la solicitud del usuario, la acción puede necesitar la capa [de negocio] [2b]. Una vez procesada la solicitud del cliente, puede desencadenar 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 indica que se muestre una vista específica [3]. Esta vista mostrará datos conocidos como el modelo de vista. Esta es la M de MVC. La acción creará este modelo M [2c] e indicará que se muestre una vista V [3];
  1. respuesta: la vista V seleccionada utiliza el modelo M construido 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 esta respuesta.

En el caso de un servicio web / JSON, la arquitectura anterior se modifica ligeramente:

  • en [4a], el modelo, que es una clase Java, se convierte 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 a la clase [GreetingController] en un controlador de Spring, lo que significa que sus métodos se registran para gestionar URL;
  • línea 15: la anotación [@RequestMapping] especifica la URL gestionada por el método, en este caso la URL [/greeting]. Más adelante veremos que esta URL se puede parametrizar 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, etc.) que se envíe al navegador del cliente, sino que genera la respuesta directamente al navegador. Aquí, produce un objeto de tipo [Greeting] (línea 18). Aunque aquí no resulte evidente a primera vista, este objeto se convertirá primero a JSON antes de enviarse al navegador. Es la presencia de una biblioteca JSON en las dependencias del proyecto lo que hace que Spring Boot configure automáticamente 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 llamado [name] (@RequestParam(value = "name"). Este puede ser un parámetro GET o POST. Este parámetro no es obligatorio (required = false). En este 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 {"id":n,"content":"text"}. En definitiva, la cadena JSON generada por el método del controlador tendrá el siguiente formato:

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

o

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

2.11.6. Configuración del proyecto

  

El proyecto está configurado por 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 es ejecutable con un método [main] específico para aplicaciones de consola. Así es, efectivamente. 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: podemos ver 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 contiene cualquier parámetro adicional;
  • línea 8: la anotación [@EnableAutoConfiguration] indica a Spring Boot que configure el proyecto;
  • línea 7: la anotación [@ComponentScan] hace que se escanee el directorio que contiene la clase [Application] en busca de componentes de Spring. Se encontrará uno: la clase [GreetingController], que tiene la anotación [@Controller], lo que la convierte en un componente de Spring;

2.11.7. Ejecución del proyecto

Ejecutemos el proyecto:

 

Obtenemos los siguientes registros de la 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 of type [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 of type [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, solicitamos la URL [http://localhost:8080/greeting]:

 

Recibimos la cadena JSON esperada. Puede resultar interesante ver los encabezados HTTP enviados por el servidor. Para ello, utilizaremos el complemento de Chrome llamado [Advanced Rest Client] (véase el apéndice):

  • en [1], la URL solicitada;
  • 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], solicitamos la misma URL, pero esta vez utilizando una solicitud POST;
  • en [7], la información se envía al servidor en formato [urlencoded];
  • en [6], el parámetro name con su valor;
  • en [8], el navegador indica al servidor que está enviando 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;

A continuación se explica cómo proceder:

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

En los registros que aparecen en la consola, es importante ver el plugin [spring-boot-maven-plugin]. Este es el plugin que genera el archivo ejecutable.

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

Utilizando una consola, navega hasta 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 está en ejecución, puede acceder a ella mediante un navegador:

 

2.11.9. Implementación de la aplicación en un servidor Tomcat

Aunque Spring Boot resulta muy práctico en el modo de desarrollo, es probable que una aplicación de producción se implemente en un servidor Tomcat real. A continuación te explicamos 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>

Hay que realizar cambios en dos lugares:

  • línea 9: debes especificar que vas 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 añade todas las clases de Tomcat a las dependencias del proyecto;
  • Línea 29: este artefacto está [provided], lo que significa que los archivos correspondientes no se incluirán en el archivo WAR generado. En su lugar, estos archivos se ubicarán en el servidor Tomcat donde se ejecutará la aplicación;

También debe configurar la aplicación web. A falta de un archivo [web.xml], esto se hace utilizando una clase que extiende [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: se sobrescribe el método [configure] (línea 8);
  • línea 10: se proporciona la clase que configura el proyecto;

Para ejecutar el proyecto, proceda de la siguiente manera:

  • en [1], ejecute el proyecto en uno de los servidores registrados en el IDE de Eclipse;
  • en [2], seleccione [tc Server Developer], que es la opción predeterminada. Se trata de una variante de Tomcat;

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

 

Ahora ya sabemos cómo generar un archivo WAR. A partir de aquí, seguiremos trabajando con Spring Boot y su archivo JAR ejecutable.

2.11.10. Creación de un nuevo proyecto web

Para crear un nuevo proyecto web, sigue estos pasos:

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

2.12. La capa [web]

  

Construiremos la capa web en varios pasos:

  • Paso 1: una capa web funcional sin autenticación;
  • Paso 2: Implementación de la autenticación con Spring Security;
  • Paso 3: Implementación de CORS [El intercambio de recursos entre orígenes (CORS) es un mecanismo que permite que muchos recursos (por ejemplo, fuentes, JavaScript, etc.) de una página web sean solicitados desde otro dominio distinto al dominio de origen del recurso. (Wikipedia)]. El cliente de nuestro servicio web será un cliente web Angular que no pertenece necesariamente al mismo dominio que nuestro servicio web. Por defecto, no puede acceder al servicio web a menos que este lo autorice a hacerlo. 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: dependencias para un proyecto Spring MVC;
  • líneas 17–21: dependencias de las capas [lógica de negocio, DAO, JPA];

2.12.2. La interfaz del servicio web

  • En [1] anterior, el navegador solo puede solicitar un número limitado de URL con una sintaxis específica;
  • en [4], recibe una respuesta JSON;

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


package rdvmedecins.web.models;
 
public class Reponse {
 
    // ----------------- properties
    // operation status
    private int status;
    // the answer JSON
    private Object data;
 
    // ---------------constructeurs
    public Reponse() {
    }
 
    public Reponse(int status, Object data) {
        this.status = status;
        this.data = data;
    }
 
    // methods
    public void incrStatusBy(int increment) {
        status += increment;
    }
 
    // ----------------------getters and setters
...
}
  • línea 7: código de error de la respuesta 0: OK, cualquier otro: KO;
  • línea 9: el cuerpo de la respuesta;

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

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

Lista de todos los médicos del centro médico [/getAllMedecins]

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

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

Agenda diaria del médico [/getAgendaMedecinJour/{idMedecin}/{yyyy-mm-dd}]

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

Añadir una cita [/ addRv]

  • en [0], la URL del servicio web;
  • en [1], se utiliza el método POST;
  • en [2], el texto JSON de la información enviada al servicio web en el formato {day, clientId, slotId};
  • en [3], el cliente especifica al servicio web que está enviando 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 también envía JSON;
  • en [6]: la respuesta JSON del servicio web. El campo [data] contiene la representación JSON de la cita añadida;

Se puede verificar la presencia de la nueva cita:

Eliminar una cita [/deleteApp]

  • en [1], la URL del servicio web;
  • en [2], se utiliza el método POST;
  • en [3], el texto JSON de la información enviada al servicio web en el formato {idRv};
  • en [4], el cliente especifica al servicio web que está enviando datos JSON;

La respuesta es entonces la siguiente:

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

Se puede comprobar que la cita se ha eliminado:

Como se muestra arriba, la cita de la paciente [Sra. GERMAN] ya no aparece en la lista.

El servicio web también permite recuperar entidades por su ID:

Todas estas URL son gestionadas por el controlador [RdvMedecinsController], que vamos a presentar a continuación.

2.12.3. Estructura básica 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() {
        // application error messages
        messages = application.getMessages();
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
...
    }
 
    // customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients() {
...
    }
 
    // list of physician slots
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
    }
 
    // list of doctor's appointments
    @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 a la clase [RdvMedecinsController] en un controlador de Spring. Además, garantiza que los métodos que gestionan las URL generen una respuesta que se convierte automáticamente a JSON;
  • líneas 9–10: Spring inyectará aquí un objeto de tipo [ApplicationModel];
  • línea 13: la anotación [@PostConstruct] marca un método para que se ejecute inmediatamente después de que se instancie la clase. Cuando se ejecuta este método, los objetos inyectados por Spring están disponibles;
  • Todos los métodos devuelven un objeto de tipo [Response] de la siguiente manera:

package rdvmedecins.web.models;
 
public class Reponse {
 
    // ----------------- properties
    // operation status
    private int status;
    // the answer
    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 para llamar al método. Aquí, el método gestiona una solicitud GET desde la URL [/getAllMedecins]. Si esta URL se solicitara mediante un POST, se rechazaría y Spring MVC enviaría un código de error HTTP al cliente web;
  • línea 32: la URL se configura con {idMedecin}. Este parámetro se recupera utilizando la anotación [@PathVariable] en la línea 33;
  • línea 33: el único parámetro [long idMedecin] recibe su valor del parámetro {idMedecin} de la URL [@PathVariable("idMedecin")]. El parámetro de la URL y el del método pueden tener nombres diferentes. Tenga en cuenta que [@PathVariable("idMedecin")] es de tipo String (toda la URL es una cadena), mientras que el parámetro [long idMedecin] es de tipo [long]. La conversión de tipos se realiza automáticamente. Se devuelve un código de error HTTP si esta conversión de tipos falla;
  • línea 65: la anotación [@RequestBody] hace referencia al cuerpo de la solicitud. En una solicitud GET, casi nunca hay un cuerpo (pero es posible incluir uno). En una solicitud POST, suele haber uno (pero es posible omitirlo). Para la URL [ajouterRv], el cliente web envía la siguiente cadena JSON en su POST:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

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


package rdvmedecins.web.models;
 
public class PostAjouterRv {
 
    // pOST DATA
    private String jour;
    private long idClient;
    private long idCreneau;
 
    // getters and setters
    ...
}

Aquí también se realizarán automáticamente las conversiones de tipo necesarias;

  • Las líneas 69-70 contienen un mecanismo similar para la URL [/deleteRv]. La cadena JSON enviada es la siguiente:
{"idRv":116}

y el tipo [PostSupprimerRv] es el siguiente:


package rdvmedecins.web.models;
 
public class PostSupprimerRv {
 
    // pOST DATA
    private long idRv;
 
    // getters and setters
    ...
}

2.12.4. Modelos de servicios web

  

Ya hemos presentado los modelos [Response, PostAddAppointment y PostDeleteAppointment]. El modelo [ApplicationModel] es el siguiente:


package rdvmedecins.web.models;
 
import java.util.Date;
...
 
@Component
public class ApplicationModel implements IMetier {
 
    // the [business] layer
    @Autowired
    private IMetier métier;
 
    // data from the [business] layer
    private List<Medecin> médecins;
    private List<Client> clients;
    // error messages
   private List<String> messages;
 
    @PostConstruct
    public void init() {
        // we get the doctors and the customers
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }
 
    // getter
    public List<String> getMessages() {
        return messages;
    }
 
    // ------------------------- [business] layer interface
    @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 a la clase [ApplicationModel] en un componente de Spring. Al igual que todos los componentes de Spring vistos hasta ahora (con la 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 a la capa [business];
  • línea 19: la anotación [@PostConstruct] garantiza que el método [init] se ejecute inmediatamente después de que se instancie la clase [ApplicationModel];
  • líneas 23-24: las listas de médicos y clientes se recuperan de la capa [business];
  • línea 26: si se produce una excepción, almacenamos los mensajes de la pila de excepciones en el campo de la línea 17;

La clase [ApplicationModel] tendrá dos funciones:

  • como caché para almacenar las listas de médicos y pacientes (clientes);
  • como una única interfaz 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 ofrece flexibilidad en la gestión de la caché. Actualmente, las franjas horarias de las citas médicas no se almacenan en caché. Para hacerlo, basta con modificar la clase [ApplicationModel]. Esto no afecta al controlador, que seguirá utilizando el método [List<Creneau> getAllCreneaux(long idMedecin)] como hasta ahora. Lo que se modificará es la implementación de este método en [ApplicationModel].

2.12.5. La clase Static

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

  

Su código es el siguiente:


package rdvmedecins.web.helpers;
 
import java.text.SimpleDateFormat;
...
 
public class Static {
 
    public Static() {
    }
 
    // list of exception error messages
    public static List<String> getErreursForException(Exception exception) {
        // retrieve the list of exception error messages
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }
 
    // mappers Object --> Map
    // --------------------------------------------------------
....
}
  • línea 12: el método [Static.getErrorsForException] que se utilizó (línea 8 más abajo) en el método [init] de la clase [ApplicationModel]:

    @PostConstruct
    public void init() {
        // we get the doctors and the customers
        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>] que contiene los mensajes de error [exception.getMessage()] de una excepción [exception] y los de sus excepciones internas [exception.getCause()].

La clase [Static] contiene otros métodos de utilidad que volveremos a ver cuando nos encontremos con ellos.

A continuación, detallaremos el manejo de las URL del servicio web. En este proceso 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 la sección 2.12.3) tiene un método [init] que se ejecuta inmediatamente después de su instanciación:


    @Autowired
    private ApplicationModel application;
    private List<String> messages;
 
    @PostConstruct
    public void init() {
        // application error messages
        messages = application.getMessages();
}
  • Línea 8: Los mensajes de error almacenados en la caché de la aplicación [ApplicationModel] se guardan localmente en el campo de la línea 3. Esto permite a los métodos determinar si la aplicación se ha inicializado correctamente.

2.12.7. La URL [/getAllMedecins]

La URL [/getAllDoctors] es gestionada por el siguiente método en el controlador [RdvMedecinsController]:


    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // list of doctors
        try {
            return new Reponse(0, application.getAllMedecins());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}
  • línea 5: comprobamos si la aplicación se ha inicializado correctamente (messages == null). Si no es así, devolvemos una respuesta con status = -1 y data = messages;
  • línea 10: en caso contrario, devolvemos la lista de médicos cuyo estado es 0. El método [application.getAllMedecins()] no lanza ninguna excepción, ya que simplemente devuelve una lista almacenada en caché. No obstante, mantendremos este tratamiento de excepciones por si los médicos ya no estuvieran almacenados en caché;

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

Image

Efectivamente, obtenemos un error. En circunstancias normales, obtenemos la siguiente vista:

2.12.8. La URL [/getAllClients]

La URL [/getAllClients] es gestionada por el siguiente método en el [RdvMedecinsController]:


    // customer list
    @RequestMapping(value = "/getAllClients")
    public Reponse getAllClients() {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // customer list
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}

Es similar al método [getAllMedecins] que ya hemos estudiado. Los resultados obtenidos son los siguientes:

2.12.9. La URL [/getAllSlots/{doctorId}]

La URL [/getAllSlots/{doctorId}] se gestiona mediante el siguiente método en el controlador [RdvMedecinsController]:


// list of physician slots
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // we get the doctor back
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // doctor's slots
        List<Creneau> créneaux = null;
        try {
            créneaux = application.getAllCreneaux(médecin.getId());
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // we return the answer
        return new Reponse(0, Static.getListMapForCreneaux(créneaux));
    }
  • línea 9: se solicita el médico identificado por el parámetro [id] desde un método local:

    private Reponse getMedecin(long id) {
        // we get the doctor back
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // existing doctor?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, médecin);
}

Este método devuelve un valor de estado en el rango [0,1,2]. Volvamos al código del método [getAllSlots]:

  • líneas 10-12: si status ≠ 0, devuelve la respuesta inmediatamente;
  • línea 13: recuperamos el médico;
  • línea 17: recuperamos los horarios de este médico;
  • línea 22: devolvemos un objeto [Static.getListMapForCreneaux(slots)] como respuesta;

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


@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • línea 13: el médico se recupera 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] obliga a realizar una unión entre las tablas [CRENEAUX] y [MEDECINS]. Como resultado, la consulta devuelve todas las franjas horarias de citas del médico, con el médico incluido en cada una de ellas. Cuando serializamos estas franjas en JSON, la cadena JSON del médico aparece en cada una de ellas. Esto es innecesario. Por lo tanto, en lugar de serializar un objeto [Creneau], serializaremos un objeto [Map] que contenga solo los campos deseados.

Volvamos al código que vimos anteriormente:


// we return the answer
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) {
        // liste de dictionnaires <String,Object>
        List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
        for (Creneau créneau : créneaux) {
            liste.add(Static.getMapForCreneau(créneau));
        }
        // on rend la liste
        return liste;
}

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


    // Creneau --> Map
    public static Map<String, Object> getMapForCreneau(Creneau créneau) {
        // qq chose à faire ?
        if (créneau == null) {
            return null;
        }
        // dictionnaire <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());
        // on rend le dictionnaire
        return hash;
}
  • línea 8: creamos un diccionario;
  • líneas 9–13: añadimos los campos que queremos conservar en la cadena JSON. El campo [doctor] no se incluye;
  • línea 15: devolvemos este diccionario;

Los resultados obtenidos son los siguientes:

o estos si el intervalo de tiempo no existe:

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

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

La URL [/getRvMedecinJour/{idMedecin}/{jour}] se gestiona mediante el siguiente método en el controlador [RdvMedecinsController]:


// list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // check the date
        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);
        }
        // we get the doctor back
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // list of appointments
        List<Rv> rvs = null;
        try {
            rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
        } catch (Exception e1) {
            return new Reponse(4, Static.getErreursForException(e1));
        }
        // we return the answer
        return new Reponse(0, Static.getListMapForRvs(rvs));
}
  • Línea 31: Devolvemos 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;
 
    // characteristics of an Rv
    @Temporal(TemporalType.DATE)
    private Date jour;
 
    // an appointment is linked to a customer
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;
 
    // an appointment is linked to a time slot
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;
 
    // foreign keys
    @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: el cliente se recupera utilizando el modo [FetchType.LAZY];
  • línea 18: la ranura se recupera utilizando el modo [FetchType.LAZY];

Recordemos la consulta JPQL que recupera 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")

Las uniones se realizan explícitamente para recuperar los campos [client] y [creneau]. Además, 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 innecesaria. Volvamos al código del método:

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

El diccionario construido para una cita es el siguiente:


    // Rv --> Map
    public static Map<String, Object> getMapForRv(Rv rv) {
        // anything to do?
        if (rv == null) {
            return null;
        }
        // dictionary <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()));
        // we return the dictionary
        return hash;
}
  • Línea 11: Recuperamos el diccionario del objeto [Creneau] que hemos presentado anteriormente;

Los resultados obtenidos son los siguientes:

o estos con un día incorrecto:

o estos con un médico incorrecto:

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

La URL [/getAgendaMedecinJour/{idMedecin}/{jour}] se gestiona mediante el siguiente método en el controlador [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // check the date
        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) });
        }
        // we get the doctor back
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // get your diary back
        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));
    }
}
  • La línea 30 devuelve un objeto de tipo List<Map<String, Object>>.

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


    // AgendaMedecinJour --> Map
    public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
        // anything to do?
        if (agenda == null) {
            return null;
        }
        // dictionary <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);
        // we return the dictionary
        return hash;
}

El diccionario creado tiene tres campos:

  • [doctor]: el médico propietario de la agenda. Hemos conservado esta información porque solo aparece una vez, mientras que en casos anteriores se repetía en cada cadena JSON;
  • [day]: el día del calendario;
  • [doctorSlots]: la lista de franjas horarias disponibles del médico, incluidas las citas programadas para esa franja;

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


    // CreneauMedecinJour --> map
    public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
        // anything to do?
        if (créneau == null) {
            return null;
        }
        // dictionary <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
        hash.put("rv", getMapForRv(créneau.getRv()));
        // we return the dictionary
        return hash;
}
  • líneas 9-10: utilizamos los diccionarios ya comentados para los tipos [Creneau] y [Rv], que por lo tanto no contienen ningún objeto [Medecin];

Los resultados obtenidos son los siguientes:

o estos si el día es incorrecto:

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

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

La URL [/getMedecinById/{id}] se gestiona mediante el siguiente método en el controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
    public Reponse getMedecinById(@PathVariable("id") long id) {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // we get the doctor back
        return getMedecin(id);
}

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


    private Reponse getMedecin(long id) {
        // we get the doctor back
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // existing doctor?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, médecin);
}

Los resultados son los siguientes:

o estos si el ID del médico es incorrecto:

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

La URL [/getClientById/{id}] se gestiona mediante el siguiente método en el controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
    public Reponse getClientById(@PathVariable("id") long id) {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // we get the customer back
        return getClient(id);
}

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


    private Reponse getClient(long id) {
        // we get the customer back
        Client client = null;
        try {
            client = application.getClientById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // existing customer?
        if (client == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, client);
}

Los resultados son los siguientes:

o estos si el ID de cliente es incorrecto:

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

La URL [/getCreneauById/{id}] se gestiona mediante el siguiente método en el controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
    public Reponse getCreneauById(@PathVariable("id") long id) {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // we get the slot back
        Reponse réponse = getCreneau(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
        }
        // result
        return réponse;
}

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


    private Reponse getCreneau(long id) {
        // we get the slot back
        Creneau créneau = null;
        try {
            créneau = application.getCreneauById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // existing niche?
        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 ranura es incorrecto:

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

La URL [/getRvById/{id}] se gestiona mediante el siguiente método en el controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
    public Reponse getRvById(@PathVariable("id") long id) {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // recovering the rv
        Reponse réponse = getRv(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
        }
        // result
        return réponse;
}

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


    private Reponse getRv(long id) {
        // we recover the Rv
        Rv rv = null;
        try {
            rv = application.getRvById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // Existing Rv?
        if (rv == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, rv);
}

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


// Rv --> Map
    public static Map<String, Object> getMapForRv2(Rv rv) {
        // qq chose à faire ?
        if (rv == null) {
            return null;
        }
        // dictionnaire <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());
        // on rend le dictionnaire
        return hash;
    }

Los resultados son los siguientes:

o estos si el ID de la cita es incorrecto:

2.12.16. La URL [/ajouterRv]

La URL [/ajouterRv] se gestiona mediante el siguiente método en el controlador [RdvMedecinsController]:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // retrieve posted values
        String jour = post.getJour();
        long idCreneau = post.getIdCreneau();
        long idClient = post.getIdClient();
        // check the date
        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);
        }
        // we get the slot back
        Reponse réponse = getCreneau(idCreneau);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Creneau créneau = (Creneau) réponse.getData();
        // we get the customer back
        réponse = getClient(idClient);
        if (réponse.getStatus() != 0) {
            réponse.incrStatusBy(2);
            return réponse;
        }
        Client client = (Client) réponse.getData();
        // we add the Rv
        Rv rv = null;
        try {
            rv = application.ajouterRv(jourAgenda, créneau, client);
        } catch (Exception e1) {
            return new Reponse(5, Static.getErreursForException(e1));
        }
        // we return the answer
        return new Reponse(0, Static.getMapForRv(rv));
    }

Aquí no hay nada que no hayamos visto antes. En la línea 41, devolvemos la cita que se añadió en la línea 36.

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

o así si, por ejemplo, introducimos un número de slot que no existe:

2.12.17. La URL [/deleteAppointment]

La URL [/deleteAppointment] se gestiona mediante el siguiente método en el controlador [RdvMedecinsController]:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // retrieve posted values
        long idRv = post.getIdRv();
        // recovering the rv
        Reponse réponse = getRv(idRv);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        // rv deletion
        try {
            application.supprimerRv(idRv);
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // ok
        return new Reponse(0, null);
    }

Los archivos <a id="supprimerrv"></a> resultantes son los siguientes:

o estos si el ID de la cita no existe:

Ya hemos terminado con el controlador. Ahora veamos 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: Establecemos el modo en [AutoConfiguration] para que Spring Boot pueda configurar el proyecto basándose en los archivos que encuentre en la ruta de clases del proyecto;
  • línea 10: especificamos que los componentes de Spring deben buscarse en el paquete [rdvmedecins.web] y sus subpaquetes. Así es como se detectarán los siguientes componentes:
    • [@RestController RdvMedecinsController] en el paquete [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] en el paquete [rdvmedecins.web.models];
  • Línea 11: Importamos la clase [DomainAndPersistenceConfig], que configura el proyecto [rdvmedecins-metier-dao] para proporcionar acceso a los beans de dicho 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);
    }
}

Línea 10: El método estático [SpringApplication.run] se ejecuta con la clase de configuración del proyecto [AppConfig] como primer parámetro. Este método configurará automáticamente el proyecto, iniciará el servidor Tomcat integrado en las dependencias e implementará el controlador [RdvMedecinsController] en él.

Los registros durante la 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 [/**] onto handler of type [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 of type [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 [lógica de negocio, DAO, JPA];
  • línea 34: se ha detectado el método que gestiona la URL [/getRvMedecinJour/{idMedecin}/{jour}]. Este proceso de detección de 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 de los clientes web;

Ahora disponemos de un servicio web operativo al que puede acceder 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, un componente del ecosistema Spring.

2.13. Introducción a Spring Security

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

  

El proyecto consta de los siguientes elementos:

  • en la carpeta [templates], encontrarás 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 ver 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 marco [Thymeleaf], que permite la creación de páginas HTML dinámicas. Este marco puede sustituir a JSP (Java Server Pages), que hasta hace poco era el marco de vista predeterminado para Spring MVC;
  • líneas 12–15: dependencia del marco Spring Security;

2.13.2. 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 enviar la página HTML al cliente. El cliente 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 solicitud HTTP actual. Se trata del nombre del usuario que ha iniciado sesión;
  • 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 «¡Hola [[${#httpServletRequest.remoteUser}]]!»;
  • línea 9: la traducción de @{/logout};
  • línea 11: un campo oculto llamado (atributo name) _csrf;

La vista final [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}"] garantiza que la etiqueta <div> solo se generará si la 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}"] garantiza que la etiqueta <div> solo se generará si la 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 la 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>

Observa en la línea 21 que Thymeleaf ha añadido un campo oculto llamado [_csrf].

2.13.3. Configuración de Spring MVC

  

La clase [MvcConfig] configura el marco 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 a la clase [MvcConfig] en una clase de configuración;
  • línea 8: la clase [MvcConfig] extiende la clase [WebMvcConfigurerAdapter] para sobrescribir ciertos 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 direcciones URL a vistas HTML. Allí se establecen las siguientes asociaciones:
URL
vista
/, /home
/plantillas/inicio.html
/hola
/plantillas/hola.html
/inicio de sesión
/plantillas/inicio-sesión.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:

En [1] arriba, las carpetas [main] y [resources] son ambas carpetas de origen. Esto significa que su contenido estará en la raíz de la ruta de clases del proyecto. Por lo tanto, en [2], las carpetas [hello] y [templates] estarán en la raíz de la ruta de clases.

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 a la clase [WebSecurityConfig] en una clase de configuración;
  • línea 10: la anotación [@EnableWebSecurity] convierte a la clase [WebSecurityConfig] en una clase de configuración de Spring Security;
  • línea 11: la clase [WebSecurity] extiende la clase [WebSecurityConfigurerAdapter] para sobrescribir ciertos métodos;
  • línea 12: redefinición de un método de la clase padre;
  • líneas 13-16: se sobrescribe el método [configure(HttpSecurity http)] para definir los derechos de acceso a las distintas URL de la aplicación;
  • línea 14: el método [http.authorizeRequests()] permite asociar direcciones URL a derechos de acceso. Allí se establecen las siguientes asociaciones:
URL
regla
código
/, /home
acceso sin autenticación

http.authorizeRequests().antMatchers("/", "/home").permitAll()
otras URL
solo acceso autenticado
http.anyRequest().authenticated();
  • Línea 15: define el método de autenticación. La autenticación se realiza a través de un formulario URL [/login] accesible para todos [http.formLogin().loginPage("/login").permitAll()]. El cierre de sesión 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 utilizando usuarios codificados de forma fija [auth.inMemoryAuthentication()]. Aquí se define un usuario con el nombre de usuario [user], la contraseña [password] y el rol [USER]. A los usuarios con el mismo rol se les pueden conceder los mismos permisos;

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] indica a Spring Boot (línea 3) que realice la configuración que el desarrollador no ha establecido explícitamente;
  • línea 9: convierte la clase [Application] en una clase de configuración de Spring;
  • línea 10: indica al sistema que explore el directorio que contiene la clase [Application] para buscar componentes de Spring. De este modo, se detectarán las dos clases [MvcConfig] y [WebSecurityConfig], ya que cuentan con 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. Vimos que se gestionaban cuatro URL [/, /home, /login, /hello] y que algunas estaban protegidas por derechos de acceso.

2.13.6. Prueba de la aplicación

Comencemos solicitando la URL [/], que es una de las cuatro URL aceptadas. Está asociada a la vista [/templates/home.html]:

 

La URL solicitada [/] es accesible para todo el mundo. Por eso hemos podido recuperarla. El enlace [aquí] es el siguiente:

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

Al hacer clic en el enlace, se solicitará la URL [/hello]. Esta está protegida:

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

http.authorizeRequests().antMatchers("/", "/home").permitAll()
otras URL
solo acceso autenticado
http.anyRequest().authenticated();

Debes estar autenticado para acceder a ella. Spring Security redirigirá entonces el navegador del cliente a la página de autenticación. Según la configuración mostrada, esta es la página con la URL [/login]. Esta página es accesible para todo el mundo:


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

Así que lo conseguimos [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 original [login.html]. Thymeleaf lo ha añadido. Este código, conocido como CSRF (Cross-Site Request Forgery), está diseñado para eliminar una vulnerabilidad de seguridad. Este token debe enviarse de vuelta a Spring Security junto con la autenticación para que sea aceptado;

Recordemos que Spring Security solo reconoce el par usuario/contraseña. 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 la 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 de usuario y contraseña esperados [4]:

  • en [4], iniciamos sesión;
  • en [5], Spring Security nos redirige a la URL [/hello] porque esa es la URL que solicitamos 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 [Cerrar sesión], se envía una solicitud POST a la URL [/logout]. Al igual que la URL [/login], esta URL es accesible para todo el mundo:


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

En nuestra asignación de URL/vistas, no hemos definido nada para la URL [/logout]. ¿Qué pasará? Probémoslo:

  • En [6], hacemos clic en el botón [Sign Out];
  • en [7], vemos que hemos sido redirigidos a la URL [http://localhost:8080/login?logout]. Spring Security solicitó esta redirección. La presencia del parámetro [logout] en la URL hizo que se mostrara la siguiente línea en 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 en una aplicación web que ya haya sido escrita. Además, hemos descubierto los siguientes puntos:

  • 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 le redirige a la página de autenticación con un parámetro de error adicional en la 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 la página de autenticación directamente sin pasar por una página intermedia, Spring Security redirige a la URL [/] (este caso no se ha demostrado);
  • Para cerrar sesión, se solicita la URL [/logout] con una solicitud POST. A continuación, Spring Security redirige a la página de autenticación con el parámetro «logout» en la URL;

Todas estas conclusiones se basan en el comportamiento predeterminado de Spring Security. Este comportamiento se puede modificar mediante la configuración, sobrescribiendo determinados métodos de la clase [WebSecurityConfigurerAdapter].

El tutorial anterior nos será de poca ayuda de aquí en adelante. De hecho, utilizaremos:

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

Hay muy pocos tutoriales disponibles sobre lo que queremos hacer aquí. La solución que propondremos es una combinación de fragmentos de código encontrados aquí y allá.

2.14. Implementación de la seguridad para el servicio de citas online

2.14.1. La base de datos

La base de datos [rdvmedecins] se está actualizando para incluir a los usuarios, sus contraseñas y sus roles. Se han añadido tres nuevas tablas:

Image

Tabla [USERS]: usuarios

  • ID: clave principal;
  • VERSION: columna de control de versiones de las filas;
  • IDENTITY: un identificador descriptivo del usuario;
  • LOGIN: el nombre de usuario;
  • PASSWORD: su contraseña;

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

 

El algoritmo utilizado para cifrar las contraseñas es el algoritmo BCRYPT.

Tabla [ROLES]: roles

  • ID: clave principal;
  • VERSION: columna de control de versiones para 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 incluir 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 las filas;
  • USER_ID: identificador de usuario;
  • ROLE_ID: identificador de un rol;
 

Dado que estamos modificando la base de datos, es necesario modificar todas las capas del proyecto [lógica de negocio, DAO, JPA]:

2.14.2. El nuevo proyecto de Eclipse para [lógica de negocio, DAO, JPA]

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

  • en [1]: el nuevo proyecto;
  • en [2]: los cambios introducidos por la implementación de la seguridad se han agrupado en un único paquete [rdvmedecins.security]. Estos nuevos elementos pertenecen a las capas [JPA] y [DAO], pero por simplicidad los he agrupado en un único paquete.

2.14.3. Las nuevas entidades [JPA]

La capa JPA define tres nuevas entidades:

  

La clase [User] representa 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;
 
    // properties
    private String identity;
    private String login;
    private String password;
 
    // manufacturer
    public User() {
    }
 
    public User(String identity, String login, String password) {
        this.identity = identity;
        this.login = login;
        this.password = password;
    }
 
    // identity
    @Override
    public String toString() {
        return String.format("User[%s,%s,%s]", identity, login, password);
    }
 
    // getters and setters
....
}
  • línea 9: la clase hereda de la clase [AbstractEntity] ya utilizada para las otras entidades;
  • líneas 13–15: no se especifican nombres de columna porque tienen los mismos nombres que sus campos asociados;

La clase [Role] refleja 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;
 
    // properties
    private String name;
 
    // manufacturers
    public Role() {
    }
 
    public Role(String name) {
        this.name = name;
    }
 
    // identity
    @Override
    public String toString() {
        return String.format("Role[%s]", name);
    }
 
    // getters and setters
...
}

La clase [UserRole] representa 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;
 
    // a UserRole refers to a User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // a UserRole refers to a Role
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;
 
    // getters and setters
...
}
  • líneas 15–17: define la clave externa de la tabla [USERS_ROLES] a la tabla [USERS];
  • líneas 19-21: implementan la clave externa de la tabla [USERS_ROLES] a la tabla [ROLES];

2.14.4. Cambios en la capa [DAO]

La capa [DAO] se ha mejorado con tres nuevos [Repositorios]:

  

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> {
 
    // list of user roles identified by id
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);
 
    // list of user roles identified by login and password
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);
 
    // search for a user via login
    User findUserByLogin(String login);
}
  • línea 9: la interfaz [UserRepository] extiende la interfaz [CrudRepository] de Spring Data (línea 4);
  • líneas 12-13: el método [getRoles(User user)] recupera todos los roles de un usuario identificado por su [id]
  • líneas 16-17: igual que arriba, pero para un usuario identificado por su nombre de usuario y contraseña;

La interfaz [RoleRepository] gestiona el acceso a las entidades [Role]:


package rdvmedecins.security;
 
import org.springframework.data.repository.CrudRepository;
 
public interface RoleRepository extends CrudRepository<Role, Long> {
 
    // search for a role by name
    Role findRoleByName(String name);
 
}
  • línea 5: la interfaz [RoleRepository] extiende la interfaz [CrudRepository];
  • línea 8: puedes 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] simplemente extiende la interfaz [CrudRepository] sin añadir ningún método nuevo;

2.14.5. Clases de gestión de usuarios y roles

  

Spring Security requiere 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;
 
    // properties
    private User user;
    private UserRepository userRepository;
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }
 
    // -------------------------interface
    @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 and 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 proporciona detalles sobre ese 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 un tipo 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: iteramos por la lista de roles del usuario de la línea 15 para construir una lista de elementos de tipo [SimpleGrantedAuthority];
  • líneas 38-40: implementamos el método [getPassword] de la interfaz [UserDetails]. Devolvemos la contraseña del usuario de la línea 15;
  • líneas 38-40: implementamos el método [getUserName] de la interfaz [UserDetails]. Devolvemos 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 requiere 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 {
        // search for user via login
        User user = userRepository.findUserByLogin(login);
        // found?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(user, userRepository);
    }
 
}
  • línea 9: la clase será un componente de 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 utilizando su nombre de usuario;
  • líneas 20-22: si no se encuentra al usuario, se lanza una excepción;
  • línea 24: se construye y devuelve un objeto [AppUserDetails]. De hecho, es de 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) {
        // syntax: login password roleName
 
        // three parameters are required
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // parameters are retrieved
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // does the role already exist?
        Role role = roleRepository.findRoleByName(roleName);
        // if it doesn't exist, we create it
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // does the user already exist?
        User user = userRepository.findUserByLogin(login);
        // if it doesn't exist, we create it
        if (user == null) {
            // hash the password with bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // save user
            user = userRepository.save(new User(login, login, crypt));
            // we create the relationship with the role
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // the user already exists - does he/she have the required role?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // if not found, we create the relationship with the role
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }
 
        // closing Spring context
        context.close();
    }
 
}
  • línea 17: la clase espera tres argumentos que definan a un usuario: su nombre de usuario, contraseña y rol;
  • líneas 25-27: se recuperan los tres parámetros;
  • línea 29: el contexto Spring se crea a partir de la clase de configuración [DomainAndPersistenceConfig]. Esta clase ya existía en el proyecto anterior. Debe actualizarse 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: Debes especificar que ahora hay componentes [Repository] en el paquete [rdvmedecins.security];
  • línea 4: debe especificar que ahora hay entidades JPA en el paquete [rdvmedecins.security];

Volvamos al código para crear un usuario:

  • líneas 30-32: recuperamos las referencias de los tres objetos [Repository] que pueden ser útiles para crear el usuario;
  • línea 34: comprobamos si el rol ya existe;
  • líneas 36-38: si no es así, lo creamos en la base de datos. Tendrá un nombre con el formato [ROLE_XX];
  • línea 40: comprobamos si el nombre de usuario ya existe;
  • líneas 42-49: si el nombre de usuario no existe, lo creamos en la base de datos;
  • línea 44: ciframos la contraseña. Aquí utilizamos la clase [BCrypt] de Spring Security (línea 4). Por lo tanto, necesitamos los archivos de este marco. 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 guarda en la base de datos;
  • línea 48: así como la relación que lo vincula a su rol;
  • líneas 51-57: si el usuario ya existe, comprobamos si el rol que queremos asignarle ya se encuentra entre sus roles;
  • Líneas 59–61: Si no se encuentra el rol buscado, se crea una fila en la tabla [USERS_ROLES] para vincular al usuario con su rol;
  • No nos hemos protegido contra posibles excepciones. Esta es una clase auxiliar para crear rápidamente un usuario con un rol.

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

Tabla [USERS]

 

Tabla [ROLES]

 

Tabla [USUARIOS_FUNCIONES]

 

Ahora veamos 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() {
        // user [admin] is retrieved
        User user = userRepository.findUserByLogin("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // check admin / admin role
        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() {
        // user [admin] is retrieved
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // check admin / admin role
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }
 
    // utility method - displays items in a collection
    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. Mostramos todos los usuarios junto con sus roles;
  • líneas 36–46: verificamos que el usuario [admin] tiene la contraseña [admin] y el rol [ROLE_ADMIN] utilizando el [UserRepository];
  • línea 41: [admin] es la contraseña en texto plano. En la base de datos, está cifrada mediante el algoritmo BCrypt. El método [BCrypt.checkpw] comprueba que la contraseña cifrada coincida con la que figura en la base de datos;
  • líneas 48-59: verificamos que el usuario [admin] tiene la contraseña [admin] y el rol [ROLE_ADMIN] utilizando [appUserDetailsService];

Las pruebas se ejecutan 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

Se añadieron las clases necesarias para Spring Security con cambios mínimos en el proyecto original. En resumen:

  • añadir 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 componentes de Spring en el paquete [rdvmedecins.security];

Este escenario tan favorable se debe a que las tres tablas añadidas a la base de datos son independientes de las tablas existentes. Incluso podríamos haberlas colocado en una base de datos separada. Esto fue posible porque decidimos que un usuario existe independientemente de los médicos y los clientes. Si estos últimos hubieran sido usuarios potenciales, habríamos tenido que crear enlaces entre la tabla [USERS] y las tablas [MEDECINS] y [CLIENTS]. Esto habría tenido un impacto significativo en el proyecto existente.

2.14.8. El proyecto Eclipse para la capa [web]

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

Los únicos cambios que hay que realizar se encuentran 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: define una clase que extiende la clase [WebSecurityConfigurerAdapter];
  • línea 13: definir un método [configure(HttpSecurity http)] que defina los derechos de acceso a las distintas URL del servicio web;
  • línea 19: definir un método [configure(AuthenticationManagerBuilder auth)] que defina a los usuarios y sus roles;

La configuración de Spring Security se gestiona 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 {
        // authentication is performed by bean [appUserDetailsService]
        // the password is encrypted using the Bcrypt hash algorithm
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // the password is transmitted by the header Authorization: Basic xxxx
        http.httpBasic();
        // only the ADMIN role can use the application
        http.authorizeRequests() //
                .antMatchers("/", "/**") // all URL
                .hasRole("ADMIN");
    }
}
  • líneas 14-15: hemos reutilizado las anotaciones del ejemplo;
  • líneas 17-18: se inyecta la clase [AppUserDetails], que proporciona 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. Toma como parámetro un tipo [AuthenticationManagerBuilder]. Este parámetro se enriquece con dos datos:
    • una referencia al [appUserDetailsService] de la línea 18, que proporciona acceso a los usuarios registrados. Tenga en cuenta aquí que no se indica explícitamente que estén almacenados en una base de datos. Por lo tanto, podrían estar en una caché, ser proporcionados por un servicio web, etc.
    • el tipo de cifrado utilizado para la contraseña. Recordemos que utilizamos el algoritmo BCrypt;
  • líneas 27-40: el método [configure(HttpSecurity http)] define los derechos de acceso a las URL del servicio web;
  • línea 30: vimos en el proyecto introductorio que, por defecto, Spring Security gestiona un token CSRF (Cross-Site Request Forgery) que el usuario que desee autenticarse debe enviar de vuelta al servidor. Aquí, este mecanismo está desactivado;
  • línea 32: habilitamos la autenticación mediante el encabezado HTTP. El cliente debe enviar el siguiente encabezado HTTP:
Authorization:Basic code

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

Authorization:Basic YWRtaW46YWRtaW4=
  • Líneas 34–36: indican que todas las URL del servicio web son accesibles para los usuarios con el rol [ROLE_ADMIN]. Esto significa que un usuario sin este rol no puede acceder al servicio web;

La clase [AppConfig], que configura toda la aplicación, se actualiza 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 {
 
}
  • El cambio se realiza en la línea 11: especifica que ahora hay dos archivos de configuración que se deben utilizar: [DomainAndPersistenceConfig] y [SecurityConfig].

2.14.9. Pruebas del servicio web

Probaremos el servicio web utilizando el cliente de Chrome [Advanced Rest Client]. Tendremos que especificar el encabezado de autenticación HTTP:

Authorization:Basic code

donde [código] es la cadena codificada en Base64 [nombre de usuario:contraseña]. Para generar este código, puedes utilizar el siguiente programa:

  

package rdvmedecins.helpers;
 
import org.springframework.security.crypto.codec.Base64;
 
public class Base64Encoder {
 
    public static void main(String[] args) {
        // we expect two arguments: login password
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // we retrieve the two arguments
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // encode the string
        byte[] data = Base64.encode(chaîne.getBytes());
        // displays its Base64 encoding
        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 ya es seguro. A continuación, utilizando el cliente de Chrome [Advanced Rest Client], solicitamos la lista de todos los médicos:

  • en [1], solicitamos la URL de los médicos;
  • en [2], utilizando un método GET;
  • en [3], proporcionamos el encabezado de autenticación HTTP. El código [YWRtaW46YWRtaW4=] es la codificación Base64 de la cadena [admin:admin];
  • en [4], enviamos la solicitud 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.

Ahora probemos una solicitud HTTP con un encabezado de autenticación incorrecto. La respuesta es 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. 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. Difiere de la anterior, que era [401 No autorizado]. En esta ocasión, el usuario se ha autenticado correctamente, pero no tiene permisos suficientes para acceder a la URL;

2.15. Conclusión

Repasemos la arquitectura general de nuestra aplicación cliente/servidor:

Ya tenemos en funcionamiento un servicio web seguro. Veremos que habrá que modificarlo debido a problemas que surgirán durante el desarrollo del cliente Angular JS. Pero esperaremos hasta encontrarnos con el problema para resolverlo. Ahora crearemos el cliente Angular que proporcionará una interfaz web para gestionar las citas médicas.