Skip to content

2. O servidor Spring 4

Na arquitetura acima, abordaremos agora a construção do serviço web / JSON criado com o framework Spring 4. Iremos escrevê-lo em várias etapas:

  • primeiro as camadas [business] e [DAO] (Data Access Object). Iremos utilizar o Spring Data aqui;
  • depois, o serviço web JSON sem autenticação. Aqui, utilizaremos o Spring MVC;
  • depois, adicionaremos o componente de autenticação utilizando o Spring Security.

Começaremos por explicar a estrutura da base de dados subjacente à aplicação.

2.1. A base de dados

A base de dados, doravante designada por [ dbrdvmedecins], é uma base de dados MySQL5 com as seguintes tabelas:

  

As consultas são geridas pelas seguintes tabelas:

  • [médicos]: contém a lista de médicos do consultório;
  • [clientes]: contém a lista de pacientes da clínica;
  • [slots]: contém os horários disponíveis para cada médico;
  • [rv]: contém a lista de consultas dos médicos.

As tabelas [roles], [users] e [users_roles] estão relacionadas com a autenticação. Por enquanto, não vamos abordá-las.

As relações entre as tabelas que gerem as consultas são as seguintes:

 
  • um horário pertence a um médico – um médico tem 0 ou mais horários;
  • uma consulta liga um cliente a um médico através do intervalo de tempo do médico;
  • um cliente tem 0 ou mais consultas;
  • um intervalo de tempo está associado a 0 ou mais consultas (em dias diferentes).

2.1.1. A tabela [MEDECINS]

Contém informações sobre os médicos geridos pela aplicação [RdvMedecins].

  • ID: Número de identificação do médico — chave primária da tabela
  • VERSION: Número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
  • LAST_NAME: o apelido do médico
  • FIRST_NAME: o nome próprio do médico
  • TITLE: o seu título (Sra., Sra., Sr.)

2.1.2. A tabela [CLIENTS]

Os clientes dos vários médicos estão armazenados na tabela [CLIENTS]:

  • ID: número de identificação do cliente - chave primária da tabela
  • VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
  • APELIDO: o apelido do cliente
  • NOME: o nome do cliente
  • TÍTULO: o seu título (Sra., Sra., Sr.)

2.1.3. A tabela [SLOTS]

Apresenta os horários disponíveis para marcação de consultas:

  • ID: Número de identificação do intervalo de tempo - chave primária da tabela (linha 8)
  • VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
  • DOCTOR_ID: Número de identificação do médico a quem este intervalo de tempo pertence – chave estrangeira na coluna DOCTORS(ID).
  • START_TIME: Hora de início do intervalo de tempo
  • MSTART: Minutos de início do intervalo de tempo
  • HFIN: hora de fim do intervalo
  • MFIN: minutos de fim do intervalo

A segunda linha da tabela [SLOTS] (ver [1] acima) indica, por exemplo, que o intervalo n.º 2 começa às 8h20 e termina às 8h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER).

2.1.4. A tabela [RV]

Apresenta a lista de consultas marcadas para cada médico:

  • ID: identificador único da consulta – chave primária
  • DAY: dia da consulta
  • SLOT_ID: intervalo horário da consulta – chave estrangeira no campo [ID] da tabela [SLOTS] – determina tanto o intervalo horário como o médico envolvido.
  • CLIENT_ID: ID do cliente para quem a reserva é feita – chave estrangeira no campo [ID] da tabela [CLIENTS]

Esta tabela possui uma restrição de exclusividade nos valores das colunas associadas (DAY, SLOT_ID):

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

Se uma linha na tabela [RV] tiver o valor (DAY1, SLOT_ID1) para as colunas (DAY, SLOT_ID), este valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que foram marcadas duas consultas ao mesmo tempo para o mesmo médico. Do ponto de vista da programação Java, o controlador JDBC da base de dados lança uma SQLException quando isto ocorre.

A linha com ID igual a 3 (ver [1] acima) significa que foi marcada uma consulta para o horário n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [SLOTS] indica-nos que o horário n.º 20 corresponde ao intervalo horário das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] indica-nos que o cliente n.º 4 é a Sra. Brigitte BISTROU.

2.2. Introdução ao Spring Data

Iremos implementar a camada [DAO] do projeto utilizando o Spring Data, um ramo do ecossistema Spring.

O site do Spring oferece vários tutoriais para começar a utilizar o Spring [http://spring.io/guides]. Iremos utilizar um deles para apresentar o Spring Data. Para tal, iremos utilizar o Spring Tool Suite (STS).

  • Em [1], importamos um dos tutoriais de [spring.io/guides];
  • Em [2], selecionamos o tutorial [Acessar dados com JPA], que demonstra como aceder a uma base de dados utilizando o Spring Data;
  • Em [3], selecionamos um projeto configurado pelo Maven;
  • em [4], o tutorial está disponível em duas formas: [initial], que é uma versão vazia que preenche seguindo o tutorial, ou [complete], que é a versão final do tutorial. Escolhemos a última;
  • Em [5], pode optar por visualizar o tutorial num navegador;
  • Em [6], o projeto final.

2.2.1. A configuração Maven do projeto

As dependências Maven do projeto estão configuradas no ficheiro [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>
  • linhas 5–9: definem um projeto Maven pai. Este projeto define a maioria das dependências do projeto. Estas podem ser suficientes, caso em que não são adicionadas dependências adicionais, ou podem não ser, caso em que as dependências em falta são adicionadas;
  • linhas 12–15: definem uma dependência do [spring-boot-starter-data-jpa]. Este artefacto contém as classes Spring Data;
  • Linhas 16–19: definem uma dependência do SGBD H2, que permite criar e gerir bases de dados na memória.

Vejamos as classes fornecidas por estas dependências:

Existem muitos:

  • algumas pertencem ao ecossistema Spring (aquelas que começam por «spring»);
  • outros pertencem ao ecossistema Hibernate (hibernate, jboss), cuja implementação JPA estamos a utilizar aqui;
  • outras são bibliotecas de testes (JUnit, Hamcrest);
  • outras são bibliotecas de registo (log4j, logback, slf4j);

Vamos ficar com todas elas. Para uma aplicação de produção, devemos ficar apenas com as que são necessárias.

Na linha 26 do ficheiro [pom.xml], encontramos a linha:


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

Esta linha está ligada às seguintes linhas:


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

Linhas 6–9: O [spring-boot-maven-plugin] permite gerar o JAR executável da aplicação. A linha 26 do ficheiro [pom.xml] especifica então a classe executável deste JAR.

2.2.2. A camada [JPA]

O acesso à base de dados é tratado através de uma camada [JPA], a Java Persistence API:

  

A aplicação é básica e gere entidades [Cliente]. A classe [Cliente] faz parte da camada [JPA] e é a seguinte:


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);
    }
 
}

Um cliente tem um ID [id], um nome próprio [firstName] e um apelido [lastName]. Cada instância [Customer] representa uma linha numa tabela de base de dados.

  • linha 8: Anotação JPA que garante que a persistência das instâncias [Customer] (Criar, Ler, Atualizar, Eliminar) será gerida por uma implementação JPA. Com base nas dependências do Maven, podemos ver que está a ser utilizada a implementação JPA/Hibernate;
  • Linhas 11–12: Anotações JPA que associam o campo [id] à chave primária da tabela [Customer]. A linha 12 indica que a implementação JPA utilizará o método de geração de chave primária específico do SGBD em uso, neste caso o H2;

Não existem outras anotações JPA. Serão, portanto, utilizados valores por defeito:

  • a tabela [Customer] receberá o nome da classe, ou seja, [Customer];
  • as colunas desta tabela receberão nomes baseados nos campos da classe: [id, firstName, lastName], observando-se que as maiúsculas e minúsculas não são levadas em conta nos nomes das colunas da tabela;

Note-se que a implementação JPA utilizada nunca é nomeada.

2.2.3. A camada [DAO]

  

A classe [CustomerRepository] implementa a camada [DAO]. O seu código é o seguinte:


package hello;
 
import java.util.List;
 
import org.springframework.data.repository.CrudRepository;
 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
 
    List<Customer> findByLastName(String lastName);
}

Trata-se, portanto, de uma interface e não de uma classe (linha 7). Ela estende a interface [CrudRepository], uma interface do Spring Data (linha 5). Esta interface é parametrizada por dois tipos: o primeiro é o tipo dos elementos geridos, neste caso o tipo [Customer]; o segundo é o tipo da chave primária dos elementos geridos, neste caso um tipo [Long]. A interface [CrudRepository] é a seguinte:


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 interface define as operações CRUD (Criar – Ler – Atualizar – Eliminar) que podem ser realizadas num tipo JPA T:

  • Linha 8: O método `save` é utilizado para persistir uma entidade `T` na base de dados. Ele persiste a entidade utilizando a chave primária que lhe foi atribuída pelo SGBD. Também permite atualizar uma entidade `T` identificada pela sua chave primária `id`. A escolha entre estas duas ações depende do valor da chave primária `id`: se for nulo, ocorre a operação de persistência; caso contrário, ocorre a operação de atualização;
  • linha 10: igual ao acima, mas para uma lista de entidades;
  • linha 12: o método findOne recupera uma entidade T identificada pela sua chave primária id;
  • linha 22: o método delete permite eliminar uma entidade T identificada pela sua chave primária id;
  • linhas 24–28: variações do método [delete];
  • linha 16: o método [findAll] recupera todas as entidades T persistidas;
  • linha 18: igual ao anterior, mas limitado às entidades para as quais foi fornecida uma lista de identificadores;

Voltemos à interface [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);
}
  • A linha 9 permite-lhe recuperar um [Cliente] pelo seu [apelido];

E é tudo quanto à camada [DAO]. Não existe uma classe de implementação para a interface anterior. Esta é gerada em tempo de execução pelo [Spring Data]. Os métodos da interface [CrudRepository] são implementados automaticamente. Quanto aos métodos adicionados à interface [CustomerRepository], depende. Voltemos à definição de [Customer]:


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

O método na linha 9 é implementado automaticamente pelo [Spring Data] porque faz referência ao campo [lastName] (linha 3) de [Customer]. Quando encontra um método [findBySomething] na interface a ser implementada, o Spring Data implementa-o utilizando a seguinte consulta JPQL (Java Persistence Query Language):

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

Portanto, o tipo T deve ter um campo chamado [something]. Assim, o método

List<Customer> findByLastName(String lastName);

será implementado com código semelhante ao seguinte:

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

onde [em] se refere ao contexto de persistência JPA. Isto só é possível se a classe [Customer] tiver um campo chamado [lastName], o que é o caso.

Em conclusão, em casos simples, o Spring Data permite-nos implementar a camada [DAO] com uma interface simples.

2.2.4. A camada [console]

  

A classe [Application] é a seguinte:


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();
    }
 
}
  • Linha 10: indica que a classe é utilizada para configurar o Spring. As versões recentes do Spring podem, de facto, ser configuradas em Java em vez de em XML. Ambos os métodos podem ser utilizados simultaneamente. No código de uma classe anotada com [Configuration], encontramos normalmente beans do Spring, ou seja, definições de classes a instanciar. Aqui, não estão definidos quaisquer beans. É importante notar aqui que, ao trabalhar com um SGBD, devem ser definidos vários beans do Spring:
    • um [EntityManagerFactory] que define a implementação JPA a utilizar,
    • um [DataSource] que define a fonte de dados a utilizar,
    • um [TransactionManager] que define o gestor de transações a utilizar;

Aqui, nenhum destes feijões está definido.

  • Linha 11: A anotação [EnableAutoConfiguration] é uma anotação do projeto [Spring Boot] (linhas 5–6). Esta anotação instrui o Spring Boot, através da classe [SpringApplication] (linha 16), a configurar a aplicação com base nas bibliotecas encontradas no seu classpath. Como as bibliotecas do Hibernate estão no classpath, o bean [entityManagerFactory] será implementado utilizando o Hibernate. Como a biblioteca do SGBD H2 está no classpath, o bean [dataSource] será implementado utilizando o H2. No bean [dataSource], devemos também definir o nome de utilizador e a palavra-passe. Aqui, o Spring Boot utilizará o administrador H2 predefinido, que não tem palavra-passe. Como a biblioteca [spring-tx] está no Classpath, será utilizado o gestor de transações do Spring.

