Skip to content

2. O servidor Spring 4

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

  • primeiro, as camadas [métier] e [DAO] (Data Access Object). Aqui, utilizaremos o Spring Data;
  • depois, o serviço web JSON sem autenticação. Aqui, utilizaremos o Spring MVC;
  • depois, adicionaremos a parte de autenticação com o Spring Security.

Começamos 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:

  • [medecins]: contém a lista dos médicos do consultório;
  • [clients]: contém a lista de doentes do consultório;
  • [creneaux]: contém os horários disponíveis de cada um dos médicos;
  • [rv]: contém a lista das consultas dos médicos.

As tabelas [roles], [users] e [users_roles] são tabelas relacionadas com a autenticação. Por enquanto, não nos vamos ocupar delas.

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 reúne simultaneamente um cliente e um médico através de um intervalo horário deste último;
  • um cliente tem 0 ou mais consultas;
  • a um intervalo horário estão associadas 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 que identifica o 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 sempre que é feita uma alteração na linha.
  • NOM: o nome do médico
  • PRENOM: o seu nome próprio
  • TITRE: o seu título (Menina, Sra., Sr.)

2.1.2. A tabela [CLIENTS]

Os clientes dos diferentes médicos estão registados 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 sempre que é feita uma alteração na linha.
  • NOM: o nome do cliente
  • PRENOM: o seu nome próprio
  • TITRE: o seu título (Menina, Sra., Sr.)

2.1.3. A tabela [CRENEAUX]

Esta tabela lista os intervalos horários em que os RV são possíveis:

  • ID: número que identifica o intervalo horário — 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 sempre que é feita uma alteração na linha.
  • ID_MEDECIN: número que identifica o médico a quem pertence este intervalo horário – chave estrangeira na coluna MEDECINS (ID).
  • HDEBUT: hora de início do horário
  • MDEBUT: minutos de início do intervalo
  • HFIN: hora de fim do intervalo
  • MFIN: minutos de fim do intervalo

A segunda linha da tabela [CRENEAUX] (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]

Esta tabela lista os RV atribuídos a cada médico:

  • ID: número que identifica o RV de forma única – chave primária
  • JOUR: dia do RV
  • ID_CRENEAU: intervalo horário do RV – chave externa no campo [ID] da tabela [CRENEAUX] – define simultaneamente o intervalo horário e o médico em questão.
  • ID_CLIENT: número do cliente para quem é feita a reserva – chave estrangeira no campo [ID] da tabela [CLIENTS]

Esta tabela possui uma restrição de unicidade na sobre os valores das colunas associadas (JOUR, ID_CRENEAU):

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

Se uma linha da tabela [RV] tiver o valor (JOUR1, ID_CRENEAU1) nas colunas (JOUR, ID_CRENEAU), esse valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que dois RV foram registados 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 um SQLException quando esta situação ocorre.

A linha de id igual a 3 (ver [1] acima) significa que um RV foi marcado para o horário n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [CRENEAUX] 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 Srta. Brigitte BISTROU.

2.2. Introdução ao Spring Data

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

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

  • em [1], importamos um dos tutoriais de [spring.io/guides];
  • em [2], selecionamos o tutorial [Accessing Data Jpa], que mostra como aceder a uma base de dados com o Spring Data;
  • em [3], escolhe-se um projeto configurado pelo Maven;
  • em [4], o tutorial pode ser disponibilizado de duas formas: [initial], que é uma versão em branco que se preenche seguindo o tutorial, ou [complete], que é a versão final do tutorial. Escolhemos esta última;
  • em [5], é possível 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>
        <!-- utilize UTF-8 para tudo -->
        <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 pai do Maven. É este que define a maior parte das dependências do projeto. Estas podem ser suficientes, caso em que não se adicionam mais, ou não, caso em que se adicionam as dependências em falta;
  • linhas 12-15: definem uma dependência do [spring-boot-starter-data-jpa]. Este artefacto contém as classes do Spring Data;
  • linhas 16-19: definem uma dependência do SGBD e do H2, que permitem criar e gerir bases de dados em memória.

Vejamos as classes introduzidas por estas dependências:

São muitas:

  • algumas pertencem ao ecossistema Spring (as que começam por «spring»);
  • outras pertencem ao ecossistema Hibernate (hibernate, jboss), cuja implementação JPA é aqui utilizada;
  • outras são bibliotecas de testes (junit, hamcrest);
  • outras são bibliotecas de registos (log4j, logback, slf4j);

Vamos mantê-las todas. Para uma aplicação em produção, seria necessário manter apenas as que são necessárias.

Na linha 26 do ficheiro [pom.xml], encontra-se a linha:


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

Esta linha está relacionada com as 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>

Nas linhas 6 a 9, o plugin [spring-boot-maven-plugin] permite gerar o ficheiro JAR executável da aplicação. A linha 26 do ficheiro [pom.xml] indica, então, a classe executável desse ficheiro JAR.

2.2.2. A camada [JPA]

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

  

A aplicação é básica e gere clientes [Customer]. A classe [Customer] 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 identificador [id], um nome próprio [firstName] e um apelido [lastName]. Cada instância [Customer] representa uma linha de uma tabela da base de dados.

  • linha 8: anotação JPA que faz com que a persistência das instâncias [Customer] (Create, Read, Update, Delete) venha a ser gerida por uma implementação JPA. De acordo com as dependências do Maven, verifica-se que é 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 da chave primária específico do SGBD utilizado, neste caso o H2;

Não existem outras anotações para JPA. Serão, então, utilizados valores por predefinição:

  • a tabela [Customer] terá o nome da classe, ou seja, [Customer];
  • as colunas desta tabela terão o nome dos campos da classe: [id, firstName, lastName], tendo em conta que as maiúsculas e minúsculas não são distinguidas no nome de uma coluna da tabela;

Note-se que, em nenhum momento, a implementação JPA utilizada é referida pelo nome.

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 é definida 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 permite persistir uma entidade T na base de dados. Este método persiste a entidade com a chave primária que lhe foi atribuída pelo SGBD. Permite também atualizar uma entidade T identificada pela sua chave primária id. A escolha entre uma ou outra ação depende do valor da chave primária id: se este for nulo, é realizada a operação de persistência; caso contrário, é realizada a operação de atualização;
  • linha 10: o mesmo, mas para uma lista de entidades;
  • linha 12: o método findOne permite recuperar 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: variantes do método [delete];
  • linha 16: o método [findAll] permite recuperar todas as entidades T persistentes;
  • linha 18: o mesmo, mas limitado às entidades cuja lista de identificadores foi passada;

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 recuperar um [Customer] pelo seu nome [lastName];

E é tudo no que diz respeito à camada [DAO]. Não existe uma classe de implementação da 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], isso depende. Voltemos à definição de [Customer]:


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

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

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

É, portanto, necessário que o tipo T tenha um campo denominado [something]. Assim, o método

List<Customer> findByLastName(String lastName);

será implementado por um código semelhante ao seguinte:

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

onde [em] designa o contexto de persistência JPA. Isto só é possível se a classe [Customer] tiver um campo denominado [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);

        // guardar alguns clientes
        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"));

        // recuperar todos os clientes
        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();

        // recuperar um cliente específico através de ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // recuperar clientes pelo apelido
        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();
    }

}
  • na linha 10: indica que a classe serve 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 em simultâneo. No código de uma classe com a anotação [Configuration], encontram-se normalmente beans do Spring, ou seja, definições de classes a instanciar. Aqui, não está definido nenhum bean. É importante recordar 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 beans está definido.

  • na linha 11: a anotação [EnableAutoConfiguration] é uma anotação proveniente do projeto [Spring Boot] (linhas 5-6). Esta anotação solicita ao Spring Boot, através da classe [SpringApplication] (linha 16), que configure 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 com o Hibernate. Como a biblioteca SGBD H2 se encontra no Classpath, o bean [dataSource] será implementado com H2. No bean [dataSource], é necessário definir também o utilizador e a sua palavra-passe. Aqui, o Spring Boot utilizará o administrador predefinido de H2, que não tem palavra-passe. Como a biblioteca [spring-tx] se encontra no Classpath, será utilizado o gestor de transações do Spring.

Além disso, a pasta onde se encontra a classe [Application] será analisada à procura 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.

Analisemos as linhas 16-17 do código:


ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
  • linha 1: é executado o método estático [run] da classe [SpringApplication] do projeto Spring Boot. 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: solicita-se a este contexto Spring um bean que implemente a interface [CustomerRepository]. Aqui, recuperamos a classe gerada pelo Spring Data para implementar esta interface.

As operações que se seguem limitam-se a utilizar os métodos do bean que implementa a interface [CustomerRepository]. Note-se, na linha 50, que o contexto é encerrado. Os resultados na consola são os seguintes:

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