Além disso, o diretório que contém a classe [Application] será verificado em busca de beans reconhecidos implicitamente pelo Spring ou definidos explicitamente por anotações do Spring. Assim, as classes [Customer] e [CustomerRepository] serão inspecionadas. Como a primeira possui a anotação [@Entity], será catalogada como uma entidade a ser gerida pelo Hibernate. Como a segunda estende a interface [CrudRepository], será registada como um bean do Spring.

Vamos examinar as linhas 16–17 do código:


ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
  • Linha 1: O método estático [run] da classe [SpringApplication] no projeto Spring Boot é executado. O seu parâmetro é a classe que possui uma anotação [Configuration] ou [EnableAutoConfiguration]. Tudo o que foi explicado anteriormente irá então ocorrer. O resultado é um contexto de aplicação Spring, ou seja, um conjunto de beans geridos pelo Spring;
  • linha 17: solicitamos um bean que implementa a interface [CustomerRepository] a partir deste contexto Spring. Aqui, recuperamos a classe gerada pelo Spring Data para implementar esta interface.

As operações seguintes utilizam simplesmente os métodos do bean que implementa a interface [CustomerRepository]. Repare que, na linha 50, o contexto é fechado. A saída da consola é a seguinte:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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
  • linhas 1-8: o logótipo do projeto Spring Boot;
  • linha 9: a classe [hello.Application] é executada;
  • linha 10: [AnnotationConfigApplicationContext] é uma classe que implementa a interface [ApplicationContext] do Spring. É um contentor de beans;
  • linha 11: o bean [entityManagerFactory] é implementado utilizando a classe [LocalContainerEntityManagerFactory], uma classe do Spring;
  • linha 12: aparece [hibernate]. Esta é a implementação JPA que foi escolhida;
  • linha 19: um dialeto Hibernate é a variante SQL a ser utilizada com o SGBD. Aqui, o dialeto [H2Dialect] indica que o Hibernate irá funcionar com o SGBD H2;
  • linhas 22–24: a tabela [CUSTOMER] é criada. Isto significa que o Hibernate foi configurado para gerar tabelas a partir de definições JPA, neste caso a definição JPA da classe [Customer];
  • linhas 27–32: registos do Hibernate a mostrar a inserção de linhas na tabela [CUSTOMER]. Isto significa que o Hibernate foi configurado para gerar registos;
  • linhas 35–39: os cinco clientes inseridos;
  • linhas 42–44: resultado do método [findOne] da interface;
  • linhas 47–50: resultados do método [findByLastName];
  • linhas 51 e seguintes: registos do encerramento do contexto Spring.

2.2.5. Configuração manual do projeto Spring Data

Duplicamos o projeto anterior para o projeto [gs-accessing-data-jpa-2]:

  

Neste novo projeto, não vamos recorrer à configuração automática fornecida pelo Spring Boot. Vamos configurá-lo manualmente. Isto pode ser útil se as configurações predefinidas não corresponderem às nossas necessidades.

Primeiro, vamos especificar as dependências necessárias no ficheiro [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>
  • linhas 3–17: Bibliotecas principais do Spring;
  • linhas 19–28: Bibliotecas Spring para gerir transações de base de dados;
  • linhas 30–34: Spring Data utilizado para aceder à base de dados;
  • linhas 36–40: Spring Boot para iniciar a aplicação;
  • linhas 48–52: o SGBD H2;
  • linhas 54–63: Os bancos de dados são frequentemente utilizados com pools de conexões abertas, o que evita a abertura e o fechamento repetidos de conexões. Aqui, a implementação utilizada é a do [commons-dbcp];

Ainda no [pom.xml], alteramos o nome da classe executável:


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

No novo projeto, a entidade [Customer] e a interface [CustomerRepository] permanecem inalteradas. Iremos modificar a classe [Application], que será dividida em duas classes:

  • [Config], que será a classe de configuração:
  • [Main], que será a classe executável;
  

A classe executável [Main] é a mesma de antes, sem as anotações de configuração:


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();
    }
 
}
  • linha 12: a classe [Main] já não tem quaisquer anotações de configuração;
  • linha 16: a aplicação é iniciada com o Spring Boot. O parâmetro [Config.class] é a nova classe de configuração do projeto;

A classe [Config] que configura o projeto é a seguinte:


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;
    }
 
}
  • linha 22: a anotação [@Configuration] torna a classe [Config] uma classe de configuração do Spring;
  • linha 21: a anotação [@EnableJpaRepositories] especifica os diretórios onde se encontram as interfaces [CrudRepository] do Spring Data. Estas interfaces tornar-se-ão componentes do Spring e estarão disponíveis no seu contexto;
  • linha 20: a anotação [@EnableTransactionManagement] indica que os métodos das interfaces [CrudRepository] devem ser executados dentro de uma transação;
  • linha 19: a anotação [@EntityScan] especifica os diretórios onde as entidades JPA devem ser pesquisadas. Aqui, ela foi comentada porque esta informação foi explicitamente fornecida na linha 50. Esta anotação deve estar presente se estiver a utilizar o modo [@EnableAutoConfiguration] e as entidades JPA não estiverem no mesmo diretório que a classe de configuração;
  • linha 18: a anotação [@ComponentScan] permite listar os diretórios onde os componentes Spring devem ser pesquisados. Os componentes Spring são classes marcadas com anotações Spring, tais como @Service, @Component, @Controller, etc. Aqui, não há outros além daqueles definidos na classe [Config], pelo que a anotação foi comentada;
  • Linhas 25–33: definem a fonte de dados, a base de dados H2. É a anotação @Bean na linha 25 que torna o objeto criado por este método um componente gerido pelo Spring. O nome do método aqui pode ser qualquer um. No entanto, deve ser nomeado [dataSource] se a EntityManagerFactory na linha 47 estiver ausente e for definida através da configuração automática;
  • linha 29: a base de dados será denominada [demo] e será gerada na pasta do projeto;
  • Linhas 36–43: definem a implementação JPA utilizada, neste caso uma implementação Hibernate. O nome do método aqui pode ser qualquer um;
  • linha 39: sem registos SQL;
  • linha 30: a base de dados será criada se não existir;
  • linhas 46–54: definem o EntityManagerFactory que irá gerir a persistência JPA. O método deve ser denominado [entityManagerFactory];
  • linha 47: o método recebe dois parâmetros dos tipos dos dois beans definidos anteriormente. Estes serão então construídos e injetados pelo Spring como parâmetros do método;
  • linha 49: define a implementação JPA a ser utilizada;
  • linha 50: especifica os diretórios onde as entidades JPA podem ser encontradas;
  • linha 51: define a fonte de dados a ser gerida;
  • linhas 57–62: o gestor de transações. O método deve ser denominado [transactionManager]. Recebe o bean das linhas 46–54 como parâmetro;
  • linha 60: o gestor de transações é associado ao EntityManagerFactory;

Os métodos anteriores podem ser definidos em qualquer ordem.

A execução do projeto produz os mesmos resultados. Um novo ficheiro aparece na pasta do projeto, o ficheiro da base de dados H2:

  

Finalmente, podemos passar sem o Spring Boot. Criamos uma segunda classe executável [Main2]:

  

A classe [Main2] tem o seguinte 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();
    }
 
}
  • Linha 15: A classe de configuração [Config] é agora utilizada pela classe Spring [AnnotationConfigApplicationContext]. Como se pode ver na linha 5, já não existe qualquer dependência do Spring Boot.

A execução produz os mesmos resultados que antes.

2.2.6. Criação de um arquivo executável

Para criar um arquivo executável do projeto, proceda da seguinte forma:

  • em [1]: crie uma configuração de tempo de execução;
  • em [2]: do tipo [Aplicação Java]
  • em [3]: especifique o projeto a executar (utilize o botão Procurar);
  • em [4]: especifique a classe a ser executada;
  • em [5]: o nome da configuração de execução — pode ser qualquer nome;
  • em [6]: exporte o projeto;
  • em [7]: como um arquivo JAR executável;
  • em [8]: especifique o caminho e o nome do ficheiro executável a ser criado;
  • em [9]: o nome da configuração de execução criada em [5];

Depois de fazer isto, abra um terminal na pasta que contém o arquivo executável:

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

O arquivo é executado da seguinte forma:


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

Os resultados apresentados na consola são os seguintes:

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. Criar um novo projeto Spring Data

Para criar um modelo de projeto Spring Data, siga estes passos:

  • Em [1], crie um novo projeto;
  • em [2]: selecione [Spring Starter Project];
  • O projeto gerado será um projeto Maven. Em [3], especifique o nome do grupo do projeto;
  • Em [4], especifique o nome do artefacto (um ficheiro JAR, neste caso) que será criado quando o projeto for compilado;
  • em [5]: especifique o pacote da classe executável que será criada no projeto;
  • em [6]: o nome do projeto no Eclipse – pode ser qualquer coisa (não tem de ser igual ao de [4]);
  • em [7]: especifique que está a criar um projeto com uma camada [JPA]. As dependências necessárias para tal projeto serão então incluídas no ficheiro [pom.xml];
  • em [8]: o projeto criado;

O ficheiro [pom.xml] inclui as dependências necessárias para um projeto 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>
  • linhas 9–12: dependências necessárias para o JPA — incluirão [Spring Data];
  • linhas 13–17: dependências necessárias para testes JUnit integrados com o Spring;

A classe executável [Application] não faz nada, mas está pré-configurada:


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);
    }
}

A classe de teste [ApplicationTests] não faz nada, mas está pré-configurada:


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() {
    }
 
}
  • linha 9: a anotação [@SpringApplicationConfiguration] permite que o ficheiro de configuração [Application] seja utilizado. A classe de teste irá, assim, beneficiar de todos os beans definidos neste ficheiro;
  • linha 8: a anotação [@RunWith] permite a integração do Spring com o JUnit: a classe pode ser executada como um teste JUnit. [@RunWith] é uma anotação do JUnit (linha 4), enquanto a classe [SpringJUnit4ClassRunner] é uma classe do Spring (linha 6);

Agora que temos um esqueleto de aplicação JPA, podemos completá-lo para escrever a camada de persistência do lado do servidor da nossa aplicação de gestão de compromissos.

2.3. O projeto de servidor do Eclipse

  

Os principais componentes do projeto são os seguintes:

  • [pom.xml]: o ficheiro de configuração Maven do projeto;
  • [rdvmedecins.entities]: as entidades JPA;
  • [rdvmedecins.repositories]: interfaces Spring Data para aceder às entidades JPA;
  • [rdvmedecins.metier]: a camada [de negócio];
  • [rdvmedecins.domain]: as entidades geridas pela camada [de negócio];
  • [rdvmdecins.config]: as classes de configuração da camada de persistência;
  • [rdvmedecins.boot]: uma aplicação de consola básica;

2.4. A configuração do Maven

O ficheiro [pom.xml] do projeto é o seguinte:


<?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>
  • linhas 8–12: O projeto depende do projeto pai [spring-boot-starter-parent]. Para as dependências já presentes no projeto pai, não é especificada nenhuma versão. Será utilizada a versão definida no projeto pai. As outras dependências são declaradas como habitualmente;
  • linhas 14–17: para o Spring Data;
  • linhas 18–22: para testes JUnit;
  • linhas 23–26: controlador JDBC para o SGBD MySQL5;
  • linhas 27–34: pool de conexões Commons DBCP;
  • linhas 35–38: biblioteca Jackson para tratamento de JSON;
  • linhas 39–43: biblioteca Google Collections;

A versão 1.1.0.RC1 do [spring-boot-starter-parent] utiliza as seguintes versões 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

As entidades JPA são os objetos que encapsulam as linhas nas tabelas da base de dados.

  

A classe [AbstractEntity] é a classe pai das entidades [Person, Slot, Appointment]. A sua definição é a seguinte:


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
    ..
}
  • linha 11: a anotação [@MappedSuperclass] indica que a classe anotada é uma superclasse das entidades JPA [@Entity];
  • linhas 15–17: definem a chave primária [id] para cada entidade. É a anotação [@Id] que torna o campo [id] uma chave primária. A anotação [@GeneratedValue(strategy = GenerationType.AUTO)] indica que o valor desta chave primária é gerado pelo SGBD e que não é imposto nenhum modo de geração;
  • Linhas 18–19: definem a versão de cada entidade. A implementação JPA incrementará este número de versão sempre que a entidade for modificada. Este número é utilizado para impedir atualizações simultâneas da entidade por dois utilizadores diferentes: dois utilizadores, U1 e U2, leem a entidade E com um número de versão igual a V1. U1 modifica E e persiste esta alteração na base de dados: o número de versão passa então a V1+1. U2, por sua vez, modifica E e persiste essa alteração na base de dados: receberá uma exceção porque a sua versão (V1) difere da que se encontra na base de dados (V1+1);
  • linhas 29–33: o método [build] inicializa os dois campos de [AbstractEntity]. Este método devolve uma referência à instância de [AbstractEntity] assim inicializada;
  • linhas 36–44: o método [equals] da classe é redefinido: duas entidades são consideradas iguais se tiverem o mesmo nome de classe e o mesmo identificador id;

A entidade [Person] é a classe pai das entidades [Doctor] e [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
    ...
}
  • linha 6: a anotação [@MappedSuperclass] indica que a classe anotada é uma superclasse das entidades JPA [@Entity];
  • linhas 10–15: uma pessoa tem um título (Sra.), um nome próprio (Jacqueline) e um apelido (Tatou). Não é fornecida qualquer informação sobre as colunas da tabela. Por predefinição, estas terão, portanto, os mesmos nomes que os campos;

A entidade [Medecin] é a seguinte:


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());
    }
 
}
  • linha 6: a classe é uma entidade JPA;
  • linha 7: associada à tabela [DOCTORS] na base de dados;
  • linha 8: a entidade [Doctor] deriva da entidade [Person];

Um médico pode ser inicializado da seguinte forma:

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

Se também quisermos atribuir-lhe um ID e uma versão, podemos escrever:

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

onde o método [build] é aquele definido em [AbstractEntity].

A entidade [Client] é a seguinte:


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());
    }
 
}
  • linha 6: a classe é uma entidade JPA;
  • linha 7: associada à tabela [CLIENTS] na base de dados;
  • linha 8: a entidade [Client] deriva da entidade [Person];

A entidade [TimeSlot] é a seguinte:


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
    ...
}
  • linha 10: a classe é uma entidade JPA;
  • linha 11: associada à tabela [CRENEAUX] na base de dados;
  • linha 12: a entidade [Creneau] deriva da entidade [AbstractEntity] e, portanto, herda os campos [id] e [version];
  • linha 16: hora de início do intervalo (14);
  • linha 17: minutos de início do intervalo (20);
  • linha 18: hora de fim do intervalo (14);
  • linha 19: minutos de fim do intervalo (40);
  • linhas 22–24: o médico responsável pelo horário. A tabela [CRENEAUX] possui uma chave estrangeira na tabela [MEDECINS]. Esta relação é representada pelas linhas 22–24;
  • Linha 22: A anotação [@ManyToOne] indica uma relação muitos-para-um (slots) para um (médico). O atributo [fetch=FetchType.LAZY] especifica que, quando uma entidade [Creneau] é solicitada a partir do contexto de persistência e tem de ser recuperada da base de dados, a entidade [Medecin] não é recuperada juntamente com ela. A vantagem deste modo é que a entidade [Doctor] só é recuperada se o programador a solicitar. Isto poupa memória e melhora o desempenho;
  • linha 23: especifica o nome da coluna da chave estrangeira na tabela [CRENEAUX];
  • Linhas 27–28: a chave estrangeira na tabela [MEDECINS];
  • linha 27: a coluna [ID_MEDECIN] já foi utilizada na linha 23. Isto significa que pode ser modificada de duas formas diferentes, o que não é permitido pela norma JPA. Por isso, adicionamos os atributos [insertable = false, updatable = false], o que significa que a coluna só pode ser lida;

A entidade [Rv] é a seguinte:


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
...
}
  • linha 14: a classe é uma entidade JPA;
  • linha 15: associada à tabela [RV] na base de dados;
  • linha 16: a entidade [Rv] deriva da entidade [AbstractEntity] e, portanto, herda os campos [id] e [version];
  • linha 21: a data do compromisso;
  • linha 20: o tipo Java [Date] contém tanto uma data como uma hora. Aqui especificamos que apenas a data é utilizada;
  • linhas 24–26: o cliente para quem esta marcação foi feita. A tabela [RV] tem uma chave estrangeira na tabela [CLIENTS]. Esta relação é representada pelas linhas 24–26;
  • linhas 29–31: o intervalo de tempo da marcação. A tabela [RV] possui uma chave estrangeira na tabela [CRENEAUX]. Esta relação é representada pelas linhas 29–31;
  • linhas 34–35: a chave estrangeira [idClient];
  • linhas 36–37: a chave estrangeira [idCreneau];

2.6. A camada [DAO]

Iremos implementar a camada [DAO] utilizando o Spring Data:

  

A camada [DAO] é implementada utilizando quatro interfaces Spring Data:

  • [ClientRepository]: fornece acesso às entidades JPA [Client];
  • [CreneauRepository]: fornece acesso às entidades JPA [Creneau];
  • [MedecinRepository]: fornece acesso às entidades JPA [Medecin];
  • [RvRepository]: fornece acesso às entidades JPA [Rv];

A interface [MedecinRepository] é a seguinte:


package rdvmedecins.repositories;
 
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Medecin;
 
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
  • Linha 7: A interface [MedecinRepository] simplesmente herda os métodos da interface [CrudRepository] sem adicionar nenhum outro;

A interface [ClientRepository] é a seguinte:


package rdvmedecins.repositories;
 
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Client;
 
public interface ClientRepository extends CrudRepository<Client, Long> {
}
  • Linha 7: A interface [ClientRepository] simplesmente herda os métodos da interface [CrudRepository] sem adicionar nenhum outro;

A interface [CreneauRepository] é a seguinte:


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);
}
  • linha 8: a interface [CreneauRepository] herda os métodos da interface [CrudRepository];
  • linhas 10-11: o método [getAllCreneaux] recupera os horários disponíveis de um médico;
  • linha 11: o parâmetro é o ID do médico. O resultado é uma lista de horários na forma de um objeto [Iterable<Creneau>];
  • linha 10: a anotação [@Query] é utilizada para especificar a consulta JPQL (Java Persistence Query Language) que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método;

A interface [RvRepository] é a seguinte:


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);
}
  • Linha 10: A interface [RvRepository] herda os métodos da interface [CrudRepository];
  • Linhas 12–13: O método [getRvMedecinJour] recupera as consultas de um médico para um determinado dia;
  • linha 13: Os parâmetros são o ID do médico e o dia. O resultado é uma lista de consultas na forma de um objeto [Iterable<Rv>];
  • linha 12: a anotação [@Query] permite especificar a consulta JPQL que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método, e o parâmetro [?2] será substituído pelo parâmetro [jour] do método. A seguinte consulta JPQL não é suficiente:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

porque os campos da classe Rv, dos tipos [Client] e [Creneau], são recuperados no modo [FetchType.LAZY], o que significa que têm de ser explicitamente solicitados para serem obtidos. Isto é feito na consulta JPQL utilizando a sintaxe [left join fetch entity], que requer que seja realizada uma junção com a tabela referenciada pela chave estrangeira, a fim de recuperar a entidade referenciada;

2.7. A camada [business]

  
  • [IMetier] é a interface da camada [business] e [Metier] é a sua implementação;
  • [DoctorDailySchedule] e [DoctorDailySlot] são duas entidades de negócio;

2.7.1. As entidades

A entidade [DoctorTimeSlot] associa um intervalo de tempo a qualquer consulta marcada dentro desse 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
...
}
  • linha 12: o intervalo de tempo;
  • linha 13: a consulta, se houver – nulo caso contrário;

A entidade [AgendaMedecinJour] é a agenda de um médico para um determinado dia, ou seja, a lista das suas consultas:


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
...
}
  • linha 13: o médico;
  • linha 14: o dia no calendário;
  • linha 15: os horários disponíveis, com ou sem marcação;

2.7.2. O serviço

A interface da camada [de negócios] é a seguinte:


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);
 
}

Os comentários explicam a função de cada método.

A implementação da interface [IMetier] é a seguinte classe [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) {
    ...
    }
 
}
  • linha 24: a anotação [@Service] é uma anotação Spring que torna a classe anotada um componente gerido pelo Spring. Pode ou não atribuir um nome a um componente. Este tem o nome [business];
  • linha 25: a classe [Metier] implementa a interface [IMetier];
  • linha 28: a anotação [@Autowired] é uma anotação do Spring. O valor do campo anotado desta forma será inicializado (injetado) pelo Spring com a referência a um componente do Spring do tipo ou nome especificado. Aqui, a anotação [@Autowired] não especifica um nome. Portanto, será realizada uma injeção baseada no tipo;
  • linha 29: o campo [medecinRepository] será inicializado com a referência a um componente Spring do tipo [MedecinRepository]. Esta será a referência à classe gerada pelo Spring Data para implementar a interface [MedecinRepository] que já apresentámos;
  • linhas 30–35: este processo é repetido para as outras três interfaces discutidas;
  • linhas 39–41: implementação do método [getAllClients];
  • linha 40: utilizamos o método [findAll] da interface [ClientRepository]. Este método retorna um tipo [Iterable<Client>], que convertemos para um [List<Client>] utilizando o método estático [Lists.newArrayList]. A classe [Lists] está definida na biblioteca Google Guava. No [pom.xml], esta dependência foi importada:

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
  • linhas 38–86: os métodos da interface [IMetier] são implementados utilizando classes da camada [DAO];

Apenas o método na linha 88 é específico da camada [business]. Foi colocado aqui porque executa lógica de negócio que vai além do simples acesso a dados. Sem este método, não haveria razão para criar uma camada [business]. O método [getAgendaMedecinJour] é o seguinte:


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;
    }

Recomenda-se aos leitores que leiam os comentários. O algoritmo é o seguinte:

  • recuperar todos os horários disponíveis para o médico especificado;
  • recuperar todas as suas consultas para o dia especificado;
  • com esta informação, podemos determinar se um intervalo de tempo está disponível ou reservado;

2.8. Configuração do projeto

  

A classe [DomainAndPersistenceConfig] configura todo o projeto:


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
 
}
  • Linha 45: Não definiremos os beans [EntityManagerFactory] e [TransactionManager]. Em vez disso, recorreremos à anotação [@EnableAutoConfiguration] do Spring Boot (linha 17);
  • Linhas 24–32: Defina a fonte de dados MySQL 5. Este é um bean que o Spring Boot geralmente não consegue configurar automaticamente;
  • Linhas 36–43: Também configuramos a implementação JPA para definir o atributo [showSql] do Hibernate como false (linha 39). Por predefinição, está definido como true;
  • Por enquanto, os únicos componentes geridos pelo Spring são os beans nas linhas 25 e 37, além dos beans [EntityManagerFactory] e [TransactionManager] através da configuração automática. Precisamos de adicionar os beans das camadas [business] e [DAO];
  • A linha 16 adiciona as interfaces do pacote [rdvmdecins.repositories] que herdam da interface [CrudRepository] ao contexto do Spring;
  • A linha 18 adiciona ao contexto Spring todas as classes do pacote [rdvmedecins] e suas subclasses que possuem uma anotação Spring. No pacote [rdvmdecins.metier], a classe [Metier] com a sua anotação [@Service] será encontrada e adicionada ao contexto Spring;
  • Linha 45: Um bean [entityManagerFactory] será definido por padrão pelo Spring Boot. Temos de indicar a este bean onde se encontram as entidades JPA que ele precisa de gerir. A linha 19 faz isso;
  • linha 20: especifica que os métodos das interfaces que herdam da interface [CrudRepository] devem ser executados dentro de uma transação;