2014-06-05 16:23:13.877  INFO 11664 --- [           main] hello.Application                        : A iniciar a aplicação no Gportpers3 com PID 11664 (D:\Temp\wksSTS\gs-accessing-data-jpa-complete\target\classes iniciado por ST em D:\Temp\wksSTS\gs-accessing-data-jpa-complete)
2014-06-05 16:23:13.936  INFO 11664 --- [           main] s.c.a.AnnotationConfigApplicationContext: Atualização de org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: data de arranque [Thu Jun 05 16:23:13 CEST 2014]; raiz da hierarquia de contexto
2014-06-05 16:23:15.424  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean: A criar o contentor JPA EntityManagerFactory para a unidade de persistência «default»
2014-06-05 16:23:15.518  INFO 11664 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: A processar 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 não encontrado
2014-06-05 16:23:15.694  INFO 11664 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Nome do fornecedor de bytecode: 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: Utilização do dialeto: org.hibernate.dialect.H2Dialect
2014-06-05 16:23:16.300  INFO 11664 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Utilizando ASTQueryTranslatorFactory
2014-06-05 16:23:16.613  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: A executar a exportação do esquema hbm2ddl
Hibernate: drop table customer if exists
Hibernate: create table customer (id bigint generated by default as identity, first_name varchar(255), last_name varchar(255), primary key (id))
2014-06-05 16:23:16.619  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Esquema de exportação concluído
2014-06-05 16:23:17.074  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : A registar beans para a exposição de JMX no arranque
2014-06-05 16:23:17.094  INFO 11664 --- [           main] hello.Application                        : Aplicação iniciada em 3,906 segundos (JVM em execução há 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 : Fechamento org.springframework.context.annotation.AnnotationConfigApplicationContext@331a8fa0: data de arranque [Thu Jun 05 16:23:13 CEST 2014]; raiz da hierarquia de contexto
2014-06-05 16:23:17.332  INFO 11664 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Desregisto de beans expostos via JMX no encerramento
2014-06-05 16:23:17.333  INFO 11664 --- [           main] j.LocalContainerEntityManagerFactoryBean : Encerramento de JPA EntityManagerFactory para a unidade de persistência «default»
2014-06-05 16:23:17.334  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: A executar a exportação do esquema hbm2ddl
Hibernate: drop table customer if exists
2014-06-05 16:23:17.336  INFO 11664 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Esquema de exportação concluído
  • 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. Trata-se de um contentor de beans;
  • linha 11: o bean [entityManagerFactory] é implementado pela classe [LocalContainerEntityManagerFactory], uma classe do Spring;
  • linha 12: surge a [hibernate]. Foi esta implementação, a JPA, que foi escolhida;
  • linha 19: um dialeto do Hibernate é a variante SQL a utilizar com o SGBD. Aqui, o dialeto [H2Dialect] indica que o Hibernate irá trabalhar com o SGBD e o H2;
  • linhas 22-24: é criada a tabela [CUSTOMER]. Isto significa que o Hibernate foi configurado para gerar as tabelas a partir das definições JPA; neste caso, a definição JPA da classe [Customer];
  • linhas 27-32: registos do Hibernate que mostram as inserções 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 no projeto [gs-accessing-data-jpa-2]:

  

Neste novo projeto, não vamos basear-nos na configuração automática feita pelo Spring Boot. Vamos fazê-la manualmente. Isto pode ser útil se as configurações predefinidas não nos servirem.

Em primeiro lugar, 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>
        <!-- Transações Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.0.5.RELEASE</version>
        </dependency>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.5.2.RELEASE</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>1.0.2.RELEASE</version>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.4.Final</version>
        </dependency>
        <!-- H2 Base de dados -->
        <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: as bibliotecas básicas do Spring;
  • linhas 19-28: as bibliotecas do Spring para gerir transações com uma base de dados;
  • linhas 30-34: Spring Data, utilizado para aceder à base de dados;
  • linhas 36-40: o Spring Boot para iniciar a aplicação;
  • linhas 48-52: o SGBD H2;
  • linhas 54-63: as bases de dados são frequentemente utilizadas com conjuntos de ligações abertas, o que evita a abertura e o encerramento repetidos das ligações. Aqui, a implementação utilizada é a do [commons-dbcp];

Ainda no [pom.xml], altera-se 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] não sofrem alterações. Vamos alterar 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 que a anterior, 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 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 {
    // a fonte de dados H2
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }

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

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

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

}
  • linha 22: a anotação [@Configuration] transforma a classe [Config] numa classe de configuração do Spring;
  • linha 21: a anotação [@EnableJpaRepositories] permite indicar as pastas onde se encontram as interfaces Spring Data [CrudRepository]. Estas interfaces tornar-se-ão componentes 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 no âmbito de uma transação;
  • linha 19: a anotação [@EntityScan] permite indicar as pastas onde as entidades JPA devem ser procuradas. Aqui, foi colocada em comentário, porque esta informação foi fornecida explicitamente na linha 50. Esta anotação deve estar presente se se utilizar o modo [@EnableAutoConfiguration] e as entidades JPA não se encontrarem na mesma pasta que a classe de configuração;
  • linha 18: a anotação [@ComponentScan] permite listar as pastas onde os componentes Spring devem ser procurados. Os componentes Spring são classes marcadas com anotações Spring, tais como @Service, @Component, @Controller, ... Aqui, não existem outros além dos que estão definidos na classe [Config], pelo que a anotação foi colocada em comentário;
  • linhas 25-33: definem a fonte de dados, a base de dados H2. É a anotação @Bean da linha 25 que torna o objeto criado por este método um componente gerido pelo Spring. O nome do método pode ser qualquer um. No entanto, deve ser chamado [dataSource] se o EntityManagerFactory da linha 47 estiver ausente e for definido por autoconfiguração;
  • linha 29: a base de dados chamar-se-á [demo] e será gerada na pasta do projeto;
  • linhas 36-43: definem a implementação JPA utilizada, neste caso uma implementação do Hibernate. O nome do método pode ser qualquer um;
  • linha 39: sem registos SQL;
  • linha 30: a base de dados será criada caso não exista;
  • linhas 46-54: definem o EntityManagerFactory que irá gerir a persistência JPA. O método deve chamar-se obrigatoriamente [entityManagerFactory];
  • linha 47: o método recebe dois parâmetros com o tipo dos dois beans definidos anteriormente. Estes serão então criados e, em seguida, injetados pelo Spring como parâmetros do método;
  • linha 49: define a implementação JPA utilizada;
  • linha 50: define as pastas onde se encontram as entidades JPA;
  • linha 51: define a fonte de dados a gerir;
  • linhas 57-62: o gestor de transações. O método deve chamar-se obrigatoriamente [transactionManager]. Recebe como parâmetro o bean das linhas 46-54;
  • 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. Surge um novo ficheiro na pasta do projeto, o da base de dados H2:

  

Por fim, é possível prescindir do Spring Boot. Cria-se 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]. Pode-se ver na linha 5 que já não existe qualquer dependência do Spring Boot.

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

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

Para criar um arquivo executável do projeto, pode-se proceder da seguinte forma:

  • em [1]: cria-se uma configuração de execução;
  • em [2]: do tipo [Java Application]
  • em [3]: indica o projeto a executar (utilize o botão Browse);
  • em [4]: indica a classe a executar;
  • em [5]: o nome da configuração de execução – pode ser qualquer um;
  • em [6]: exporta-se o projeto;
  • em [7]: sob a forma de um arquivo executável JAR;
  • em [8]: indica o caminho e o nome do ficheiro executável a criar;
  • em [9]: o nome da configuração de execução criada em [5];

Feito isto, abre-se 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 obtidos no terminal 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 para mais detalhes.
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 esqueleto de projeto Spring Data, pode-se proceder da seguinte forma:

  • em [1], cria-se um novo projeto;
  • em [2]: do tipo [Spring Starter Project];
  • o projeto gerado será um projeto Maven. Em [3], indica-se o nome do grupo do projeto;
  • no [4]: indica-se o nome do artefacto (um jar, neste caso) que será criado durante a compilação do projeto;
  • em [5]: indica-se o pacote da classe executável que será criada no projeto;
  • em [6]: o nome do projeto no Eclipse – pode ser qualquer um (não tem de ser idêntico a [4]);
  • em [7]: indica-se que se vai criar um projeto com uma camada [JPA]. As dependências necessárias para esse projeto serão então incluídas no ficheiro [pom.xml];
  • em [8]: o projeto criado;

O ficheiro [pom.xml] integra 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/> <!-- pesquisar o pai no repositório -->
    </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: as dependências necessárias para o JPA – irão incluir o [Spring Data];
  • linhas 13-17: as dependências necessárias para os 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 testes [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 utilizar o ficheiro de configuração [Application]. A classe de teste beneficiará assim de todos os beans que forem definidos por este ficheiro;
  • linha 8: a anotação [@RunWith] permite a integração do Spring com JUnit: a classe poderá ser executada como um teste JUnit. [@RunWith] é uma anotação JUnit (linha 4), enquanto a classe [SpringJUnit4ClassRunner] é uma classe Spring (linha 6);

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

2.3. O projeto Eclipse do servidor

  

Os principais elementos do projeto são os seguintes:

  • [pom.xml]: ficheiro de configuração Maven do projeto;
  • [rdvmedecins.entities]: as entidades JPA;
  • [rdvmedecins.repositories]: as interfaces Spring Data de acesso às entidades JPA;
  • [rdvmedecins.metier]: a camada [métier];
  • [rdvmedecins.domain]: as entidades manipuladas pela camada [métier];
  • [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>
        <!-- utilize UTF-8 para tudo -->
        <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 baseia-se no projeto pai [spring-boot-starter-parent]. Para as dependências já presentes no projeto pai, não se especifica a versão. Será utilizada a versão definida no projeto pai. Quanto às restantes dependências, estas são declaradas normalmente;
  • linhas 14-17: para o Spring Data;
  • linhas 18-22: para os testes JUnit;
  • linhas 23-26: controlador JDBC do SGBD MySQL5;
  • linhas 27-34: conjunto de ligações Commons DBCP;
  • linhas 35-38: biblioteca Jackson de gestão do JSON;
  • linhas 39-43: biblioteca do Google para gestão de coleções;

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

As entidades JPA são os objetos que irão encapsular as linhas das tabelas da base de dados.

  

A classe [AbstractEntity] é a classe pai das entidades [Personne, Creneau, Rv]. 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;
    }

    // inicialização
    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 e setters
    ..
}
  • linha 11: a anotação [@MappedSuperclass] indica que a classe anotada é pai das entidades JPA e [@Entity];
  • linhas 15-17: definem a chave primária [id] de 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 qualquer modo de geração;
  • linhas 18-19: definem a versão de cada entidade. A implementação JPA irá incrementar este número de versão sempre que a entidade for alterada. Este número serve para impedir a atualização simultânea 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 altera E e grava essa alteração na base de dados: o número de versão passa então para V1+1. U2, por sua vez, altera E e grava essa alteração na base de dados: receberá uma exceção, pois possui uma versão (V1) diferente da que consta na base de dados (V1+1);
  • linhas 29-33: o método [build] permite inicializar os dois campos de [AbstractEntity]. Este método torna a referência da instância [AbstractEntity] assim inicializada;
  • linhas 36-44: o método [equals] da classe é redefinido: duas entidades serão consideradas iguais se tiverem o mesmo nome de classe e o mesmo identificador id;

A entidade [Personne] é a classe pai das entidades [Medecin] 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;
    // atributos de uma pessoa
    @Column(length = 5)
    private String titre;
    @Column(length = 20)
    private String nom;
    @Column(length = 20)
    private String prenom;

    // construtor por predefinição
    public Personne() {
    }

    // construtor com parâmetros
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

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

    // getters e setters
    ...
}
  • linha 6: a anotação [@MappedSuperclass] indica que a classe anotada é a classe-pai das entidades JPA e [@Entity];
  • linhas 10-15: uma pessoa tem um título (Melle), um nome próprio (Jacqueline) e um apelido (Tatou). Não é fornecida qualquer informação sobre as colunas da tabela. Por conseguinte, estas terão, por predefinição, 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;

    // construtor por predefinição
    public Medecin() {
    }

    // construtor com parâmetros
    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 [MEDECINS] da base de dados;
  • linha 8: a entidade [Medecin] deriva da entidade [Personne];

Um médico pode ser inicializado da seguinte forma:

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