2.9. Testes para a camada [business]

  

A classe [rdvmedecins.tests.Metier] é uma classe de teste 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);
        }
    }
 
}
  • linha 22: a anotação [@SpringApplicationConfiguration] permite que o ficheiro de configuração [DomainAndPersistenceConfig], discutido anteriormente, seja utilizado. A classe de teste beneficia, assim, de todos os beans definidos por este ficheiro;
  • linha 23: a anotação [@RunWith] permite a integração do Spring com o JUnit: a classe pode ser executada como um teste JUnit. [@RunWith] é uma anotação do JUnit (linha 9), enquanto a classe [SpringJUnit4ClassRunner] é uma classe do Spring (linha 12);
  • Linhas 26–27: Injeção de uma referência à camada [business] na classe de teste;
  • muitos testes são simplesmente testes visuais:
    • linhas 32–33: lista de clientes;
    • linhas 35–36: lista de médicos;
    • linhas 39–40: lista dos horários disponíveis de um médico;
    • linha 43: lista de consultas de um médico;
  • linha 50: adicionar uma nova consulta. O método [addAppt] devolve a consulta com informações adicionais, a sua chave primária id;
  • linha 53: esta chave primária é utilizada para pesquisar a consulta na base de dados;
  • linha 54: verificamos se a consulta que está a ser pesquisada e a consulta encontrada são a mesma. Recorde-se que o método [equals] da entidade [Rv] foi redefinido: duas consultas são iguais se tiverem o mesmo id. Aqui, isto mostra-nos que a consulta adicionada foi de facto inserida na base de dados;
  • Linhas 61–73: Tentamos adicionar o mesmo compromisso uma segunda vez. Isto deve ser rejeitado pelo SGBD porque existe uma restrição de unicidade:

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 ;

A linha 8 acima especifica que a combinação [DAY, SLOT_ID] deve ser única, o que impede que duas marcações sejam agendadas no mesmo intervalo de tempo no mesmo dia.

  • linha 73: verificamos se ocorreu realmente uma exceção;
  • linha 77: recuperamos o calendário do médico para quem acabámos de adicionar um compromisso;
  • linha 79: verificamos se a consulta adicionada está efetivamente presente na agenda;
  • linha 82: eliminamos a consulta adicionada;
  • linha 84: recuperamos a consulta eliminada da base de dados;
  • linha 85: verificamos se recuperámos um ponteiro nulo, indicando que a consulta que procurámos não existe;

O teste é executado com sucesso:

 

2.10. O programa de consola

  

O programa de consola é básico. Ilustra como recuperar uma chave estrangeira:


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);
        }
    }
 
}

O programa adiciona um compromisso e, em seguida, verifica se este foi adicionado.

  • linha 19: a classe [SpringApplication] utilizará a classe de configuração [DomainAndPersistenceConfig];
  • linha 20: supressão dos registos de arranque da aplicação;
  • linha 22: a classe [SpringApplication] é executada. Ela retorna um contexto Spring, ou seja, a lista de beans registados;
  • linha 24: é recuperada uma referência ao bean que implementa a interface [IMetier]. Trata-se, portanto, de uma referência à camada [business];
  • linhas 27–31: Adicionar uma nova consulta para hoje, para o cliente n.º 1 no horário n.º 1. O cliente e o horário foram criados do zero para demonstrar que apenas são utilizados identificadores. Inicializámos a versão aqui, mas poderíamos ter utilizado qualquer valor. Ela não é utilizada aqui;
  • linha 34: queremos saber qual o médico que ocupa o horário n.º 1. Para tal, precisamos de consultar a base de dados para o horário n.º 1. Como estamos no modo [FetchType.LAZY], o médico não é devolvido juntamente com o horário. No entanto, certificámo-nos de incluir um campo [idMedecin] na entidade [Creneau] para recuperar a chave primária do médico;
  • linha 35: recuperamos a chave primária do médico;
  • linha 36: exibimos a lista de consultas do médico;

A saída da consola é a seguinte:

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. Introdução ao Spring MVC

Vamos agora abordar a construção da camada web. Esta camada consiste principalmente em métodos que tratam de URLs específicas e respondem com uma linha de texto no formato JSON (JavaScript Object Notation). Esta camada web é uma interface web, por vezes designada por API web. Iremos implementar esta interface utilizando o Spring MVC, outro componente do ecossistema Spring. Começaremos por rever um dos guias disponíveis em [http://spring.io].

2.11.1. O projeto de demonstração

  • em [1], importamos um dos guias do Spring;
  • em [2], selecionamos o exemplo [Rest Service];
  • em [3], selecionamos o projeto Maven;
  • em [4], selecionamos a versão final do guia;
  • em [5], confirmamos;
  • em [6], o projeto importado;

Os serviços web acessíveis através de URLs padrão que devolvem texto JSON são frequentemente designados por serviços REST (REpresentational State Transfer). Neste documento, referir-me-ei simplesmente ao serviço que vamos construir como um serviço web/JSON. Diz-se que um serviço é RESTful se seguir determinadas regras. Não tentei cumprir essas regras.

Vamos agora examinar o projeto importado, começando pela sua configuração do Maven.

2.11.2. Configuração do Maven

O ficheiro [pom.xml] é o seguinte:


<?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>
  • linhas 10–14: tal como no projeto [Spring Data], o projeto pai [Spring Boot] está presente;
  • linhas 17–20: O artefacto [spring-boot-starter-web] inclui as bibliotecas necessárias para um projeto Spring MVC. Em particular, inclui um servidor Tomcat incorporado. A aplicação será executada neste servidor;
  • linhas 21–24: A biblioteca Jackson lida com JSON: convertendo um objeto Java numa cadeia JSON e vice-versa;

Esta configuração inclui um grande número de bibliotecas:

Acima, vemos os três arquivos do servidor Tomcat.

2.11.3. A arquitetura de um serviço REST Spring

O Spring MVC implementa o padrão arquitetónico MVC (Modelo–Visão–Controlador) da seguinte forma:

O processamento de um pedido do cliente decorre da seguinte forma:

  1. solicitação - os URLs solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... O [Dispatcher Servlet] é a classe Spring que lida com os URLs recebidos. Ele «encaminha» o URL para a ação que deve tratá-lo. Estas ações são métodos de classes específicas chamadas [Controllers]. O C em MVC aqui é a cadeia [Dispatcher Servlet, Controller, Action]. Se nenhuma ação tiver sido configurada para tratar a URL recebida, o [Dispatcher Servlet] responderá que a URL solicitada não foi encontrada (erro 404 NOT FOUND);
  1. o processamento
  • a ação selecionada pode utilizar os parâmetros que o [Servlet Dispatcher] lhe passou. Estes podem provir de várias fontes:
    • o caminho [/param1/param2/...] da URL,
    • os parâmetros da URL [p1=v1&p2=v2],
    • dos parâmetros enviados pelo navegador com o seu pedido;
  • ao processar a solicitação do utilizador, a ação pode necessitar da camada [de negócios] [2b]. Uma vez processada a solicitação do cliente, ela pode desencadear várias respostas. Um exemplo clássico é:
    • uma página de erro, se a solicitação não puder ser processada corretamente
    • uma página de confirmação, caso contrário
  • a ação instrui que uma vista específica seja exibida [3]. Esta vista exibirá dados conhecidos como o modelo de vista. Este é o M em MVC. A ação criará este modelo M [2c] e instruirá que uma vista V seja exibida [3];
  1. resposta - a vista V selecionada utiliza o modelo M construído pela ação para inicializar as partes dinâmicas da resposta HTML que deve enviar ao cliente e, em seguida, envia essa resposta.

Para um serviço web / JSON, a arquitetura anterior é ligeiramente modificada:

  • em [4a], o modelo, que é uma classe Java, é convertido numa cadeia JSON por uma biblioteca JSON;
  • em [4b], esta cadeia JSON é enviada para o navegador;

2.11.4. O controlador C

  

A aplicação importada tem o seguinte 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));
    }
}
  • linha 9: a anotação [@Controller] torna a classe [GreetingController] um controlador Spring, o que significa que os seus métodos são registados para tratar URLs;
  • linha 15: a anotação [@RequestMapping] especifica a URL tratada pelo método, neste caso a URL [/greeting]. Veremos mais adiante que esta URL pode ser parametrizada e que é possível recuperar esses parâmetros;
  • linha 16: a anotação [@ResponseBody] indica que o método não gera um modelo para uma vista (JSP, JSF, Thymeleaf, etc.) a ser enviada para o navegador do cliente, mas sim gera a resposta diretamente para o próprio navegador. Aqui, produz um objeto do tipo [Greeting] (linha 18). Embora não seja imediatamente evidente aqui, este objeto será primeiro convertido para JSON antes de ser enviado para o navegador. É a presença de uma biblioteca JSON nas dependências do projeto que faz com que o Spring Boot configure automaticamente o projeto desta forma;
  • Linha 17: O método [greeting] tem um parâmetro [String name]. A anotação [@RequestParam(value = "name", required = false, defaultValue = "World"] indica que este parâmetro deve ser inicializado com um parâmetro chamado [name] (@RequestParam(value = "name"). Este pode ser um parâmetro GET ou POST. Este parâmetro não é obrigatório (required = false). Neste caso, o parâmetro [name] do método será inicializado com o valor [World] (defaultValue = "World").

2.11.5. O modelo M

O modelo M produzido pelo método anterior é o seguinte 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;
    }
}

A transformação JSON deste objeto irá criar a string {"id":n,"content":"text"}. Por fim, a string JSON produzida pelo método do controlador terá o seguinte formato:

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

ou

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

2.11.6. Configuração do projeto

  

O projeto é configurado pela seguinte classe [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);
    }
}
  • Linha 11: Curiosamente, esta classe é executável com um método [main] específico para aplicações de consola. É efetivamente esse o caso. A classe [SpringApplication] na linha 12 irá iniciar o servidor Tomcat presente nas dependências e implementar o serviço REST nele;
  • linha 4: podemos ver que a classe [SpringApplication] pertence ao projeto [Spring Boot];
  • linha 12: o primeiro parâmetro é a classe que configura o projeto, o segundo contém quaisquer parâmetros adicionais;
  • linha 8: a anotação [@EnableAutoConfiguration] instrui o Spring Boot a configurar o projeto;
  • linha 7: a anotação [@ComponentScan] faz com que o diretório que contém a classe [Application] seja verificado em busca de componentes Spring. Será encontrado um: a classe [GreetingController], que possui a anotação [@Controller], tornando-a um componente Spring;

2.11.7. Executar o projeto

Vamos executar o projeto:

 

Recebemos os seguintes registos da 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)
  • linha 12: o servidor Tomcat inicia na porta 8080 (linha 11);
  • linha 16: o servlet [DispatcherServlet] está presente;
  • linha 19: o método [GreetingController.greeting] foi detetado;

Para testar a aplicação web, solicitamos a URL [http://localhost:8080/greeting]:

 

Recebemos a cadeia JSON esperada. Pode ser interessante ver os cabeçalhos HTTP enviados pelo servidor. Para tal, utilizaremos o plugin do Chrome chamado [Advanced Rest Client] (ver Apêndices):

  • em [1], o URL solicitado;
  • em [2], é utilizado o método GET;
  • em [3], a resposta JSON;
  • em [4], o servidor indicou que estava a enviar uma resposta no formato JSON;
  • em [5], solicitamos a mesma URL, mas desta vez utilizando uma solicitação POST;
  • em [7], a informação é enviada para o servidor no formato [urlencoded];
  • em [6], o parâmetro name com o seu valor;
  • em [8], o navegador informa ao servidor que está a enviar informações [urlencoded];
  • em [9], a resposta JSON do servidor;

2.11.8. Criação de um arquivo executável

É possível criar um arquivo executável fora do Eclipse. A configuração necessária encontra-se no ficheiro [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>
  • As linhas 9–12 definem o plugin que irá criar o arquivo executável;
  • A linha 3 define a classe executável do projeto;

Eis como proceder:

  • em [1]: execute uma meta do Maven;
  • em [2]: existem duas metas: [clean] para eliminar a pasta [target] do projeto Maven, [package] para a regenerar;
  • em [3]: a pasta [target] gerada ficará localizada nesta pasta;
  • em [4]: o alvo é gerado;

Nos registos que aparecem na consola, é importante verificar se o plugin [spring-boot-maven-plugin] está presente. Este é o plugin que gera o arquivo executável.

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

Utilizando um console, navegue até à pasta gerada:

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
...
  • linha 5: o arquivo gerado;

Este arquivo é executado da seguinte forma:

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)
...

Agora que a aplicação web está em execução, pode aceder-lhe utilizando um navegador:

 

2.11.9. Implantação da aplicação num servidor Tomcat

Embora o Spring Boot seja muito prático no modo de desenvolvimento, é provável que uma aplicação de produção seja implementada num servidor Tomcat real. Veja como fazê-lo:

Modifique o ficheiro [pom.xml] da seguinte forma:


<?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>

É necessário efetuar alterações em dois locais:

  • linha 9: deve especificar que vai gerar um ficheiro WAR (Web Archive);
  • Linhas 26–30: é necessário adicionar uma dependência do artefacto [spring-boot-starter-tomcat]. Este artefacto adiciona todas as classes do Tomcat às dependências do projeto;
  • Linha 29: este artefacto é [fornecido], o que significa que os arquivos correspondentes não serão incluídos no ficheiro WAR gerado. Em vez disso, esses arquivos estarão localizados no servidor Tomcat onde a aplicação será executada;

Deve também configurar a aplicação web. Na ausência de um ficheiro [web.xml], isto é feito utilizando uma classe que estende [SpringBootServletInitializer]:

  

A classe [ApplicationInitializer] é a seguinte:


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);
    }
 
}
  • linha 6: a classe [ApplicationInitializer] estende a classe [SpringBootServletInitializer];
  • linha 9: o método [configure] é reescrito (linha 8);
  • linha 10: é fornecida a classe que configura o projeto;

Para executar o projeto, proceda da seguinte forma:

  • em [1], execute o projeto num dos servidores registados no IDE Eclipse;
  • em [2], selecione [tc Server Developer], que é a opção predefinida. Trata-se de uma variante do Tomcat;

Depois de fazer isto, pode introduzir o URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] num navegador:

 

Agora já sabemos como gerar um arquivo WAR. A seguir, continuaremos a trabalhar com o Spring Boot e o seu arquivo JAR executável.

2.11.10. Criar um novo projeto web

Para criar um novo projeto web, siga estes passos:

  • em [1]: Ficheiro / Novo / Projeto Spring Starter
  • em [2]: selecione [Web]. Não selecione nenhuma biblioteca de visualizações, pois num serviço web / JSON não existem visualizações;
  • O projeto criado será um projeto Maven. Em [3], introduza o nome do grupo para o artefacto Maven a ser criado; em [4], introduza o nome do artefacto;
  • em [5], introduza o nome de um pacote onde o Spring irá colocar a classe de configuração do projeto;
  • em [6], atribua um nome ao projeto Eclipse — pode ser diferente do nome em [4];
 

2.12. A camada [web]

  

Vamos construir a camada web em várias etapas:

  • Etapa 1: uma camada web funcional sem autenticação;
  • Etapa 2: Implementação da autenticação com o Spring Security;
  • Etapa 3: Implementação de CORS [A partilha de recursos entre origens (CORS) é um mecanismo que permite que muitos recursos (por exemplo, tipos de letra, JavaScript, etc.) numa página web sejam solicitados a partir de outro domínio fora do domínio de origem do recurso. (Wikipedia)]. O cliente do nosso serviço web será um cliente web Angular que não pertence necessariamente ao mesmo domínio que o nosso serviço web. Por predefinição, não pode aceder ao serviço web, a menos que este o autorize a fazê-lo. Veremos como;

2.12.1. Configuração do Maven

O ficheiro [pom.xml] do projeto é o seguinte:


<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>
  • linhas 7–11: o projeto Maven pai;
  • linhas 13–16: dependências para um projeto Spring MVC;
  • linhas 17–21: dependências das camadas [lógica de negócio, DAO, JPA];

2.12.2. A interface do serviço web

  • Em [1] acima, o navegador só pode solicitar um número limitado de URLs com uma sintaxe específica;
  • em [4], recebe uma resposta JSON;

As respostas do nosso serviço web terão todas o mesmo formato, correspondendo à representação JSON de um objeto do tipo [Response], como se segue:


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
...
}
  • linha 7: código de erro da resposta 0: OK, qualquer outra coisa: KO;
  • linha 9: o corpo da resposta;

Apresentamos agora as capturas de ecrã que ilustram a interface do serviço web / JSON:

Lista de todos os pacientes do consultório médico [/getAllClients]

Lista de todos os médicos do consultório [/getAllMedecins]

Lista dos horários disponíveis de um médico [/getAllCreneaux/{idMedecin}]

Lista de consultas de um médico [/getRvMedecinJour/{idMedecin}/{aaaa-mm-dd}

Agenda diária do médico [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-dd}]

Para adicionar ou eliminar uma consulta, utilizamos a extensão do Chrome [Advanced Rest Client], uma vez que estas operações são realizadas através de um pedido POST.

Adicionar um compromisso [/ addRv]

  • em [0], a URL do serviço web;
  • em [1], é utilizado o método POST;
  • em [2], o texto JSON das informações enviadas ao serviço web no formato {day, clientId, slotId};
  • em [3], o cliente especifica ao serviço web que está a enviar informações no formato JSON;

A resposta é então a seguinte:

  • em [4]: o cliente envia o cabeçalho indicando que os dados que está a enviar estão no formato JSON;
  • em [5]: o serviço web responde que também está a enviar JSON;
  • em [6]: a resposta JSON do serviço web. O campo [data] contém a representação JSON do compromisso adicionado;

A presença do novo compromisso pode ser verificada:

Eliminar um compromisso [/deleteApp]

  • em [1], o URL do serviço web;
  • em [2], é utilizado o método POST;
  • em [3], o texto JSON da informação enviada ao serviço web na forma {idRv};
  • em [4], o cliente especifica ao serviço web que está a enviar dados JSON;

A resposta é então a seguinte:

  • em [5]: o campo [status] é definido como 0, indicando que a operação foi bem-sucedida;

A eliminação do compromisso pode ser verificada:

Como se pode ver acima, a consulta da paciente [Sra. GERMAN] já não consta da lista.

O serviço web também permite recuperar entidades pelo seu ID:

Todas estas URLs são geridas pelo controlador [RdvMedecinsController], que iremos agora apresentar.

2.12.3. A estrutura do controlador [ RdvMedecinsController]

  

O controlador [RdvMedecinsController] é o seguinte:


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) {
...
    }
}
  • linha 6: a anotação [@RestController] torna a classe [RdvMedecinsController] um controlador Spring. Além disso, garante que os métodos que tratam de URLs gerem uma resposta que é automaticamente convertida para JSON;
  • linhas 9–10: Um objeto do tipo [ApplicationModel] será injetado aqui pelo Spring;
  • linha 13: a anotação [@PostConstruct] marca um método para ser executado imediatamente após a instância da classe. Quando este método é executado, os objetos injetados pelo Spring estão disponíveis;
  • Todos os métodos devolvem um objeto do tipo [Response] da seguinte forma:

package rdvmedecins.web.models;
 
public class Reponse {
 
    // ----------------- properties
    // operation status
    private int status;
    // the answer
    private Object data;
...
}

Este objeto é serializado em JSON antes de ser enviado para o navegador do cliente;

  • linha 20: a anotação [@RequestMapping] define as condições para chamar o método. Aqui, o método trata de uma solicitação GET da URL [/getAllMedecins]. Se esta URL fosse solicitada via POST, seria rejeitada e o Spring MVC enviaria um código de erro HTTP ao cliente web;
  • linha 32: a URL é configurada com {idMedecin}. Este parâmetro é recuperado utilizando a anotação [@PathVariable] na linha 33;
  • linha 33: o único parâmetro [long idMedecin] recebe o seu valor do parâmetro {idMedecin} na URL [@PathVariable("idMedecin")]. O parâmetro na URL e o do método podem ter nomes diferentes. Note que [@PathVariable("idMedecin")] é do tipo String (toda a URL é uma String), enquanto o parâmetro [long idMedecin] é do tipo [long]. A conversão de tipos é realizada automaticamente. É devolvido um código de erro HTTP se esta conversão de tipos falhar;
  • linha 65: a anotação [@RequestBody] refere-se ao corpo da solicitação. Numa solicitação GET, quase nunca há um corpo (mas é possível incluir um). Numa solicitação POST, geralmente há um (mas é possível omiti-lo). Para a URL [ajouterRv], o cliente web envia a seguinte string JSON na sua solicitação POST:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

A sintaxe [@RequestBody PostAjouterRv post] (linha 65), combinada com o facto de o método esperar JSON [consumes = "application/json; charset=UTF-8"] (linha 64), fará com que a cadeia JSON enviada pelo cliente web seja deserializada num objeto do tipo [PostAjouter]. Este objeto é definido da seguinte forma:


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

Aqui também, as conversões de tipo necessárias ocorrerão automaticamente;

  • As linhas 69–70 contêm um mecanismo semelhante para o URL [/deleteRv]. A string JSON enviada é a seguinte:
{"idRv":116}

e o tipo [PostSupprimerRv] é o seguinte:


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

2.12.4. Modelos de serviços web

  

Já apresentámos os modelos [Response, PostAddAppointment, PostDeleteAppointment]. O modelo [ApplicationModel] é o seguinte:


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);
    }
 
}
  • linha 6: a anotação [@Component] torna a classe [ApplicationModel] um componente Spring. Tal como todos os componentes Spring vistos até agora (com exceção de @Controller), apenas um único objeto deste tipo será instanciado (singleton);
  • linha 7: a classe [ApplicationModel] implementa a interface [IMetier];
  • linhas 10–11: Uma referência à camada [business] é injetada pelo Spring;
  • linha 19: a anotação [@PostConstruct] garante que o método [init] será executado imediatamente após a instância da classe [ApplicationModel] ser criada;
  • linhas 23–24: as listas de médicos e clientes são recuperadas da camada [business];
  • linha 26: se ocorrer uma exceção, armazenamos as mensagens da pilha de exceções no campo da linha 17;

A classe [ApplicationModel] terá duas finalidades:

  • como um cache para armazenar as listas de médicos e pacientes (clientes);
  • como uma interface única para os controladores;

A arquitetura da camada web evolui da seguinte forma:

  • em [2b], os métodos do(s) controlador(es) comunicam com o singleton [ApplicationModel];

Esta estratégia proporciona flexibilidade na gestão da cache. Atualmente, os horários das consultas médicas não são armazenados na cache. Para os armazenar, basta modificar a classe [ApplicationModel]. Isto não tem qualquer impacto no controlador, que continuará a utilizar o método [List<Creneau> getAllCreneaux(long idMedecin)] tal como fazia anteriormente. É a implementação deste método em [ApplicationModel] que será alterada.

2.12.5. A Classe Static

A classe [Static] contém um conjunto de métodos utilitários estáticos que não têm aspetos «de negócio» ou «web»:

  

O código é o seguinte:


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
    // --------------------------------------------------------
....
}
  • linha 12: o método [Static.getErrorsForException] que foi utilizado (linha 8 abaixo) no método [init] da classe [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);
        }
}