Se, além disso, se pretender atribuir-lhe um identificador e uma versão, poderá escrever-se:

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;

    // construtor por predefinição
    public Client() {
    }

    // construtor com parâmetros
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

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

}
  • linha 6: a classe é uma entidade JPA;
  • linha 7: associada à tabela [CLIENTS] da base de dados;
  • linha 8: a entidade [Client] deriva da entidade [Personne];

A entidade [Creneau] é 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;
    // características de um horário de RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // um horário está associado a um médico
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

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

    // construtor por predefinição
    public Creneau() {
    }

    // construtor com parâmetros
    public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
        this.medecin = medecin;
        this.hdebut = hdebut;
        this.mdebut = mdebut;
        this.hfin = hfin;
        this.mfin = mfin;
    }

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

    // chave estrangeira
    public long getIdMedecin() {
        return idMedecin;
    }

    // setter - getter
    ...
}
  • linha 10: a classe é uma entidade JPA;
  • linha 11: associada à tabela [CRENEAUX] da base de dados;
  • linha 12: a entidade [Creneau] deriva da entidade [AbstractEntity] e, por conseguinte, herda o identificador [id] e a versão [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 titular do 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 de muitos (intervalos) para um (médico). O atributo [fetch=FetchType.LAZY] indica que, quando se solicita uma entidade [Creneau] ao contexto de persistência e esta tem de ser pesquisada na base de dados, a entidade [Medecin] não é devolvida juntamente com ela. A vantagem deste modo é que a entidade [Medecin] só é pesquisada se o programador o solicitar. Desta forma, poupa-se memória e ganha-se em desempenho;
  • linha 23: indica 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 alterada de duas formas diferentes, o que não é permitido pela norma JPA. Adicionam-se, portanto, os atributos [insertable = false, updatable = false], o que faz com que a coluna só possa 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;

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

    // um RV está associado a um cliente
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // um RV está associado a um intervalo de tempo
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

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

    // fabricante por predefinição
    public Rv() {
    }

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

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

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

    public long getIdClient() {
        return idClient;
    }

    // getters e setters
...
}
  • linha 14: a classe é uma entidade JPA;
  • linha 15: associada à tabela [RV] da base de dados;
  • linha 16: a entidade [Rv] deriva da entidade [AbstractEntity] e, por conseguinte, herda o identificador [id] e a versão [version];
  • linha 21: a data do compromisso;
  • linha 20: o tipo [Date] de Java contém tanto uma data como uma hora. Aqui especifica-se que apenas a data é utilizada;
  • linhas 24-26: o cliente para quem esta marcação foi efetuada. A tabela [RV] possui uma chave estrangeira na tabela [CLIENTS]. Esta relação é representada pelas linhas 24-26;
  • linhas 29-31: o intervalo horário do compromisso. 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]

Vamos implementar a camada [DAO] com o Spring Data:

  

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

  • [ClientRepository]: dá acesso às entidades JPA e [Client];
  • [CreneauRepository]: dá acesso às entidades JPA e [Creneau];
  • [MedecinRepository]: dá acesso às entidades JPA e [Medecin];
  • [RvRepository]: dá acesso às entidades JPA e [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] limita-se a herdar os métodos da interface [CrudRepository] sem adicionar outros;

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] limita-se a herdar os métodos da interface [CrudRepository], sem adicionar outros;

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> {
    // lista dos horários de atendimento de um médico
    @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] permite obter os horários disponíveis de um médico;
  • linha 11: o parâmetro é o identificador do médico. O resultado é uma lista de horários disponíveis na forma de um objeto [Iterable<Creneau>];
  • linha 10: a anotação [@Query] permite 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] permite obter as consultas de um médico para um determinado dia;
  • linha 13: os parâmetros são o identificador 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. Não basta utilizar a seguinte consulta JPQL:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

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

2.7. A camada [métier]

  
  • [IMetier] é a interface da camada [métier] e [Metier] é a sua implementação;
  • [AgendaMedecinJour] e [CreneauMedecinJour] são duas entidades de negócio;

2.7.1. As entidades

A entidade [CreneauMedecinJour] associa um intervalo horário e o eventual compromisso marcado nesse intervalo:


package rdvmedecins.domain;

import java.io.Serializable;

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

public class CreneauMedecinJour implements Serializable {

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

    // construtores
    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 e setters
...
}
  • linha 12: o intervalo horário;
  • linha 13: a eventual consulta – null 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;
    // campos
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;

    // construtores
    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 e setters
...
}
  • linha 13: o médico;
  • linha 14: o dia na agenda;
  • linha 15: os seus intervalos horários com ou sem consultas;

2.7.2. O serviço

A interface da camada [métier] é 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 {

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

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

    // lista de horários disponíveis de um médico
    public List<Creneau> getAllCreneaux(long idMedecin);

    // lista de consultas de um médico, num determinado dia
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour);

    // encontrar um cliente identificado pelo seu ID
    public Client getClientById(long id);

    // encontrar um cliente identificado pelo seu ID
    public Medecin getMedecinById(long id);

    // encontrar uma consulta identificada pelo seu ID
    public Rv getRvById(long id);

    // encontrar um intervalo horário identificado pelo seu ID
    public Creneau getCreneauById(long id);

    // adicionar um RV
    public Rv ajouterRv(Date jour, Creneau créneau, Client client);

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

    // função
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);

}

Os comentários explicam a função de cada um dos métodos.

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 {

    // repositórios
    @Autowired
    private MedecinRepository medecinRepository;
    @Autowired
    private ClientRepository clientRepository;
    @Autowired
    private CreneauRepository creneauRepository;
    @Autowired
    private RvRepository rvRepository;

    // implementação da interface
    @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 do Spring que torna a classe anotada um componente gerido pelo Spring. É possível atribuir ou não um nome a um componente. Este é denominado [métier];
  • linha 25: a classe [Metier] implementa a interface [IMetier];
  • linha 28: a anotação [@Autowired] é uma anotação do Spring. O valor do campo assim anotado será inicializado (injetado) pelo Spring com a referência de um componente do Spring do tipo ou com o nome especificados. Neste caso, a anotação [@Autowired] não especifica nenhum nome. Por conseguinte, será efetuada uma injeção por tipo;
  • linha 29: o campo [medecinRepository] será inicializado com a referência a um componente Spring do tipo [MedecinRepository]. Trata-se da 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 analisadas;
  • linhas 39-41: implementação do método [getAllClients];
  • linha 40: utilizamos o método [findAll] da interface [ClientRepository]. Este método devolve um tipo [Iterable<Client>], que transformamos em [List<Client>] com o método estático [Lists.newArrayList]. A classe [Lists] está definida na biblioteca Google Guava. Em [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 com a ajuda das classes da camada [DAO];

Apenas o método da linha 88 é específico da camada [métier]. Foi colocado aqui porque realiza um processamento específico da área de negócio que não se resume a um simples acesso aos dados. Sem este método, não haveria motivo para criar uma camada [métier]. O método [getAgendaMedecinJour] é o seguinte:


public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        // lista de horários disponíveis do médico
        List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
        // lista de marcações desse mesmo médico para esse mesmo dia
        List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
        // cria-se um dicionário a partir das consultas marcadas
        Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
        for (Rv resa : reservations) {
            hReservations.put(resa.getCreneau().getId(), resa);
        }
        // cria-se a agenda para o dia solicitado
        AgendaMedecinJour agenda = new AgendaMedecinJour();
        // o médico
        agenda.setMedecin(getMedecinById(idMedecin));
        // o dia
        agenda.setJour(jour);
        // os horários disponíveis
        CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
        agenda.setCreneauxMedecinJour(creneauxMedecinJour);
        // preenchimento dos intervalos de marcação
        for (int i = 0; i < creneauxHoraires.size(); i++) {
            // linha i da agenda
            creneauxMedecinJour[i] = new CreneauMedecinJour();
            // intervalo horário
            Creneau créneau = creneauxHoraires.get(i);
            long idCreneau = créneau.getId();
            creneauxMedecinJour[i].setCreneau(créneau);
            // o intervalo está livre ou reservado?
            if (hReservations.containsKey(idCreneau)) {
                // o intervalo está ocupado — regista-se a reserva
                Rv resa = hReservations.get(idCreneau);
                creneauxMedecinJour[i].setRv(resa);
            }
        }
        // retornamos o resultado
        return agenda;
    }

Convidamos o leitor a ler os comentários. O algoritmo é o seguinte:

  • recuperam-se todos os horários disponíveis do médico indicado;
  • recuperam-se todas as suas consultas para o dia indicado;
  • com estas duas informações, é possível determinar se um intervalo horário está livre ou ocupado;

2.8. A configuração do projeto

  

A classe [DomainAndPersitenceConfig] 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 {

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

    // O provedor JPA — não é necessário se estiver satisfeito com os valores predefinidos utilizados pelo Spring Boot
    // aqui define-se para ativar/desativar os registos SQL
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }

    // o EntityManagerFactory e o TransactionManager são definidos com valores predefinidos pelo Spring Boot

}
  • linha 45: não vamos definir os beans [EntityManagerFactory] e [TransactionManager]. Para tal, vamos recorrer à anotação [@EnableAutoConfiguration] do Spring Boot (linha 17);
  • linhas 24-32: definem a fonte de dados MySQL5. Trata-se de um bean que, geralmente, não pode ser detectado pelo Spring Boot;
  • linhas 36-43: configuramos também a implementação JPA para definir o atributo [showSql] do Hibernate como falso (linha 39). Por predefinição, este atributo está definido como verdadeiro;
  • por enquanto, os únicos componentes geridos pelo Spring são os beans das linhas 25 e 37, além dos beans [EntityManagerFactory] e [TransactionManager], por autoconfiguração. Temos de adicionar os beans das camadas [métier] e [DAO];
  • a linha 16 adiciona ao contexto do Spring as interfaces do pacote [rdvmdecins.repositories] que herdam da interface [CrudRepository];
  • a linha 18 adiciona ao contexto do Spring todas as classes do pacote [rdvmedecins] e as suas descendentes que possuam uma anotação do Spring. No pacote [rdvmdecins.metier], a classe [Metier], com a sua anotação [@Service], será encontrada e adicionada ao contexto do Spring;
  • linha 45: um bean [entityManagerFactory] será definido por predefinição pelo Spring Boot. É necessário indicar a este bean onde se encontram as entidades JPA que ele deve gerir. É a linha 19 que faz isso;
  • linha 20: indica que os métodos das interfaces que herdam da interface [CrudRepository] devem ser executados no âmbito de uma transação;

2.9. Os testes da camada [métier]

  

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(){
        // visualização de clientes
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // visualização de médicos
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // visualização dos horários de um médico
        Medecin médecin = medecins.get(0);
        List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
        display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
        // lista de consultas de um médico, num determinado dia
        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));
        // adicionar um 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);
        // verificação
        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));
        // adicionar um RV no mesmo horário do mesmo dia
        // deve provocar uma exceção
        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();
            }
            // regista-se o erro
            erreur = true;
        }
        // verifica-se se ocorreu um erro
        Assert.assertTrue(erreur);
        // lista de RV
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // exibição da agenda
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // eliminar um RV
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // verificação
        rv2 = métier.getRvById(rv.getId());
        Assert.assertNull(rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
    }

    // método utilitário - apresenta os elementos de uma coleção
    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 utilizar o ficheiro de configuração [DomainAndPersistenceConfig] analisado anteriormente. 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 poderá ser executada como um teste JUnit. [@RunWith] é uma anotação JUnit (linha 9), enquanto a classe [SpringJUnit4ClassRunner] é uma classe Spring (linha 12);
  • linhas 26-27: injeção na classe de teste de uma referência à camada [métier];
  • muitos testes são apenas testes visuais simples:
    • 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 das consultas de um médico;
  • linha 50: adição de uma nova consulta. O método [ajouterRv] devolve a consulta com uma informação adicional, a sua chave primária id;
  • linha 53: utiliza-se esta chave primária para procurar a consulta na base de dados;
  • linha 54: verifica-se se a consulta procurada e a consulta encontrada são as mesmas. Recorde-se que o método [equals] da entidade [Rv] foi redefinido: dois compromissos são iguais se tiverem o mesmo id. Aqui, isto mostra-nos que o compromisso adicionado foi efetivamente inserido na base de dados;
  • linhas 61-73: tenta-se adicionar pela segunda vez o mesmo compromisso. Isto deve ser rejeitado pelo SGBD, uma vez que 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 indica que a combinação [JOUR, ID_CRENEAU] deve ser única, o que impede a inserção de dois compromissos no mesmo dia e no mesmo intervalo horário.

  • linha 73: verifica-se se ocorreu efetivamente uma exceção;
  • linha 77: solicita-se a agenda do médico para quem acabou de ser adicionado um compromisso;
  • linha 79: verifica-se se a consulta adicionada consta efetivamente da agenda;
  • linha 82: elimina-se a consulta adicionada;
  • linha 84: procura-se na base de dados a consulta eliminada;
  • linha 85: verifica-se se foi recuperado um ponteiro null, o que indica que a consulta procurada não existe;

A execução do teste foi bem-sucedida:

 

2.10. O programa de consola

  

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


package rdvmedecins.boot;

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

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

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

public class Boot {
    // arranque
    public static void main(String[] args) {
        // prepara-se a configuração
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // iniciamos a configuração
        ConfigurableApplicationContext context = app.run(args);
        // função
        IMetier métier = context.getBean(IMetier.class);
        try {
            // adicionar um RV
            Date jour = new Date();
            System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
            Client client = (Client) new Client().build(1L, 1L);
            Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
            Rv rv = métier.ajouterRv(jour, créneau, client);
            System.out.println(String.format("Rv ajouté = %s", rv));
            // verificação
            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());
        }
        // encerramento do contexto Spring
        context.close();
    }

    // método utilitário - apresenta os elementos de uma coleção
    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] irá 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 devolve um contexto Spring, ou seja, a lista de beans registados;
  • linha 24: obtém-se uma referência ao bean que implementa a interface [IMetier]. Trata-se, portanto, de uma referência à camada [métier];
  • linhas 27-31: adição de um novo compromisso para hoje, para o cliente n.º 1 no horário n.º 1. O cliente e o horário foram criados de raiz para demonstrar que apenas os identificadores são utilizados. Inicializou-se aqui a versão, mas poderia ter-se colocado qualquer valor. Ela não é utilizada aqui;
  • linha 34: queremos saber qual é o médico associado ao intervalo n.º 1. Para isso, precisamos de consultar a base de dados para obter o intervalo n.º 1. Como estamos no modo [FetchType.LAZY], o médico não é apresentado juntamente com o intervalo. No entanto, tivemos o cuidado de prever um campo [idMedecin] na entidade [Creneau] para recuperar a chave primária do médico;
  • linha 35: recupera-se a classe primária do médico;
  • linha 36: exibe-se a lista de consultas do médico;

Os resultados na consola são os seguintes:

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

Passamos agora à construção da camada web. Esta é constituída principalmente por métodos que processam URL específicos e respondem com uma linha de texto no formato JSON (Javascript Object Notation). Esta camada web é uma interface web a que por vezes se chama «API web». Vamos implementar esta interface com o Spring MVC, outro ramo do ecossistema Spring. Começamos por estudar 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 URL padrão e que fornecem texto JSON são frequentemente designados por serviços REST (REpresentational State Transfer). Neste documento, limitarei-me a chamar ao serviço que vamos construir um serviço web / JSON. Um serviço é considerado Restful se respeitar determinadas regras. Não procurei respeitar essas regras.

Vamos agora analisar o projeto importado, começando pela sua configuração 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], encontramos o projeto pai [Spring Boot];
  • 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. É neste servidor que a aplicação será executada;
  • linhas 21-24: a biblioteca Jackson gere o JSON: transformação de um objeto Java numa cadeia JSON e vice-versa;

As bibliotecas incluídas nesta configuração são muito numerosas:

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

2.11.3. A arquitetura de um serviço Spring REST