O método constrói um objeto [List<String>] contendo as mensagens de erro [exception.getMessage()] de uma exceção [exception] e as das suas exceções internas [exception.getCause()].

A classe [Static] contém outros métodos utilitários que revisitaremos quando os encontrarmos.

Vamos agora detalhar o tratamento das URLs do serviço web. Três classes principais estão envolvidas neste processo:

  • o controlador [RdvMedecinsController];
  • a classe de métodos utilitários [Static];
  • a classe de cache [ApplicationModel];
  

2.12.6. O método [init] do controlador

O controlador [RdvMedecinsController] (ver secção 2.12.3) possui um método [init] que é executado imediatamente após a sua instanciação:


    @Autowired
    private ApplicationModel application;
    private List<String> messages;
 
    @PostConstruct
    public void init() {
        // application error messages
        messages = application.getMessages();
}
  • Linha 8: As mensagens de erro armazenadas no cache da aplicação [ApplicationModel] são guardadas localmente no campo da linha 3. Isto permite que os métodos determinem se a aplicação foi inicializada corretamente.

2.12.7. A URL [/getAllMedecins]

A URL [/getAllDoctors] é tratada pelo seguinte método no 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));
        }
}
  • linha 5: verificamos se a aplicação foi inicializada corretamente (messages == null). Caso contrário, devolvemos uma resposta com status = -1 e data = messages;
  • linha 10: caso contrário, devolvemos a lista de médicos com um estado igual a 0. O método [application.getAllMedecins()] não lança uma exceção porque se limita a devolver uma lista armazenada em cache. No entanto, manteremos este tratamento de exceções para o caso de os médicos já não se encontrarem em cache;

Ainda não ilustrámos o caso em que a aplicação não conseguiu inicializar corretamente. Vamos parar o SGBD MySQL5, iniciar o serviço web e, em seguida, solicitar a URL [/getAllMedecins]:

Image

De facto, obtemos um erro. Em circunstâncias normais, obtemos a seguinte visualização:

2.12.8. A URL [/getAllClients]

A URL [/getAllClients] é tratada pelo seguinte método no [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));
        }
}

É semelhante ao método [getAllMedecins] que já estudámos. Os resultados obtidos são os seguintes:

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

A URL [/getAllSlots/{doctorId}] é tratada pelo seguinte método no 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));
    }
  • linha 9: o médico identificado pelo parâmetro [id] é solicitado a partir de um 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 devolve um valor de estado no intervalo [0,1,2]. Voltemos ao código do método [getAllSlots]:

  • linhas 10–12: se status ≠ 0, devolve a resposta imediatamente;
  • linha 13: recuperamos o médico;
  • linha 17: recuperamos os horários disponíveis deste médico;
  • linha 22: devolvemos um objeto [Static.getListMapForCreneaux(slots)] como resposta;

Vamos rever a definição da classe [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;
...
}
  • linha 13: o médico é recuperado no modo [FetchType.LAZY];

Lembre-se da consulta JPQL que implementa o método [getAllCreneaux] na camada [DAO]:


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

A notação [c.medecin.id] força uma junção entre as tabelas [CRENEAUX] e [MEDECINS]. Como resultado, a consulta devolve todos os horários de consulta do médico, com o médico incluído em cada um deles. Quando serializamos esses horários para JSON, a cadeia JSON do médico aparece em cada um deles. Isto é desnecessário. Assim, em vez de serializar um objeto [Creneau], serializaremos um objeto [Map] contendo apenas os campos desejados.

Voltemos ao código que vimos anteriormente:


// we return the answer
return new Reponse(0, Static.getListMapForCreneaux(créneaux));

O método [Static.getListMapForCreneaux] é o seguinte:


    // 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;
}

e o método [Static.getMapForCreneau] é o seguinte:


    // 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;
}
  • linha 8: criamos um dicionário;
  • linhas 9–13: adicionamos os campos que queremos manter na string JSON. O campo [doctor] não está incluído;
  • linha 15: devolvemos este dicionário;

Os resultados obtidos são os seguintes:

ou estes, caso o intervalo de tempo não exista:

ou estes, em caso de erro ao aceder à base de dados:

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

A URL [/getRvMedecinJour/{idMedecin}/{jour}] é tratada pelo seguinte método no 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));
}
  • Linha 31: Devolvemos um objeto List<Map<String, Object>> em vez de um objeto List<Rv>. Recorde a definição da classe [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;
 
...
 
}
  • linha 11: o cliente é recuperado utilizando o modo [FetchType.LAZY];
  • linha 18: o slot é recuperado utilizando o modo [FetchType.LAZY];

Recordemos a consulta JPQL que recupera os compromissos:


@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")

As junções são realizadas explicitamente para recuperar os campos [client] e [creneau]. Além disso, devido à junção [cr.medecin.id=?1], também teremos o médico. O médico aparecerá, portanto, na cadeia JSON de cada consulta. No entanto, esta informação duplicada é desnecessária. Voltemos ao código do método:

  • linha 31: construímos nós próprios o dicionário a ser serializado em JSON;

O dicionário construído para uma consulta é o seguinte:


    // 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;
}
  • Linha 11: Recuperamos o dicionário do objeto [Creneau] que apresentámos anteriormente;

Os resultados obtidos são os seguintes:

ou estes com um dia incorreto:

ou estes com um médico incorreto:

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

A URL [/getAgendaMedecinJour/{idMedecin}/{jour}] é tratada pelo seguinte método no 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));
    }
}
  • A linha 30 devolve um objeto do tipo List<Map<String, Object>>.

O método [Static.getMapForAgendaMedecinJour] é o seguinte:


    // 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;
}

O dicionário construído tem três campos:

  • [doctor]: o médico proprietário da agenda. Mantivemos esta informação porque aparece apenas uma vez, enquanto nos casos anteriores era repetida em todas as cadeias JSON;
  • [day]: o dia do calendário;
  • [doctorSlots]: a lista dos horários disponíveis do médico, incluindo quaisquer consultas agendadas para esse horário;

O método [getMapForCreneauMedecinJour] utilizado na linha 13 é o seguinte:


    // 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;
}
  • linhas 9-10: usamos os dicionários já discutidos para os tipos [Creneau] e [Rv], que, portanto, não contêm quaisquer objetos [Medecin];

Os resultados obtidos são os seguintes:

ou estes, se o dia estiver incorreto:

ou estes, se o número de identificação do médico for inválido:

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

A URL [/getMedecinById/{id}] é tratada pelo seguinte método no 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);
}

Na linha 8, o método [getMedecin] é o seguinte:


    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);
}

Os resultados são os seguintes:

ou estes, se o ID do médico estiver incorreto:

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

A URL [/getClientById/{id}] é tratada pelo seguinte método no 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);
}

Na linha 8, o método [getClient] é o seguinte:


    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);
}

Os resultados são os seguintes:

ou estes, se o ID do cliente estiver incorreto:

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

A URL [/getCreneauById/{id}] é tratada pelo seguinte método no 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;
}

Na linha 8, o método [getCreneau] é o seguinte:


    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);
}

Os resultados obtidos são os seguintes:

ou estes, se o número da ranhura estiver incorreto:

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

A URL [/getRvById/{id}] é tratada pelo seguinte método no 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;
}

Na linha 8, o método [getRv] é o seguinte:


    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);
}

Na linha 10, o método [Static.getMapForRv2] é o seguinte:


// 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;
    }

Os resultados são os seguintes:

ou estes, se o ID da marcação estiver incorreto:

2.12.16. A URL [/ajouterRv]

A URL [/ajouterRv] é tratada pelo seguinte método no 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));
    }

Não há aqui nada que não tenhamos visto antes. Na linha 41, devolvemos o compromisso que foi adicionado na linha 36.

Os resultados obtidos têm este aspeto com o [Advanced Rest Client]:

ou assim se, por exemplo, indicarmos um número de slot inexistente:

2.12.17. A URL [/deleteAppointment]

A URL [/deleteAppointment] é tratada pelo seguinte método no 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);
    }

Os arquivos <a id="supprimerrv"></a> resultantes são os seguintes:

ou estes, caso o ID da marcação não exista:

Já terminámos com o controlador. Agora vamos ver como configurar o projeto.

2.12.18. Configuração do serviço web

  

A classe de configuração [AppConfig] é a seguinte:


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 {
 
}
  • Linha 9: Definimos o modo como [AutoConfiguration] para que o Spring Boot possa configurar o projeto com base nos ficheiros que encontrar no classpath do projeto;
  • linha 10: especificamos que os componentes Spring devem ser procurados no pacote [rdvmedecins.web] e nos seus subpacotes. É assim que os seguintes componentes serão descobertos:
    • [@RestController RdvMedecinsController] no pacote [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] no pacote [rdvmedecins.web.models];
  • Linha 11: Importamos a classe [DomainAndPersistenceConfig], que configura o projeto [rdvmedecins-metier-dao] para fornecer acesso aos beans desse projeto;

2.12.19. A classe executável do serviço web

  

A classe [Boot] é a seguinte:


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);
    }
}

Linha 10: O método estático [SpringApplication.run] é executado com a classe de configuração do projeto [AppConfig] como seu primeiro parâmetro. Este método irá configurar automaticamente o projeto, iniciar o servidor Tomcat incorporado nas dependências e implementar o controlador [RdvMedecinsController] nele.

Os registos durante a execução são os seguintes:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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
  • linha 17: o servidor Tomcat inicia;
  • linhas 23-31: as camadas [lógica de negócio, DAO, JPA] são inicializadas;
  • linha 34: o método que trata da URL [/getRvMedecinJour/{idMedecin}/{jour}] foi descoberto. Este processo de descoberta de métodos do controlador repete-se até à linha 44;
  • linha 52: o servlet Spring MVC [DispatcherServlet] está pronto para responder a pedidos de clientes web;

Temos agora um serviço web funcional que pode ser consultado por um cliente web. Vamos agora abordar a segurança deste serviço: queremos que apenas determinadas pessoas possam gerir as consultas médicas. Para tal, utilizaremos o framework Spring Security, um componente do ecossistema Spring.

2.13. Introdução ao Spring Security

Vamos importar mais uma vez um guia do Spring, seguindo os passos 1 a 3 abaixo:

  

O projeto é composto pelos seguintes elementos:

  • na pasta [templates], encontrará as páginas HTML do projeto;
  • [Application]: é a classe executável do projeto;
  • [MvcConfig]: é a classe de configuração do Spring MVC;
  • [WebSecurityConfig]: é a classe de configuração do Spring Security;

2.13.1. Configuração do Maven

O projeto [3] é um projeto Maven. Vamos examinar o seu ficheiro [pom.xml] para ver as suas dependências:


    <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>
  • linhas 1–5: o projeto é um projeto Spring Boot;
  • linhas 8–11: dependência da estrutura [Thymeleaf], que permite a criação de páginas HTML dinâmicas. Esta estrutura pode substituir o JSP (Java Server Pages), que até recentemente era a estrutura de visualização padrão para o Spring MVC;
  • linhas 12–15: dependência da estrutura Spring Security;

2.13.2. Visualizações Thymeleaf

  

A vista [home.html] é a seguinte:

  

<!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>
  • Os atributos [th:xx] são atributos do Thymeleaf. São interpretados pelo Thymeleaf antes de a página HTML ser enviada para o cliente. O cliente não os vê;
  • linha 12: o atributo [th:href="@{/hello}"] irá gerar o atributo [href] da tag <a>. O valor [@{/hello}] irá gerar o caminho [<context>/hello], em que [context] é o contexto da aplicação web;

O código HTML gerado é o seguinte:

<!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>
  • linha 10: o contexto da aplicação é a raiz /;

A visualização [hello.html] é a seguinte:

  

<!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>
  • linha 9: O atributo [th:inline="text"] irá gerar o texto da tag <h1>. Este texto contém uma expressão $ que deve ser avaliada. O elemento [[${#httpServletRequest.remoteUser}]] é o valor do atributo [RemoteUser] do pedido HTTP atual. Este é o nome do utilizador que está conectado;
  • linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da tag [form]. O valor [@{/logout}] irá gerar o caminho [<context>/logout], onde [context] é o contexto da aplicação web;

O código HTML gerado é o seguinte:

<!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>
  • linha 8: a tradução de Olá [[${#httpServletRequest.remoteUser}]]!;
  • linha 9: a tradução de @{/logout};
  • linha 11: um campo oculto denominado (atributo name) _csrf;

A visualização final [login.html] é a seguinte:

  

<!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>
  • Linha 9: O atributo [th:if="${param.error}"] garante que a tag <div> só será gerada se o URL que exibe a página de login contiver o parâmetro [error] (http://context/login?error);
  • linha 10: o atributo [th:if="${param.logout}"] garante que a tag <div> só será gerada se a URL que exibe a página de login contiver o parâmetro [logout] (http://context/login?logout);
  • linhas 11–23: um formulário HTML;
  • linha 11: o formulário será enviado para a URL [<context>/login], onde <context> é o contexto da aplicação web;
  • linha 13: um campo de entrada denominado [username];
  • linha 17: um campo de entrada denominado [password];

O código HTML gerado é o seguinte:

<!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>

Repare que, na linha 21, o Thymeleaf adicionou um campo oculto chamado [_csrf].

2.13.3. Configuração do Spring MVC

  

A classe [MvcConfig] configura a estrutura 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");
    }
 
}
  • linha 7: a anotação [@Configuration] torna a classe [MvcConfig] uma classe de configuração;
  • linha 8: a classe [MvcConfig] estende a classe [WebMvcConfigurerAdapter] para substituir determinados métodos;
  • linha 10: redefinição de um método da classe pai;
  • linhas 11–16: o método [addViewControllers] permite associar URLs a vistas HTML. São feitas as seguintes associações:
URL
view
/, /home
/templates/home.html
/olá
/templates/hello.html
/login
/modelos/login.html

O sufixo [html] e a pasta [templates] são os valores predefinidos utilizados pelo Thymeleaf. Podem ser alterados através da configuração. A pasta [templates] deve estar na raiz do classpath do projeto:

Em [1] acima, as pastas [main] e [resources] são ambas pastas de origem. Isto significa que o seu conteúdo estará na raiz do classpath do projeto. Portanto, em [2], as pastas [hello] e [templates] estarão na raiz do classpath.

2.13.4. Configuração do Spring Security

  

A classe [WebSecurityConfig] configura a estrutura 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");
    }
}
  • linha 9: a anotação [@Configuration] torna a classe [WebSecurityConfig] uma classe de configuração;
  • linha 10: a anotação [@EnableWebSecurity] torna a classe [WebSecurityConfig] uma classe de configuração do Spring Security;
  • linha 11: a classe [WebSecurity] estende a classe [WebSecurityConfigurerAdapter] para substituir determinados métodos;
  • linha 12: redefinição de um método da classe pai;
  • linhas 13–16: o método [configure(HttpSecurity http)] é substituído para definir direitos de acesso para os vários URLs da aplicação;
  • linha 14: o método [http.authorizeRequests()] permite associar URLs a direitos de acesso. São feitas as seguintes associações:
URL
regra
código
/, /home
acesso sem autenticação

http.authorizeRequests().antMatchers("/", "/home").permitAll()
outras URLs
apenas acesso autenticado
http.anyRequest().authenticated();
  • Linha 15: define o método de autenticação. A autenticação é realizada através de um formulário URL [/login] acessível a todos [http.formLogin().loginPage("/login").permitAll()]. O logout também é acessível a todos.
  • linhas 19-21: redefinem o método [configure(AuthenticationManagerBuilder auth)] que gere os utilizadores;
  • linha 20: a autenticação é realizada utilizando utilizadores codificados [auth.inMemoryAuthentication()]. Um utilizador é definido aqui com o nome de utilizador [user], palavra-passe [password] e função [USER]. Aos utilizadores com a mesma função podem ser atribuídas as mesmas permissões;

2.13.5. Classe executável

  

A classe [Application] é a seguinte:


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);
    }
 
}
  • Linha 8: A anotação [@EnableAutoConfiguration] instrui o Spring Boot (linha 3) a realizar a configuração que o programador não definiu explicitamente;
  • linha 9: torna a classe [Application] uma classe de configuração do Spring;
  • linha 10: instrui o sistema a analisar o diretório que contém a classe [Application] para procurar componentes Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão, assim, encontradas porque possuem a anotação [@Configuration];
  • linha 13: o método [main] da classe executável;
  • linha 14: o método estático [SpringApplication.run] é executado com a classe de configuração [Application] como parâmetro. Já nos deparámos com este processo e sabemos que o servidor Tomcat incorporado nas dependências Maven do projeto será iniciado e o projeto implementado nele. Vimos que quatro URLs eram geridas [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.

2.13.6. Testar a aplicação

Vamos começar por solicitar a URL [/], que é uma das quatro URLs aceites. Está associada à vista [/templates/home.html]:

 

A URL solicitada [/] é acessível a todos. É por isso que conseguimos recuperá-la. O link [aqui] é o seguinte:

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

A URL [/hello] será solicitada quando clicarmos no link. Esta está protegida:

URL
regra
código
/, /home
acesso sem autenticação

http.authorizeRequests().antMatchers("/", "/home").permitAll()
outras URLs
apenas acesso autenticado
http.anyRequest().authenticated();

É necessário estar autenticado para aceder. O Spring Security redirecionará então o navegador do cliente para a página de autenticação. Com base na configuração apresentada, esta é a página no URL [/login]. Esta página é acessível a todos:


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

Assim, obtemos isto [1]:

O código-fonte da página obtida é o seguinte:

<!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>
  • Na linha 7, aparece um campo oculto que não consta na página original [login.html]. Foi adicionado pelo Thymeleaf. Este código, conhecido como CSRF (Cross-Site Request Forgery), foi concebido para eliminar uma vulnerabilidade de segurança. Este token deve ser reenviado ao Spring Security juntamente com a autenticação para que seja aceite;

Recordamos que apenas o par utilizador/palavra-passe é reconhecido pelo Spring Security. Se introduzirmos outra coisa em [2], obtemos a mesma página com uma mensagem de erro em [3]. O Spring Security redirecionou o navegador para o URL [http://localhost:8080/login?error]. A presença do parâmetro [error] desencadeou a exibição da tag:


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

Agora, vamos introduzir os valores esperados de utilizador/palavra-passe [4]:

  • em [4], fazemos o login;
  • em [5], o Spring Security redireciona-nos para a URL [/hello] porque essa é a URL que solicitámos quando fomos redirecionados para a página de login. A identidade do utilizador foi exibida pela seguinte linha de [hello.html]:

    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

A página [5] apresenta o seguinte formulário:


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

Quando clica no botão [Sair], é enviada uma solicitação POST para a URL [/logout]. Tal como a URL [/login], esta URL é acessível a todos:


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

No nosso mapeamento de URL/visualização, não definimos nada para a URL [/logout]. O que irá acontecer? Vamos experimentar:

  • Em [6], clicamos no botão [Sair];
  • em [7], vemos que fomos redirecionados para a URL [http://localhost:8080/login?logout]. O Spring Security solicitou este redirecionamento. A presença do parâmetro [logout] na URL fez com que a seguinte linha fosse exibida na vista:

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

2.13.7. Conclusão

No exemplo anterior, poderíamos ter escrito primeiro a aplicação web e, posteriormente, protegido-a. O Spring Security é não intrusivo. É possível implementar segurança numa aplicação web que já tenha sido escrita. Além disso, descobrimos os seguintes pontos:

  • é possível definir uma página de autenticação;
  • a autenticação deve ser acompanhada pelo token CSRF emitido pelo Spring Security;
  • se a autenticação falhar, é redirecionado para a página de autenticação com um parâmetro de erro adicional no URL;
  • se a autenticação for bem-sucedida, é redirecionado para a página solicitada no momento da autenticação. Se solicitar a página de autenticação diretamente, sem passar por uma página intermédia, o Spring Security redireciona-o para a URL [/] (este caso não foi demonstrado);
  • O utilizador sai da sessão solicitando a URL [/logout] com um pedido POST. O Spring Security redireciona-o então para a página de autenticação com o parâmetro «logout» na URL;

Todas estas conclusões baseiam-se no comportamento padrão do Spring Security. Este comportamento pode ser alterado através da configuração, substituindo determinados métodos da classe [WebSecurityConfigurerAdapter].

O tutorial anterior será de pouca utilidade para nós daqui em diante. Na verdade, iremos utilizar:

  • uma base de dados para armazenar utilizadores, as suas palavras-passe e as suas funções;
  • autenticação baseada em cabeçalhos HTTP;

Existem muito poucos tutoriais disponíveis para o que pretendemos fazer aqui. A solução que iremos propor é uma combinação de trechos de código encontrados aqui e ali.

2.14. Implementação de segurança para o serviço de marcação de consultas online

2.14.1. A base de dados

A base de dados [rdvmedecins] está a ser atualizada para incluir os utilizadores, as suas palavras-passe e as suas funções. Foram adicionadas três novas tabelas:

Image

Tabela [USERS]: utilizadores

  • ID: chave primária;
  • VERSION: coluna de versionamento de linhas;
  • IDENTITY: um identificador descritivo para o utilizador;
  • LOGIN: o nome de utilizador do utilizador;
  • PASSWORD: a sua palavra-passe;

Na tabela USERS, as palavras-passe não são armazenadas em texto simples:

 

O algoritmo utilizado para encriptar as palavras-passe é o algoritmo BCRYPT.

Tabela [ROLES]: funções

  • ID: chave primária;
  • VERSION: coluna de versão para a linha;
  • NAME: nome da função. Por predefinição, o Spring Security espera nomes no formato ROLE_XX, por exemplo, ROLE_ADMIN ou ROLE_GUEST;
 

Tabela [USERS_ROLES]: tabela de junção USERS/ROLES

Um utilizador pode ter várias funções, e uma função pode incluir vários utilizadores. Esta é uma relação muitos-para-muitos representada pela tabela [USERS_ROLES].

  • ID: chave primária;
  • VERSION: coluna de versionamento de linhas;
  • USER_ID: identificador do utilizador;
  • ROLE_ID: identificador de uma função;
 

Como estamos a modificar a base de dados, todas as camadas do projeto [lógica de negócio, DAO, JPA] têm de ser modificadas:

2.14.2. O novo projeto Eclipse para [lógica de negócio, DAO, JPA]

Duplicamos o projeto inicial [rdvmedecins-business-dao] para [rdvmedecins-business-dao-v2]:

  • em [1]: o novo projeto;
  • em [2]: as alterações introduzidas pela implementação da segurança foram agrupadas num único pacote [rdvmedecins.security]. Estes novos elementos pertencem às camadas [JPA] e [DAO], mas, por uma questão de simplicidade, agrupei-os num único pacote.

2.14.3. As novas entidades [JPA]

A camada JPA define três novas entidades:

  

A classe [User] representa a tabela [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
....
}
  • linha 9: a classe estende a classe [AbstractEntity] já utilizada para as outras entidades;
  • linhas 13–15: não são especificados nomes de colunas porque têm os mesmos nomes que os seus campos associados;

A classe [Role] reflete a tabela [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
...
}

A classe [UserRole] representa a tabela [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
...
}
  • linhas 15–17: definir a chave estrangeira da tabela [USERS_ROLES] para a tabela [USERS];
  • linhas 19-21: implementam a chave estrangeira da tabela [USERS_ROLES] para a tabela [ROLES];

2.14.4. Alterações na camada [DAO]

A camada [DAO] foi melhorada com três novos [Repository]s:

  

A interface [UserRepository] gere o acesso às 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);
}
  • linha 9: a interface [UserRepository] estende a interface [CrudRepository] do Spring Data (linha 4);
  • linhas 12-13: o método [getRoles(User user)] recupera todas as funções de um utilizador identificado pelo seu [id]
  • linhas 16-17: igual ao anterior, mas para um utilizador identificado pelo seu nome de utilizador e palavra-passe;

A interface [RoleRepository] gere o acesso às 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);
 
}
  • linha 5: a interface [RoleRepository] estende a interface [CrudRepository];
  • linha 8: pode pesquisar uma função pelo seu nome;