O Spring MVC implementa o modelo de arquitetura denominado MVC (Modelo – Vista – Controlador) da seguinte forma:

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

  1. pedido – os URL solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... A [Dispatcher Servlet] é a classe do Spring que processa os URL recebidos. Esta «encaminha» o URL para a ação que deve processá-lo. Estas ações são métodos de classes específicas denominadas [Contrôleurs]. O C de MVC é, neste caso, a cadeia [Dispatcher Servlet, Contrôleur, Action]. Se nenhuma ação tiver sido configurada para processar o URL recebido, o servlet [Dispatcher Servlet] responderá que o URL solicitado não foi encontrado (erro 404 NOT FOUND);
  1. processamento
  • a ação selecionada pode utilizar os parâmetros parami que a servlet [Dispatcher Servlet] lhe transmitiu. Estes podem provir de várias fontes:
    • do caminho [/param1/param2/...] do URL,
    • dos parâmetros [p1=v1&p2=v2] do URL,
    • dos parâmetros enviados pelo navegador juntamente com o seu pedido;
  • no processamento do pedido do utilizador, a ação pode necessitar da camada [metier] [2b]. Uma vez processado o pedido do cliente, este pode gerar várias respostas. Um exemplo clássico é:
    • uma página de erro, caso a solicitação não tenha podido ser processada corretamente
    • uma página de confirmação, caso contrário
  • a ação solicita que uma determinada vista seja apresentada [3]. Esta vista irá apresentar dados a que se chama o modelo da vista. É o M de MVC. A ação irá criar este modelo M [2c] e solicitar que uma vista V seja apresentada [3];
  1. resposta — a vista V selecionada utiliza o modelo M criado 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 alterada:

  • em [4a], o modelo, que é uma classe Java, é transformado 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] transforma a classe [GreetingController] num controlador Spring, ou seja, os seus métodos estão registados para processar URL;
  • linha 15: a anotação [@RequestMapping] indica o URL que o método processa, neste caso o URL [/greeting]. Veremos mais adiante que este URL pode ser configurado e que é possível recuperar esses parâmetros;
  • linha 16: a anotação [@ResponseBody] indica que o método não produz um modelo para uma vista (JSP, JSF, Thymeleaf, ...) que será posteriormente enviada para o navegador do cliente, mas produz ela própria a resposta enviada ao navegador. Neste caso, produz um objeto do tipo [Greeting] (linha 18). Embora não seja evidente aqui, este objeto será primeiro transformado em 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, por meio de autoconfiguração, configure 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 denominado [name](@RequestParam(value = "name"). Este pode ser o parâmetro de um GET ou de um POST. Este parâmetro não é obrigatório (required = false). Neste último 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 criará a cadeia de caracteres {"id":n,"content":"texto"}. Por fim, a cadeia 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 através de um método [main] específico das aplicações de consola. É efetivamente esse o caso. A classe [SpringApplication] da linha 12 irá iniciar o servidor Tomcat presente nas dependências e implementar o serviço REST nesse servidor;
  • linha 4: vemos que a classe [SpringApplication] pertence ao projeto [Spring Boot];
  • linha 12: o primeiro parâmetro é a classe que configura o projeto, o segundo são eventuais parâmetros;
  • linha 8: a anotação [@EnableAutoConfiguration] solicita ao Spring Boot que efetue a configuração do projeto;
  • linha 7: a anotação [@ComponentScan] faz com que a pasta que contém a classe [Application] seja explorada para procurar os componentes Spring. Será encontrado um, a classe [GreetingController], que possui a anotação [@Controller], o que a torna um componente Spring;

2.11.7. Execução do projeto

Vamos executar o projeto:

 

Obtêm-se 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 [/**] para o manipulador do tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-11 14:31:39.160  INFO 11744 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] para o manipulador do tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-11 14:31:39.448  INFO 11744 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-06-11 14:31:39.490  INFO 11744 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-06-11 14:31:39.492  INFO 11744 --- [           main] hello.Application                        : Started Application in 3.45 seconds (JVM running for 3.93)
  • 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 o URL [http://localhost:8080/greeting]:

 

Recebemos efetivamente a cadeia esperada JSON. Pode ser interessante ver os cabeçalhos HTTP enviados pelo servidor. Para tal, vamos utilizar o plugin do Chrome denominado [Advanced Rest Client] (ver Anexos):

  • 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], solicita-se o mesmo URL, mas desta vez com um POST;
  • em [7], as informações são enviadas ao servidor na forma [urlencoded];
  • em [6], o parâmetro «name» com o seu valor;
  • em [8], o navegador indica ao servidor que lhe 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;

O procedimento é o seguinte:

  • no [1]: executa-se um objetivo Maven;
  • em [2]: existem dois objetivos (goals): [clean] para eliminar a pasta [target] do projeto Maven e [package] para a regenerar;
  • em [3]: a pasta [target] gerada será criada nesta pasta;
  • no [4]: gera-se o ficheiro de destino;

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

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

Na consola, acedemos à 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-se aceder à mesma através de um navegador:

 

2.11.9. Implantar a aplicação num servidor Tomcat

Embora o Spring Boot seja muito prático no modo de desenvolvimento, é provável que uma aplicação em produção venha a ser implementada num servidor Tomcat real. Eis como proceder:

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

As alterações devem ser feitas em dois locais:

  • linha 9: é necessário indicar que se vai gerar um arquivo WAR (Web ARchive);
  • linhas 26-30: é necessário adicionar uma dependência do artefacto [spring-boot-starter-tomcat]. Este artefacto inclui todas as classes do Tomcat nas dependências do projeto;
  • linha 29: este artefacto é o [provided], ou seja, os arquivos correspondentes não serão incluídos no WAR gerado. Com efeito, esses arquivos estarão disponíveis no servidor Tomcat onde a aplicação será executada;

Além disso, é necessário configurar a aplicação web. Na ausência do ficheiro [web.xml], isto é feito com uma classe que herda de [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] é redefinido (linha 8);
  • linha 10: é fornecida a classe que configura o projeto;

Para executar o projeto, pode-se proceder da seguinte forma:

  • no [1], executa-se o projeto num dos servidores registados no IDE Eclipse;
  • no [2], seleciona-se o [tc Server Developer], que está presente por predefinição. Trata-se de uma variante do Tomcat;

Feito isto, pode-se aceder ao URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] num navegador:

 

Agora já sabemos como gerar um arquivo WAR. Posteriormente, 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, pode-se proceder da seguinte forma:

  • em [1]: Ficheiro / Novo / Projeto Spring Starter
  • em [2]: selecionar [Web]. Não se selecionam bibliotecas de vistas, pois num serviço web / JSON não existem vistas;
  • o projeto criado será um projeto Maven. Em [3], define-se o grupo do artefacto Maven que será criado; em [4], o nome do artefacto;
  • em [5], introduz-se o nome de um pacote onde o Spring irá colocar a classe de configuração do projeto;
  • em [6], atribui-se um nome ao projeto Eclipse – que pode ser diferente de [4];
 

2.12. A camada [web]

  

Vamos construir a camada web em várias etapas:

  • etapa 1: uma camada web operacional sem autenticação;
  • etapa 2: implementação da autenticação com o Spring Security;
  • etapa 3: implementação do CORS [Cross-origin resource sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]. O cliente do nosso serviço web será um cliente web Angular que não pertencerá necessariamente ao mesmo domínio que o nosso serviço web. Por predefinição, não poderá, portanto, aceder ao mesmo, a menos que o serviço web o autorize. 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 pai do Maven;
  • linhas 13-16: as dependências para um projeto Spring MVC;
  • linhas 17-21: as dependências do projeto das camadas [métier, DAO, JPA];

2.12.2. A interface do serviço web

  • em [1], acima, o navegador só pode solicitar um número limitado de URL 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 à transformação JSON de um objeto do tipo [Reponse], como se segue:


package rdvmedecins.web.models;

public class Reponse {

    // ----------------- propriedades
    // estado da operação
    private int status;
    // a resposta JSON
    private Object data;

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

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

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

    // ----------------------getters e setters
...
}
  • linha 7: código de erro da resposta 0: OK, caso contrário: 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 médico [/getAllMedecins]

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

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

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

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

Adicionar uma consulta [/ajouterRv]

  • no [0], o URL do serviço web;
  • em [1], é utilizado o método POST;
  • em [2], o texto JSON das informações transmitidas ao serviço web na forma {dia, idClient, idCreneau};
  • em [3], o cliente indica ao serviço web que lhe 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 envia JSON;
  • em [6]: a resposta JSON do serviço web. O campo [data] contém o formato JSON do compromisso adicionado;

É possível verificar a existência do novo compromisso:

Eliminar um compromisso [/supprimerRv]

  • em [1], o URL do serviço web;
  • em [2], é utilizado o método POST;
  • em [3], o texto JSON das informações transmitidas ao serviço web na forma {idRv};
  • em [4], o cliente indica ao serviço web que lhe está a enviar informações JSON;

A resposta é então a seguinte:

  • em [5]: o campo [status] está a 0, indicando assim que a operação foi bem-sucedida;

É possível verificar a eliminação do compromisso:

Acima, a consulta do doente [Mme GERMAN] já não consta.

O serviço web também permite recuperar entidades através do seu identificador:

Todas estas entidades URL são processadas pelo controlador [RdvMedecinsController], que apresentamos agora.

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() {
        // mensagens de erro da aplicação
        messages = application.getMessages();
    }

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

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

    // lista de horários disponíveis de um médico
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
    }

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

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

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

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

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

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

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

    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(
            @PathVariable("idMedecin") long idMedecin,
            @PathVariable("jour") String jour) {
...
    }
}
  • linha 6: a anotação [@RestController] transforma a classe [RdvMedecinsController] num controlador Spring. Além disso, implica também que os métodos que processam os URL irão gerar uma resposta que será automaticamente transformada em JSON;
  • linhas 9-10: um objeto do tipo [ApplicationModel] será injetado aqui pelo Spring;
  • linha 13: a anotação [@PostConstruct] marca um método a ser executado imediatamente após a instanciação da classe. Quando este é executado, os objetos injetados pelo Spring já estão disponíveis;
  • todos os métodos devolvem um objeto do tipo [Reponse], como se segue:

package rdvmedecins.web.models;

public class Reponse {

    // ----------------- propriedades
    // estado da operação
    private int status;
    // a resposta
    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 de chamada do método. Aqui, o método processa um pedido GET proveniente do URL [/getAllMedecins]. Se esta URL fosse solicitada por um POST, seria recusada e o Spring MVC enviaria um código de erro HTTP ao cliente web;
  • linha 32: o URL é configurado por {idMedecin}. Este parâmetro é recuperado com a anotação [@PathVariable] na linha 33;
  • linha 33: o único parâmetro [long idMedecin] recebe o seu valor do parâmetro {idMedecin} do URL [@PathVariable("idMedecin")]. O parâmetro no URL e o do método podem ter nomes diferentes. É importante referir aqui que o [@PathVariable("idMedecin")] é do tipo String (todo o URL é um String), enquanto o parâmetro [long idMedecin] é do tipo [long]. A alteração de tipo é efetuada automaticamente. É devolvido um código de erro HTTP se essa alteração de tipo falhar;
  • linha 65: a anotação [@RequestBody] designa o corpo da consulta. Numa solicitação GET, quase nunca há corpo (mas é possível incluir um). Numa solicitação POST, na maioria das vezes há corpo (mas é possível não incluir nenhum). Para o URL [ajouterRv], o cliente web envia no seu POST a seguinte cadeia JSON:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

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


package rdvmedecins.web.models;

public class PostAjouterRv {

    // dados da publicação
    private String jour;
    private long idClient;
    private long idCreneau;

    // getters e setters
    ...
}

Também aqui as alterações de tipo necessárias ocorrerão automaticamente;

  • nas linhas 69-70, encontramos um mecanismo semelhante para o URL [/supprimerRv]. A cadeia JSON enviada é a seguinte:
{"idRv":116}

e o tipo [PostSupprimerRv] é o seguinte:


package rdvmedecins.web.models;

public class PostSupprimerRv {

    // dados do post
    private long idRv;

    // getters e setters
    ...
}

2.12.4. Os modelos do serviço web

  

Já apresentámos os modelos [Reponse, PostAjouterRv, PostSupprimerRv]. O modelo [ApplicationModel] é o seguinte:


package rdvmedecins.web.models;

import java.util.Date;
...

@Component
public class ApplicationModel implements IMetier {

    // a camada [métier]
    @Autowired
    private IMetier métier;

    // dados provenientes da camada [métier]
    private List<Medecin> médecins;
    private List<Client> clients;
    // mensagens de erro
   private List<String> messages;

    @PostConstruct
    public void init() {
        // recuperam-se os médicos e os clientes
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }

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

    // ------------------------- interface da camada [métier]
    @Override
    public List<Client> getAllClients() {
        return clients;
    }

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

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

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

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

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

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

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

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

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

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

}
  • linha 6: a anotação [@Component] transforma a classe [ApplicationModel] num componente Spring. Tal como todos os componentes Spring vistos até agora (com exceção de @Controller), será instanciado apenas um objeto deste tipo (singleton);
  • linha 7: a classe [ApplicationModel] implementa a interface [IMetier];
  • linhas 10-11: uma referência na camada [métier] é injetada pelo Spring;
  • linha 19: a anotação [@PostConstruct] faz com que o método [init] seja executado imediatamente após a instanciação da classe [ApplicationModel];
  • linhas 23-24: recuperam-se as listas de médicos e de clientes a partir da camada [métier];
  • linha 26: se ocorrer uma exceção, as mensagens da pilha de exceções são armazenadas no campo da linha 17;

A classe [ApplicationModel] servirá para duas coisas:

  • como cache para armazenar as listas de médicos e de pacientes (clientes);
  • como 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 dos médicos não são armazenados na cache. Para os incluir, basta alterar 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 na classe [ApplicationModel] que será alterada.

2.12.5. A classe Static

A classe [Static] reúne um conjunto de métodos estáticos utilitários que não têm qualquer caráter «de negócio» ou «web»:

  

O seu código é o seguinte:


package rdvmedecins.web.helpers;

import java.text.SimpleDateFormat;
...

public class Static {

    public Static() {
    }

    // lista de mensagens de erro de uma exceção
    public static List<String> getErreursForException(Exception exception) {
        // recupera-se a lista de mensagens de erro da exceção
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }

    // mapeadores Object --> Map
    // --------------------------------------------------------
....
}
  • linha 12: o método [Static.getErreursForException] que foi utilizado (linha 8 abaixo) no método [init] da classe [ApplicationModel]:

    @PostConstruct
    public void init() {
        // recupera-se os médicos e os clientes
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
}

O método cria um objeto [List<String>] com as mensagens de erro [exception.getMessage()] de uma exceção [exception] e das mensagens que esta contém, [exception.getCause()].

A classe [Static] contém outros métodos utilitários aos quais voltaremos quando os encontrarmos.

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

  • 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 parágrafo 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() {
        // mensagens de erro da aplicação
        messages = application.getMessages();
}
  • linha 8: as mensagens de erro armazenadas na aplicação em cache [ApplicationModel] são guardadas localmente no campo da linha 3. Isto permitirá que os métodos saibam se a aplicação foi inicializada corretamente.

2.12.7. O URL [/getAllMedecins]

O URL [/getAllMedecins] é processado pelo seguinte método do controlador [RdvMedecinsController]:


    // lista de médicos
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
    public Reponse getAllMedecins() {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // lista de médicos
        try {
            return new Reponse(0, application.getAllMedecins());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}
  • linha 5: verifica-se se a aplicação foi inicializada corretamente (messages==null). Se não for esse o caso, é devolvida uma resposta com status=-1 e data=messages;
  • linha 10: caso contrário, devolve-se a lista de médicos com um status igual a 0. O método [application.getAllMedecins()] não lança uma exceção, pois limita-se a devolver uma lista que se encontra em cache. No entanto, manteremos este tratamento de exceções para o caso de os médicos já não estarem em cache;

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

Image

Recebemos, de facto, um erro. Num contexto normal, obtemos a seguinte visualização:

2.12.8. O URL [/getAllClients]

O URL [/getAllClients] é processado pelo seguinte método do controlador [RdvMedecinsController]:


    // lista de clientes
    @RequestMapping(value = "/getAllClients")
    public Reponse getAllClients() {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // lista de clientes
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
}

É análogo ao método [getAllMedecins] já analisado. Os resultados obtidos são os seguintes:

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

O URL [/getAllCreneaux/{idMedecin}] é processado pelo seguinte método do controlador [RdvMedecinsController]:


// lista de horários de um médico
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
    public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // recuperar o médico
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // horários do médico
        List<Creneau> créneaux = null;
        try {
            créneaux = application.getAllCreneaux(médecin.getId());
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // envio da resposta
        return new Reponse(0, Static.getListMapForCreneaux(créneaux));
    }
  • linha 9: o médico identificado pelo parâmetro [id] é solicitado a um método local:

    private Reponse getMedecin(long id) {
        // recuperação do médico
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // médico já existente?
        if (médecin == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, médecin);
}

Retorna-se deste método com um status em [0,1,2]. Voltemos ao código do método [getAllCreneaux]:

  • linhas 10-12: se for status!=0, devolve-se imediatamente a resposta;
  • linha 13: recuperamos o médico;
  • linha 17: recuperam-se os horários disponíveis desse médico;
  • linha 22: envia-se como resposta um objeto [Static.getListMapForCreneaux(créneaux)];

Recorde-se a definição da classe [Creneau]:


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

    private static final long serialVersionUID = 1L;
    // características de um horário de RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // um horário está associado a um médico
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // chave estrangeira
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • linha 13: o médico é procurado no modo [FetchType.LAZY];

Recordemos a 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 a junção entre as tabelas [CRENEAUX] e [MEDECINS]. Assim, a consulta devolve todos os horários do médico, com o nome do médico incluído em cada um deles. Quando se serializam esses horários na tabela JSON, a cadeia JSON do médico aparece em cada um deles. Isto é desnecessário. Assim, em vez de serializar um objeto [Creneau], vamos serializar um objeto [Map] no qual incluiremos apenas os campos desejados.

Voltemos ao código analisado inicialmente:


// retornamos a resposta
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) {
        // lista de dicionários <String, Object>
        List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
        for (Creneau créneau : créneaux) {
            liste.add(Static.getMapForCreneau(créneau));
        }
        // retorna a lista
        return liste;
}

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


    // Creneau --> Map
    public static Map<String, Object> getMapForCreneau(Creneau créneau) {
        // há algo a fazer?
        if (créneau == null) {
            return null;
        }
        // dicionário <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());
        // convertemos o dicionário
        return hash;
}
  • linha 8: cria-se um dicionário;
  • linhas 9-13: inserem-se os campos que se pretende manter na cadeia JSON. O campo [medecin] não consta dessa cadeia;
  • linha 15: devolve-se este dicionário;

Os resultados obtidos são os seguintes:

ou estes, caso o intervalo não exista:

ou estes, em caso de erro de acesso à base de dados:

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

O URL [/getRvMedecinJour/{idMedecin}/{jour}] é processado pelo seguinte método do controlador [RdvMedecinsController]:


// lista de consultas de um médico
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // verifica-se a data
        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);
        }
        // recuperamos o médico
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // lista das suas consultas
        List<Rv> rvs = null;
        try {
            rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
        } catch (Exception e1) {
            return new Reponse(4, Static.getErreursForException(e1));
        }
        // envia-se a resposta
        return new Reponse(0, Static.getListMapForRvs(rvs));
}
  • linha 31: é devolvido um objeto List<Map<String,Object>> em vez de um objeto List<Rv>. Recorde-se a definição da classe [Rv]:

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

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

    // uma consulta está associada a um cliente
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // uma consulta está associada a um intervalo de tempo
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

    // chaves externas
    @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 é pesquisado com o modo [FetchType.LAZY];
  • linha 18: o intervalo de tempo é pesquisado com o modo [FetchType.LAZY];

Recordemos a consulta JPQL que procura 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")

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

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

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


    // Rv --> Mapa
    public static Map<String, Object> getMapForRv(Rv rv) {
        // há algo a fazer?
        if (rv == null) {
            return null;
        }
        // dicionário <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()));
        // devolve-se o dicionário
        return hash;
}
  • linha 11: retomamos o dicionário do objeto [Creneau] que apresentámos anteriormente;

Os resultados obtidos são os seguintes:

ou ainda estes, com um dia incorreto:

ou ainda estes, com um médico incorreto:

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

O URL [/getAgendaMedecinJour/{idMedecin}/{jour}] é processado pelo seguinte método do controlador [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // verifica-se a data
        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) });
        }
        // recupera-se o médico
        Reponse réponse = getMedecin(idMedecin);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Medecin médecin = (Medecin) réponse.getData();
        // recupera-se a agenda do médico
        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));
    }
}
  • na linha 30, é devolvido um objeto do tipo List<Map<String,Object>.

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


    // AgendaMedecinJour --> Mapa
    public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
        // há algo a fazer?
        if (agenda == null) {
            return null;
        }
        // dicionário <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);
        // criamos o dicionário
        return hash;
}

O dicionário criado tem três campos:

  • [medecin]: o médico proprietário da agenda. Esta informação foi mantida porque só aparece uma vez, ao passo que, nos casos anteriores, era repetida em cada cadeia JSON;
  • [jour]: o dia da agenda;
  • [creneauxMedecin]: a lista dos horários disponíveis do médico, com uma eventual consulta nesse horário;

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


    // CreneauMedecinJour --> mapa
    public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
        // há algo a fazer?
        if (créneau == null) {
            return null;
        }
        // dicionário <String,Object>
        Map<String, Object> hash = new HashMap<String, Object>();
        hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
        hash.put("rv", getMapForRv(créneau.getRv()));
        // devolvemos o dicionário
        return hash;
}
  • linhas 9-10: utilizam-se os dicionários já analisados para os tipos [Creneau] e [Rv], que, por conseguinte, não incluem o objeto [Medecin];

Os resultados obtidos são os seguintes:

ou estes, caso o dia esteja errado:

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

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

O URL [/getMedecinById/{id}] é processado pelo seguinte método do controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
    public Reponse getMedecinById(@PathVariable("id") long id) {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // recuperar o médico
        return getMedecin(id);
}

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


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

Os resultados obtidos são os seguintes:

ou estes, caso o número do médico esteja incorreto:

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

O URL [/getClientById/{id}] é processado pelo seguinte método do controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
    public Reponse getClientById(@PathVariable("id") long id) {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // a recuperar o cliente
        return getClient(id);
}

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


    private Reponse getClient(long id) {
        // a recuperar o cliente
        Client client = null;
        try {
            client = application.getClientById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // cliente já existe?
        if (client == null) {
            return new Reponse(2, null);
        }
        // ok
        return new Reponse(0, client);
}

Os resultados obtidos são os seguintes:

ou estes, caso o número do cliente esteja incorreto:

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

O URL [/getCreneauById/{id}] é processado pelo seguinte método do controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
    public Reponse getCreneauById(@PathVariable("id") long id) {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // a recuperar o intervalo de tempo
        Reponse réponse = getCreneau(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
        }
        // resultado
        return réponse;
}

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


    private Reponse getCreneau(long id) {
        // a recuperar o intervalo
        Creneau créneau = null;
        try {
            créneau = application.getCreneauById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // intervalo existente?
        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, caso o número do intervalo de tempo esteja incorreto:

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

O URL [/getRvById/{id}] é processado pelo seguinte método do controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
    public Reponse getRvById(@PathVariable("id") long id) {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // a recuperação do rv
        Reponse réponse = getRv(id);
        if (réponse.getStatus() == 0) {
            réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
        }
        // resultado
        return réponse;
}

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


    private Reponse getRv(long id) {
        // a recuperar o Rv
        Rv rv = null;
        try {
            rv = application.getRvById(id);
        } catch (Exception e1) {
            return new Reponse(1, Static.getErreursForException(e1));
        }
        // Rv existente?
        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 --> Mapa
    public static Map<String, Object> getMapForRv2(Rv rv) {
        // há algo a fazer?
        if (rv == null) {
            return null;
        }
        // dicionário <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());
        // retornamos o dicionário
        return hash;
    }

Os resultados obtidos são os seguintes:

ou estes, caso o número da consulta esteja incorreto:

2.12.16. O URL [/ajouterRv]

O URL [/ajouterRv] é processado pelo seguinte método do controlador [RdvMedecinsController]:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // recuperamos os valores enviados
        String jour = post.getJour();
        long idCreneau = post.getIdCreneau();
        long idClient = post.getIdClient();
        // verifica-se a data
        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);
        }
        // recuperamos o intervalo de tempo
        Reponse réponse = getCreneau(idCreneau);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        Creneau créneau = (Creneau) réponse.getData();
        // recuperamos o cliente
        réponse = getClient(idClient);
        if (réponse.getStatus() != 0) {
            réponse.incrStatusBy(2);
            return réponse;
        }
        Client client = (Client) réponse.getData();
        // adiciona-se a marcação
        Rv rv = null;
        try {
            rv = application.ajouterRv(jourAgenda, créneau, client);
        } catch (Exception e1) {
            return new Reponse(5, Static.getErreursForException(e1));
        }
        // retorna a resposta
        return new Reponse(0, Static.getMapForRv(rv));
    }

Não há aqui nada que já não tenha sido visto. Na linha 41, devolve-se o compromisso que foi adicionado na linha 36.

Os resultados obtidos são semelhantes a estes com o cliente [Advanced Rest Client]:

ou então assim, se, por exemplo, for indicado um número de intervalo inexistente:

2.12.17. O URL [/supprimerRv]

O URL [/supprimerRv] é processado pelo seguinte método do controlador [RdvMedecinsController]:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
        // estado da aplicação
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // recuperam-se os valores lançados
        long idRv = post.getIdRv();
        // recuperar o rv
        Reponse réponse = getRv(idRv);
        if (réponse.getStatus() != 0) {
            return réponse;
        }
        // eliminação do Rv
        try {
            application.supprimerRv(idRv);
        } catch (Exception e1) {
            return new Reponse(3, Static.getErreursForException(e1));
        }
        // ok
        return new Reponse(0, null);
    }

Os resultados obtidos pelo são os seguintes:

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

Já terminámos com o controlador. Vamos agora 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: entramos no modo [AutoConfiguration] para que o Spring Boot possa configurar o projeto com base nos ficheiros que encontrar no Classpath do projeto;
  • linha 10: solicita-se que os componentes Spring sejam procurados no pacote [rdvmedecins.web] e nos seus descendentes. É assim que serão encontrados os componentes:
    • [@RestController RdvMedecinsController] no pacote [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] no pacote [rdvmedecins.web.models];
  • linha 11: importa-se a classe [DomainAndPersistenceConfig], que configura o projeto [rdvmedecins-metier-dao] para permitir o 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);
    }
}

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

Os registos de 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 [/**] para o handler do tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:46.851  INFO 9388 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] para o manipulador do tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-06-12 17:30:47.131  INFO 9388 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-06-12 17:30:47.169  INFO 9388 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-06-12 17:30:47.170  INFO 9388 --- [           main] rdvmedecins.web.boot.Boot                : Started Boot in 6.302 seconds (JVM running for 6.906)
2014-06-12 17:30:55.520  INFO 9388 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-06-12 17:30:55.520  INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2014-06-12 17:30:55.538  INFO 9388 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 18 ms
  • linha 17: o servidor Tomcat inicia;
  • linhas 23-31: as camadas [métier, DAO, JPA] são inicializadas;
  • linha 34: o método que processa o URL [/getRvMedecinJour/{idMedecin}/{jour}] foi detetado. Este processo de deteção dos métodos do controlador repete-se até à linha 44;
  • linha 52: o servlet do Spring MVC [DispatcherServlet] está pronto para responder aos pedidos dos clientes web;

Já temos um serviço web operacional que pode ser consultado através de um cliente web. Vamos agora abordar a segurança deste serviço: queremos que apenas determinadas pessoas possam gerir as consultas dos médicos. Para tal, vamos utilizar o framework Spring Security, um componente do ecossistema Spring.

2.13. Introdução ao Spring Security

Vamos importar novamente um guia do Spring, seguindo os passos 1 a 3 abaixo:

  

O projeto é composto pelos seguintes elementos:

  • na pasta [templates], encontram-se 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 analisar o seu ficheiro [pom.xml] para conhecer 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 do framework [Thymeleaf], que permite criar páginas HTML dinâmicas. Este framework pode substituir as páginas JSP (Java Server Pages), que até recentemente eram, por predefinição, o framework de visualizações do Spring MVC;
  • linhas 12-15: dependência do framework Spring Security;

2.13.2. As vistas 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 ao cliente. Este não os vê;
  • linha 12: o atributo [th:href="@{/hello}"] irá gerar o atributo [href] da baliza <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 vista [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] da consulta HTTP atual. Trata-se do nome do utilizador que está ligado;
  • linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da baliza [form]. O valor [@{/logout}] irá gerar o caminho [<context>/logout], 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>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 «Hello [[${#httpServletRequest.remoteUser}]]!»;
  • linha 9: a tradução de @{/logout};
  • linha 11: um campo oculto denominado (atributo name) _csrf;

A última vista [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}"] faz com que a baliza <div> só seja gerada se o URL, que apresenta a página de início de sessão, contiver o parâmetro [error] (http://context/login?error);
  • linha 10: o atributo [th:if="${param.logout}"] faz com que a tag <div> só seja gerada se o URL, que apresenta a página de início de sessão, contiver o parâmetro [logout] (http://context/login?logout);
  • linhas 11-23: um formulário HTML;
  • linha 11: o formulário será enviado para o URL [<context>/login], em que <context> é o contexto da aplicação web;
  • linha 13: um campo de introdução de dados denominado [username];
  • linha 17: um campo de introdução de dados 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>

Note-se, na linha 21, que o Thymeleaf adicionou um campo oculto denominado [_csrf].

2.13.3. Configuração Spring MVC

  

A classe [MvcConfig] configura o framework Spring MVC:


package hello;

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

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

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

}
  • linha 7: a anotação [@Configuration] transforma a classe [MvcConfig] numa classe de configuração;
  • linha 8: a classe [MvcConfig] estende a classe [WebMvcConfigurerAdapter] para redefinir alguns dos seus métodos;
  • linha 10: redefinição de um método da classe pai;
  • linhas 11-16: o método [addViewControllers] permite associar URL a vistas HTML. São feitas as seguintes associações:
URL
vista
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/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:

Acima de [1], as pastas [main] e [resources] são ambas pastas de origem (source folders). Isto significa que o seu conteúdo estará na raiz do Classpath do projeto. Assim, no [2], as pastas [hello] e [templates] estarão na raiz do Classpath.

2.13.4. Configuração do Spring Security

  

A classe [WebSecurityConfig] configura o framework 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] transforma a classe [WebSecurityConfig] numa 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 redefinir alguns dos seus métodos;
  • linha 12: redefinição de um método da classe pai;
  • linhas 13-16: o método [configure(HttpSecurity http)] é redefinido para definir os direitos de acesso às diferentes instâncias URL da aplicação;
  • linha 14: o método [http.authorizeRequests()] permite associar URL 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()
autres URL
Acesso apenas com autenticação
http.anyRequest().authenticated();
  • linha 15: define o método de autenticação. A autenticação é feita através de um formulário URL [/login] acessível a todos [http.formLogin().loginPage("/login").permitAll()]. O logout também está acessível a todos.
  • linhas 19-21: redefinem o método [configure(AuthenticationManagerBuilder auth)] que gere os utilizadores;
  • linha 20: a autenticação é feita com utilizadores definidos de forma «estática» [auth.inMemoryAuthentication()]. Um utilizador é aqui definido com o nome de utilizador [user], a palavra-passe [password] e a função [USER]. É possível conceder os mesmos direitos a utilizadores com a mesma função;

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] solicita ao Spring Boot (linha 3) que efetue a configuração que o programador não terá feito explicitamente;
  • linha 9: transforma a classe [Application] numa classe de configuração do Spring;
  • linha 10: solicita a análise da pasta da classe [Application] para procurar componentes Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão assim detetadas, uma vez que 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 incluído nas dependências Maven do projeto será iniciado e que o projeto será implementado nesse servidor. Vimos que quatro instâncias de URL eram geridas por [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.

2.13.6. Testes da aplicação

Comecemos por solicitar o URL [/], que é um dos quatro URL aceites. Está associado à vista [/templates/home.html]:

 

A URL solicitada, [/], está acessível a todos. Foi por isso que a obtivemos. O link [here] é o seguinte:

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

O URL [/hello] será solicitado quando se clicar no link. Este está protegido:

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

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Acesso apenas com autenticação
http.anyRequest().authenticated();

É necessário estar autenticado para o obter. O Spring Security irá então redirecionar o navegador do cliente para a página de autenticação. De acordo com a configuração apresentada, trata-se da página URL [/login]. Esta página está acessível a todos:


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

Assim, obtemos [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, surge um campo oculto que não consta na página original [login.html]. Foi o Thymeleaf que o adicionou. Este código, denominado CSRF (Cross Site Request Forgery), tem como objetivo eliminar uma falha de segurança. Este token deve ser reenviado ao Spring Security juntamente com a autenticação para que esta seja aceite;

Recordamos que apenas o utilizador «user/password» é 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] provocou a exibição da baliza:


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

Agora, introduzamos os valores esperados para o utilizador/palavra-passe [4]:

  • em [4], identificamo-nos;
  • em [5], o Spring Security redireciona-nos para URL [/hello], pois era URL que estávamos a solicitar quando fomos redirecionados para a página de início de sessão. A identidade do utilizador foi apresentada na 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>

Ao clicar no botão [Sign Out], será efetuado um POST no URL [/logout]. Este, tal como o URL e o [/login], está acessível a todos:


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

Na nossa associação URL / vistas, não definimos nada para o URL [/logout]. O que irá acontecer? Vamos experimentar:

  • em [6], clicamos no botão [Sign Out];
  • em [7], vemos que fomos redirecionados para o URL [http://localhost:8080/login?logout]. Foi o Spring Security que solicitou este redirecionamento. A presença do parâmetro [logout] no URL fez com que fosse apresentada a seguinte linha 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, só depois, implementado a segurança. O Spring Security não é intrusivo. É possível implementar a segurança numa aplicação web já 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, o utilizador é redirecionado para a página de autenticação, com um parâmetro «error» adicional no token URL;
  • se a autenticação for bem-sucedida, o utilizador é redirecionado para a página solicitada no momento em que a autenticação ocorreu. Se se aceder diretamente à página de autenticação sem passar por uma página intermédia, o Spring Security redireciona-nos para o URL [/] (este caso não foi apresentado);
  • desautentificamo-nos ao aceder à página URL [/logout] com um POST. O Spring Security redireciona-nos então para a página de autenticação com o parâmetro «logout» no URL;

Todas estas conclusões baseiam-se nos comportamentos por predefinição do Spring Security. Estes comportamentos podem ser alterados através da configuração, redefinindo determinados métodos da classe [WebSecurityConfigurerAdapter].

O tutorial anterior será de pouca utilidade daqui em diante. Iremos, de facto, utilizar:

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

Existem poucos tutoriais sobre o que pretendemos fazer aqui. A solução que iremos propor é uma compilação de códigos encontrados aqui e ali.

2.14. Implementação da segurança no serviço web de marcação de consultas

2.14.1. A base de dados

A base de dados [rdvmedecins] é atualizada para incluir os utilizadores, as suas palavras-passe e as suas funções. Surgem três novas tabelas:

Image

Tabela [USERS]: os utilizadores

  • ID: chave primária;
  • VERSION: coluna de controlo de versões da linha;
  • IDENTITY: uma identidade descritiva do 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 que encripta as palavras-passe é o algoritmo BCRYPT.

Tabela [ROLES]: as funções

  • ID: chave primária;
  • VERSION: coluna de controlo de versões da 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 agrupar vários utilizadores. Existe uma relação muitos-para-muitos representada pela tabela [USERS_ROLES].

  • ID: chave primária;
  • VERSION: coluna de controlo de versões da linha;
  • USER_ID: identificador de um utilizador;
  • ROLE_ID: identificador de uma função;
 

Como estamos a alterar a base de dados, todas as camadas do projeto [métier, DAO, JPA] têm de ser alteradas:

2.14.2. O novo projeto Eclipse do [métier, DAO, JPA]

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

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

2.14.3. As novas entidades [JPA]

A camada JPA define três novas entidades:

  

A classe [User] é a imagem da 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;

    // características
    private String identity;
    private String login;
    private String password;

    // fabricante
    public User() {
    }

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

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

    // getters e setters
....
}
  • linha 9: a classe estende a classe [AbstractEntity] já utilizada para as outras entidades;
  • linhas 13-15: não se especifica nenhum nome para as colunas, uma vez que estas têm o mesmo nome que os campos que lhes estão associados;

A classe [Role] é o reflexo da 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;

    // características
    private String name;

    // fabricantes
    public Role() {
    }

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

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

    // getters e setters
...
}

A classe [UserRole] é a imagem da 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;

    // um UserRole faz referência a um User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // um UserRole faz referência a um Role
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // getters e setters
...
}
  • linhas 15-17: definem a chave estrangeira da tabela [USERS_ROLES] para a tabela [USERS];
  • linhas 19-21: definem a chave estrangeira da tabela [USERS_ROLES] para a tabela [ROLES];

2.14.4. Alterações na camada [DAO]

A camada [DAO] é enriquecida com três novos [Repository]:

  

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

    // lista de funções de um utilizador identificado pelo seu ID
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // lista de funções de um utilizador identificado pelo seu nome de utilizador e palavra-passe
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // pesquisa de um utilizador através do seu nome de utilizador
    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)] permite obter todas as funções de um utilizador identificado pelo seu [id]
  • linhas 16-17: o mesmo, 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> {

    // pesquisa de uma função através do seu nome
    Role findRoleByName(String name);

}
  • linha 5: a interface [RoleRepository] estende a interface [CrudRepository];
  • linha 8: é possível 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] limita-se a estender a interface [CrudRepository] sem lhe adicionar novos métodos;

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

  

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

 

Esta interface é aqui implementada 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;

    // propriedades
    private User user;
    private UserRepository userRepository;

    // construtores
    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 e 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 permite obter os detalhes desse 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]. Este método deve construir uma coleção de elementos do tipo [GrantedAuthority] ou derivado. Aqui, utilizamos o tipo derivado [SimpleGrantedAuthority] (linha 32), que encapsula o nome de uma das funções do utilizador da linha 15;
  • linhas 31-33: percorre-se a lista de funções do utilizador da linha 15 para construir uma lista de elementos do tipo [SimpleGrantedAuthority];
  • linhas 38-40: implementam o método [getPassword] da interface [UserDetails]. Retorna-se a palavra-passe do utilizador da linha 15;
  • linhas 38-40: implementam o método [getUserName] da interface [UserDetails]. Retorna-se o nome de 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 exige 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 {
        // procura-se o utilizador pelo seu nome de utilizador
        User user = userRepository.findUserByLogin(login);
        // Encontrado?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // retornam os detalhes do utilizador
        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;
  • linha 18: o utilizador é procurado através do seu nome de utilizador;
  • linhas 20-22: se não for encontrado, é lançada uma exceção;
  • linha 24: é criado e apresentado um objeto [AppUserDetails]. Este é, de facto, do tipo [UserDetails] (linha 16);

2.14.6. Testes da camada [DAO]

  

Em primeiro lugar, 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) {
        // sintaxe: nome de utilizador palavra-passe roleName

        // são necessários três parâmetros
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // recuperam-se os parâmetros
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // contexto Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // a função já existe?
        Role role = roleRepository.findRoleByName(roleName);
        // se não existir, criamo-lo
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // o utilizador já existe?
        User user = userRepository.findUserByLogin(login);
        // Se não existir, criamo-lo
        if (user == null) {
            // a palavra-passe é hashada com o bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // guardamos o utilizador
            user = userRepository.save(new User(login, login, crypt));
            // criamos a relação com a função
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // o utilizador já existe — tem a função solicitada?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // se não for encontrado, cria-se a relação com a função
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // encerramento do contexto Spring
        context.close();
    }

}
  • linha 17: a classe espera três argumentos que definem um utilizador: o seu nome de utilizador, a sua palavra-passe e a sua 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 alterada 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: é necessário indicar que agora existem componentes [Repository] no pacote [rdvmedecins.security];
  • linha 4: é necessário indicar que agora existem entidades JPA no pacote [rdvmedecins.security];

Voltemos ao código de criação de um utilizador:

  • linhas 30-32: recuperamos as referências dos três [Repository] que nos podem ser úteis para criar o utilizador;
  • linha 34: verifica-se se a função já existe;
  • linhas 36-38: se não for o caso, criamo-lo na base de dados. Terá um nome do tipo [ROLE_XX];
  • linha 40: verifica-se se o nome de utilizador já existe;
  • linhas 42-49: se o nome de utilizador não existir, é criado na base de dados;
  • linha 44: encripta-se a palavra-passe. Aqui, utiliza-se a classe [BCrypt] do Spring Security (linha 4). Por isso, são necessários os ficheiros deste 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: caso em que o login já exista – verifica-se então se, entre as suas funções, já se encontra a função que se pretende atribuir-lhe;
  • linhas 59-61: se a função procurada não for encontrada, cria-se uma linha na tabela [USERS_ROLES] para associar o utilizador à sua função;
  • não se previu a ocorrência de eventuais exceções. Trata-se de uma classe de suporte para criar rapidamente um utilizador com uma função.

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

Tabela [USERS]

Tabela
 

Tabela [ROLES]

 

Tabela [USERS_ROLES]

 

Consideremos agora 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() {
        // recupera-se o utilizador [admin]
        User user = userRepository.findUserByLogin("admin");
        // verifica-se se a sua palavra-passe é [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // verifica-se a função de administrador / administrador
        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() {
        // recupera-se o utilizador [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // verifica-se se a sua palavra-passe é [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // verifica-se a função de admin / admin
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // método utilitário — apresenta os elementos de uma coleção
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • linhas 27-34: teste visual. São apresentados todos os utilizadores com as respetivas funções;
  • linhas 36-46: verifica-se se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN], utilizando o repositório [UserRepository];
  • linha 41: [admin] é a palavra-passe em texto simples. Na base, está encriptada de acordo com o algoritmo BCrypt. O método [ BCrypt.checkpw] permite verificar se a palavra-passe em texto simples, uma vez encriptada, é efetivamente igual à que se encontra na base;
  • linhas 48-59: verifica-se se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN], utilizando o serviço [appUserDetailsService];

A execução dos testes é bem-sucedida 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 intermédia

A adição das classes necessárias ao Spring Security foi possível com poucas alterações ao projeto original. Recorde-se que:

  • 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 das entidades JPA e dos 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. Teria sido até possível colocá-las numa base de dados separada. Isto foi possível porque se decidiu que um utilizador tinha uma existência independente dos médicos e dos clientes. Se estes últimos fossem utilizadores potenciais, teria sido necessário criar ligações entre a tabela [USERS] e as tabelas [MEDECINS] e [CLIENTS]. Tal teria, então, tido um impacto significativo no projeto existente.

2.14.8. O projeto Eclipse da camada [web]

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

As únicas alterações a efetuar são no pacote [rdvmedecins.web.config], onde é necessário configurar o Spring Security. 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");
    }
}

Vamos seguir 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 aos diferentes URL 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 é assegurada 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 {
        // a autenticação é feita pelo bean [appUserDetailsService]
        // a palavra-passe é encriptada pelo algoritmo de hash Bcrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // a palavra-passe é transmitida através do cabeçalho «Authorization: Basic xxxx»
        http.httpBasic();
        // apenas a função ADMIN pode utilizar a aplicação
        http.authorizeRequests() //
                .antMatchers("/", "/**") // todos os URL
                .hasRole("ADMIN");
    }
}
  • linhas 14-15: foram utilizadas as anotações do exemplo;
  • linhas 17-18: a classe [AppUserDetails], que concede 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 como parâmetro um tipo [AuthenticationManagerBuilder]. Este parâmetro é enriquecido com duas informações:
    • uma referência ao serviço [appUserDetailsService] da linha 18, que concede acesso aos utilizadores registados. Note-se aqui que o facto de estarem registados numa base de dados não é 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 aqui que utilizámos o algoritmo BCrypt;
  • linhas 27-40: o método [configure(HttpSecurity http)] define os direitos de acesso aos URL do serviço web;
  • linha 30: vimos no projeto de introdução que, por predefinição, o Spring Security gerava um token CSRF (Cross Site Request Forgery) que o utilizador que pretendesse autenticar-se tinha de reenviar ao servidor. Aqui, este mecanismo está desativado;
  • linha 32: ativamos o modo de autenticação por cabeçalho HTTP. O cliente deverá enviar o seguinte cabeçalho HTTP:
Authorization:Basic code

onde «code» é a codificação da cadeia «login:password» através do algoritmo Base64. Por exemplo, a codificação Base64 da cadeia admin:admin é YWRtaW46YWRtaW4=. Assim, o 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 URL do serviço web estão acessíveis aos utilizadores com a função [ROLE_ADMIN]. Isto significa que um utilizador que não tenha essa função não pode aceder ao serviço web;

A classe [AppConfig], que configura toda a aplicação, evolui 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 ocorre na linha 11: indica-se que existem agora dois ficheiros de configuração a utilizar: [DomainAndPersistenceConfig] e [SecurityConfig].

2.14.9. Testes do serviço web

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

Authorization:Basic code

onde [code] é o código Base64 da cadeia [login:password]. Para gerar este código, pode utilizar-se o seguinte programa:

  

package rdvmedecins.helpers;

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

public class Base64Encoder {

    public static void main(String[] args) {
        // são esperados dois argumentos: nome de utilizador e palavra-passe
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // recuperam-se os dois argumentos
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // codifica-se a cadeia
        byte[] data = Base64.encode(chaîne.getBytes());
        // exibe-se a sua codificação Base64
        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, iniciamos o serviço web, agora seguro. Em seguida, com o cliente Chrome [Advanced Rest Client], solicitamos a lista de todos os médicos:

  • em [1], solicitamos o URL dos médicos;
  • em [2], utilizando o método GET;
  • em [3], fornecemos o cabeçalho HTTP da autenticação. O código [YWRtaW46YWRtaW4=] é a codificação Base64 da cadeia [admin:admin];
  • em [4], enviamos o comando 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.

Vamos agora tentar uma solicitação HTTP com um cabeçalho de autenticação incorreto. A resposta é então 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 / user. Este 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. É diferente da anterior, que era [401 Unauthorized]. Desta vez, o utilizador autenticou-se corretamente, mas não possui direitos suficientes para aceder ao URL;

2.15. Conclusion

Recordemos a arquitetura global da nossa aplicação cliente/servidor:

Um serviço web seguro está agora operacional. Veremos que terá de ser alterado devido a problemas que surgirão durante a construção do cliente Angular JS. Mas vamos esperar por encontrar o problema para o resolver. Vamos agora construir o cliente Angular que irá oferecer uma interface web para gerir as consultas dos médicos.