A interface [userRoleRepository] gere o acesso às entidades [UserRole]:


package rdvmedecins.security;
 
import org.springframework.data.repository.CrudRepository;
 
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
 
}
  • linha 5: a interface [UserRoleRepository] simplesmente estende a interface [CrudRepository] sem adicionar nenhum método novo;

2.14.5. Classes de gestão de utilizadores e funções

  

O Spring Security requer a criação de uma classe que implemente a seguinte interface [UsersDetail]:

 

Esta interface é implementada aqui pela classe [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
    ...
}
  • linha 10: a classe [AppUserDetails] implementa a interface [UserDetails];
  • linhas 15-16: a classe encapsula um utilizador (linha 15) e o repositório que fornece detalhes sobre esse utilizador (linha 16);
  • linhas 22–25: o construtor que instancia a classe com um utilizador e o seu repositório;
  • linhas 28–35: implementação do método [getAuthorities] da interface [UserDetails]. Deve construir uma coleção de elementos do tipo [GrantedAuthority] ou de um tipo derivado. Aqui, usamos o tipo derivado [SimpleGrantedAuthority] (linha 32), que encapsula o nome de uma das funções do utilizador da linha 15;
  • linhas 31–33: percorremos a lista de funções do utilizador da linha 15 para construir uma lista de elementos do tipo [SimpleGrantedAuthority];
  • linhas 38–40: implementamos o método [getPassword] da interface [UserDetails]. Devolvemos a palavra-passe do utilizador da linha 15;
  • linhas 38–40: implementamos o método [getUserName] da interface [UserDetails]. Devolvemos o nome de utilizador do utilizador da linha 15;
  • linhas 47–50: a conta do utilizador nunca expira;
  • linhas 52–55: a conta do utilizador nunca é bloqueada;
  • linhas 57–60: as credenciais do utilizador nunca expiram;
  • linhas 62–65: a conta do utilizador está sempre ativa;

O Spring Security também requer a existência de uma classe que implemente a interface [AppUserDetailsService]:

 

Esta interface é implementada pela seguinte classe [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);
    }
 
}
  • linha 9: a classe será um componente Spring, pelo que estará disponível no seu contexto;
  • linhas 12–13: o componente [UserRepository] será injetado aqui;
  • linhas 16–25: implementação do método [loadUserByUsername] da interface [UserDetailsService] (linha 10). O parâmetro é o nome de utilizador do utilizador;
  • linha 18: o utilizador é procurado através do seu nome de utilizador;
  • linhas 20–22: se o utilizador não for encontrado, é lançada uma exceção;
  • linha 24: um objeto [AppUserDetails] é construído e devolvido. É, de facto, do tipo [UserDetails] (linha 16);

2.14.6. Testes da camada [DAO]

  

Primeiro, criamos uma classe executável [CreateUser] capaz de criar um utilizador com uma função:


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();
    }
 
}
  • linha 17: a classe espera três argumentos que definem um utilizador: o seu nome de utilizador, palavra-passe e função;
  • linhas 25–27: os três parâmetros são recuperados;
  • linha 29: o contexto Spring é construído a partir da classe de configuração [DomainAndPersistenceConfig]. Esta classe já existia no projeto anterior. Deve ser atualizada da seguinte forma:

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
  • Linha 1: Deve especificar que agora existem componentes [Repository] no pacote [rdvmedecins.security];
  • linha 4: deve especificar que existem agora entidades JPA no pacote [rdvmedecins.security];

Voltemos ao código para criar um utilizador:

  • linhas 30–32: recuperamos as referências dos três objetos [Repository] que podem ser úteis para criar o utilizador;
  • linha 34: verificamos se a função já existe;
  • linhas 36–38: se não, criamo-la na base de dados. Terá um nome do tipo [ROLE_XX];
  • linha 40: verificamos se o login já existe;
  • linhas 42-49: se o nome de utilizador não existir, criamo-lo na base de dados;
  • linha 44: encriptamos a palavra-passe. Aqui, utilizamos a classe [BCrypt] do Spring Security (linha 4). Por isso, precisamos dos arquivos para este framework. O ficheiro [pom.xml] inclui uma nova dependência:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • Linha 46: O utilizador é guardado na base de dados;
  • linha 48: assim como a relação que o liga à sua função;
  • linhas 51–57: se o login já existir, verificamos se a função que pretendemos atribuir-lhe já se encontra entre as suas funções;
  • Linhas 59–61: Se a função procurada não for encontrada, é criada uma linha na tabela [USERS_ROLES] para ligar o utilizador à sua função;
  • Não implementámos proteções contra possíveis exceções. Esta é uma classe auxiliar para criar rapidamente um utilizador com uma função.

Quando a classe é executada com os argumentos [x x guest], obtêm-se os seguintes resultados na base de dados:

Tabela [USERS]

 

Tabela [FUNÇÕES]

 

Tabela [USERS_ROLES]

 

Agora, vamos considerar a segunda classe [UsersTest], que é um teste 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);
        }
    }
}
  • linhas 27–34: teste visual. Exibimos todos os utilizadores juntamente com as suas funções;
  • linhas 36–46: verificamos se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN] utilizando o [UserRepository];
  • linha 41: [admin] é a palavra-passe em texto simples. Na base de dados, esta é encriptada utilizando o algoritmo BCrypt. O método [BCrypt.checkpw] verifica se a palavra-passe encriptada corresponde à que se encontra na base de dados;
  • linhas 48–59: verificamos se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN] utilizando o [appUserDetailsService];

Os testes são executados com sucesso com os seguintes registos:

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. Conclusão provisória

As classes necessárias para o Spring Security foram adicionadas com alterações mínimas ao projeto original. Recapitulando:

  • adição de uma dependência do Spring Security no ficheiro [pom.xml];
  • criação de três tabelas adicionais na base de dados;
  • criação de entidades JPA e componentes Spring no pacote [rdvmedecins.security];

Este cenário muito favorável decorre do facto de as três tabelas adicionadas à base de dados serem independentes das tabelas existentes. Poderíamos até tê-las colocado numa base de dados separada. Isto foi possível porque decidimos que um utilizador existe independentemente dos médicos e dos clientes. Se estes últimos fossem utilizadores potenciais, teríamos de criar ligações entre a tabela [USERS] e as tabelas [MEDECINS] e [CLIENTS]. Isto teria tido um impacto significativo no projeto existente.

2.14.8. O projeto Eclipse para a camada [web]

O projeto anterior [rdvmedecins-webapi] está duplicado no projeto [rdvmedecins-webapi-v2] [1]:

As únicas alterações a efetuar devem ser feitas no pacote [rdvmedecins.web.config], onde o Spring Security deve ser configurado. Já nos deparámos com uma classe de configuração do 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 o mesmo procedimento:

  • linha 11: definir uma classe que estenda a classe [WebSecurityConfigurerAdapter];
  • linha 13: definir um método [configure(HttpSecurity http)] que define os direitos de acesso às várias URLs do serviço web;
  • linha 19: definir um método [configure(AuthenticationManagerBuilder auth)] que define os utilizadores e as suas funções;

A configuração do Spring Security é gerida pela classe [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");
    }
}
  • linhas 14-15: reutilizámos as anotações do exemplo;
  • linhas 17-18: a classe [AppUserDetails], que fornece acesso aos utilizadores da aplicação, é injetada;
  • linhas 20-21: o método [configure(HttpSecurity http)] define os utilizadores e as suas funções. Recebe um tipo [AuthenticationManagerBuilder] como parâmetro. Este parâmetro é enriquecido com duas informações:
    • uma referência ao [appUserDetailsService] da linha 18, que fornece acesso aos utilizadores registados. Note-se aqui que o facto de estarem armazenados numa base de dados não é explicitamente mencionado. Podem, portanto, estar num cache, fornecidos por um serviço web, etc.
    • o tipo de encriptação utilizado para a palavra-passe. Recorde-se que utilizámos o algoritmo BCrypt;
  • linhas 27–40: o método [configure(HttpSecurity http)] define os direitos de acesso às URLs do serviço web;
  • linha 30: vimos no projeto introdutório que, por predefinição, o Spring Security gere um token CSRF (Cross-Site Request Forgery) que o utilizador que deseja autenticar-se deve enviar de volta ao servidor. Aqui, este mecanismo está desativado;
  • linha 32: ativamos a autenticação via cabeçalho HTTP. O cliente deve enviar o seguinte cabeçalho HTTP:
Authorization:Basic code

onde code é a codificação Base64 da cadeia de caracteres login:password. Por exemplo, a codificação Base64 da cadeia de caracteres admin:admin é YWRtaW46YWRtaW4=. Portanto, um utilizador com o nome de utilizador [admin] e a palavra-passe [admin] enviará o seguinte cabeçalho HTTP para se autenticar:

Authorization:Basic YWRtaW46YWRtaW4=
  • Linhas 34–36: indicam que todos os URLs do serviço web estão acessíveis a utilizadores com a função [ROLE_ADMIN]. Isto significa que um utilizador sem esta função não pode aceder ao serviço web;

A classe [AppConfig], que configura toda a aplicação, é atualizada da seguinte forma:

  

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 {
 
}
  • A alteração é feita na linha 11: especifica que agora existem dois ficheiros de configuração a utilizar: [DomainAndPersistenceConfig] e [SecurityConfig].

2.14.9. Teste do serviço web

Iremos testar o serviço web utilizando o cliente Chrome [Advanced Rest Client]. Teremos de especificar o cabeçalho de autenticação HTTP:

Authorization:Basic code

onde [código] é a cadeia codificada em Base64 [login:password]. Para gerar este código, pode utilizar o seguinte 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));
    }
 
}

Se executarmos este programa com os dois argumentos [admin admin]:

  

obtemos o seguinte resultado:

YWRtaW46YWRtaW4=

Agora que sabemos como gerar o cabeçalho de autenticação HTTP, lançamos o serviço web agora seguro. Em seguida, utilizando o cliente Chrome [Advanced Rest Client], solicitamos a lista de todos os médicos:

  • em [1], solicitamos a URL dos médicos;
  • em [2], utilizando o método GET;
  • em [3], fornecemos o cabeçalho de autenticação HTTP. O código [YWRtaW46YWRtaW4=] é a codificação Base64 da cadeia [admin:admin];
  • em [4], enviamos o pedido HTTP;

A resposta do servidor é a seguinte:

  • em [1], o cabeçalho de autenticação HTTP;
  • em [2], o servidor devolve uma resposta JSON;
  • em [3], a lista de médicos.

Agora vamos tentar uma solicitação HTTP com um cabeçalho de autenticação incorreto. A resposta é a seguinte:

  • em [1] e [3]: o cabeçalho de autenticação HTTP;
  • em [2]: a resposta do serviço web;

Agora, vamos experimentar o utilizador / user. Ele existe, mas não tem acesso ao serviço web. Se executarmos o programa de codificação Base64 com os dois argumentos [user user]:

  

obtemos o seguinte resultado:

dXNlcjp1c2Vy
  • em [1] e [3]: o cabeçalho de autenticação HTTP;
  • em [2]: a resposta do serviço web. Diferencia-se da anterior, que era [401 Não autorizado]. Desta vez, o utilizador autenticou-se com sucesso, mas não possui permissões suficientes para aceder ao URL;

2.15. Conclusão

Vamos rever a arquitetura geral da nossa aplicação cliente/servidor:

Um serviço web seguro está agora operacional. Veremos que ele precisará de ser modificado devido a problemas que surgirão durante o desenvolvimento do cliente Angular JS. Mas vamos esperar até encontrarmos o problema para o resolver. Vamos agora construir o cliente Angular que fornecerá uma interface web para a gestão das consultas médicas.