Skip to content

8. Estudo de caso

8.1. Introdução

Propomos a criação de uma aplicação web para agendar consultas num consultório médico. Este problema foi abordado no documento «Tutorial AngularJS / Spring 4», disponível no URL [http://tahe.developpez.com/angularjs-spring4/]. A arquitetura desta aplicação era a seguinte:

  • Em [1], um servidor web fornece páginas estáticas a um navegador. Estas páginas contêm uma aplicação AngularJS construída com base no padrão MVC (Model–View–Controller). O modelo aqui abrange tanto as vistas como o domínio, representado aqui pela camada [Services];
  • o utilizador interage com as vistas que lhe são apresentadas no navegador. As suas ações exigirão, por vezes, consultar o servidor Spring 4 [2]. O servidor processará o pedido e devolverá uma resposta JSON (JavaScript Object Notation) [3]. Esta resposta será utilizada para atualizar a vista apresentada ao utilizador.

Propomos pegar nesta aplicação e implementá-la de ponta a ponta utilizando o Spring MVC. A arquitetura passa então a ser a seguinte:

O navegador irá ligar-se a uma aplicação [Web 1] implementada com Spring MVC, que irá recuperar os seus dados de um serviço web [Web 2] também implementado com Spring MVC.

8.2. Funcionalidades da aplicação

Convidamos os leitores a explorar as funcionalidades da aplicação, testando-a. Carregamos os projetos Maven da pasta [case-study] no STS:

Primeiro, vamos criar a base de dados MySQL 5 [dbrdvmedecins] utilizando a ferramenta [Wamp Server] (ver secção 9.5):

  • Em [1], selecione a ferramenta [phpMyAdmin] no WampServer;
  • Em [2], selecione a opção [Importar];
  • Em [3], selecione o ficheiro [database/dbrdvmedecins.sql];
  • em [4], execute-o;
  • em [5], a base de dados é criada.

Em seguida, precisamos de iniciar o servidor ligado à base de dados. Este é o projeto [rdvmedecins-webjson-server]

O servidor estará disponível no URL [http://localhost:8080]. Isto pode ser alterado no ficheiro [application.properties] do projeto:

  

server.port=8080

As credenciais de acesso à base de dados estão armazenadas na classe [DomainAndPersistenceConfig] do projeto [rdvmedecins-metier-dao]:

  

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

Se aceder à base de dados MySQL utilizando credenciais diferentes, é aqui que deve efetuar as alterações.

Em seguida, tal como no servidor anterior, iniciamos o servidor [rdvmedecins-springthymeleaf-server]:

 

Este servidor está disponível por predefinição no URL [http://localhost:8081]. Mais uma vez, isto pode ser configurado no ficheiro [application.properties] do projeto:


server.port=8081

Além disso, este servidor deve conhecer o URL do servidor ligado à base de dados. Esta configuração encontra-se na classe [AppConfig] acima:


    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // racine service web / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout en millisecondes
    private final int TIMEOUT = 5000;
    // CORS
private final boolean CORS_ALLOWED=true;

Se o primeiro servidor foi iniciado numa porta diferente da 8080, deve modificar a linha 5.

Em seguida, utilizando um navegador, aceda ao URL [http://localhost:8081/boot.html]:

  • em [1], a página de início de sessão da aplicação;
  • em [2] e [3], o nome de utilizador e a palavra-passe do utilizador que pretende utilizar a aplicação. Existem dois utilizadores: admin/admin (nome de utilizador/palavra-passe) com a função (ADMIN) e user/user com a função (USER). Apenas a função ADMIN tem permissão para utilizar a aplicação. A função USER existe apenas para demonstrar a resposta do servidor neste caso de utilização;
  • em [4], o botão que permite ligar-se ao servidor;
  • em [5], o idioma da aplicação. Existem duas opções: francês (padrão) e inglês;
  • em [6], o URL do servidor [rdvmedecins-springthymeleaf-server];
  • em [1], faz o login;
  • depois de iniciar sessão, pode escolher o médico que deseja consultar [2] e a data da consulta [3]. Assim que o médico e a data forem selecionados, o calendário é apresentado automaticamente:
  • assim que o calendário do médico for exibido, pode reservar um horário [5];
  • Em [6], selecione o paciente para a consulta e confirme a sua seleção em [7];

Assim que a consulta for confirmada, será automaticamente redirecionado para o calendário, onde a nova consulta já estará listada. Esta consulta pode ser eliminada posteriormente [8].

As principais funcionalidades já foram descritas. São simples. Vamos terminar com as definições de idioma:

Image

  • em [1], pode alternar do francês para o inglês;
  • Em [2], a visualização muda para inglês, incluindo o calendário;

8.3. A base de dados

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

  

As consultas são geridas pelas seguintes tabelas:

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

As tabelas [roles], [users] e [users_roles] estão relacionadas com a autenticação. Por enquanto, não vamos abordá-las. As relações entre as tabelas que gerem as consultas são as seguintes:

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

8.3.1. A tabela [DOCTORS]

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 cada vez que é feita uma alteração na linha.
  • LAST_NAME: o apelido do médico
  • FIRST_NAME: o nome próprio do médico
  • TITLE: o seu título (Sra., Sra., Sr.)

8.3.2. A tabela [CLIENTS]

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

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

8.3.3. A tabela [SLOTS]

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

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

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

8.3.4. A tabela [RV]

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

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

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

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

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

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

8.3.5. Criação da base de dados

Para criar a base de dados [dbrdvmedecins], é fornecido um script [dbrdvmedecins.sql] com os exemplos deste documento [1-3]:

Utilizamos a ferramenta [PhpMyAdmin] do WampServer:

  • Em [1], selecione a ferramenta [phpMyAdmin] do WampServer;
  • em [2], selecione a opção [Importar];
  • em [3], selecione o ficheiro [database/dbrdvmedecins.sql];
  • em [4], execute-o;
  • em [5], a base de dados é criada.

8.4. O Serviço Web / JSON

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

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

O que se segue é uma reprodução do documento [http://tahe.developpez.com/angularjs-spring4/] com algumas modificações.

8.4.1. Introdução ao Spring Data

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

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

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

8.4.1.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.1.10.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <!-- use UTF-8 for everything -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • linhas 5–9: definem um projeto Maven pai. Este projeto define a maioria das dependências do projeto. Estas podem ser suficientes, caso em que não são adicionadas dependências adicionais, ou podem não ser, caso em que as dependências em falta são adicionadas;
  • linhas 12–15: definem uma dependência do [spring-boot-starter-data-jpa]. Este artefacto contém as classes Spring Data;
  • Linhas 16–19: definem uma dependência do SGBD H2, que permite criar e gerir bases de dados na memória.

Vejamos as classes fornecidas por estas dependências:

São muitas:

  • algumas pertencem ao ecossistema Spring (aquelas que começam por spring);
  • outros fazem parte do ecossistema Hibernate (Hibernate, JBoss), e aqui usamos a implementação JPA;
  • outras são bibliotecas de testes (JUnit, Hamcrest);
  • outras são bibliotecas de registo (log4j, logback, slf4j);

Vamos mantê-las todas. Para uma aplicação de produção, apenas as necessárias devem ser mantidas.

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


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

Esta linha está ligada às seguintes linhas:


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

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

8.4.1.2. A camada [JPA]

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

  

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


package hello;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
@Entity
public class Customer {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;
 
    protected Customer() {
    }
 
    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
 
    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }
 
}

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

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

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

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

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

8.4.1.3. A camada [DAO]

  

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


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

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


package org.springframework.data.repository;
 
import java.io.Serializable;
 
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
 
    <S extends T> S save(S entity);
 
    <S extends T> Iterable<S> save(Iterable<S> entities);
 
    T findOne(ID id);
 
    boolean exists(ID id);
 
    Iterable<T> findAll();
 
    Iterable<T> findAll(Iterable<ID> ids);
 
    long count();
 
    void delete(ID id);
 
    void delete(T entity);
 
    void delete(Iterable<? extends T> entities);
 
    void deleteAll();
}

Esta interface define as operações CRUD (Criar – Ler – Atualizar – Eliminar) que podem ser realizadas num tipo JPA T:

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

Voltemos à interface [CustomerRepository]:


package hello;
 
import java.util.List;
 
import org.springframework.data.repository.CrudRepository;
 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
 
    List<Customer> findByLastName(String lastName);
}
  • A linha 9 permite-lhe recuperar um [Cliente] pelo seu [apelido];

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


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

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

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

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

List<Customer> findByLastName(String lastName);

será implementado com código semelhante ao seguinte:

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

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

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

8.4.1.4. A camada [console]

  

A classe [Application] é a seguinte:


package hello;
 
import java.util.List;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
 
@Configuration
@EnableAutoConfiguration
public class Application {
 
    public static void main(String[] args) {
 
        ConfigurableApplicationContext context = SpringApplication.run(Application.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
 
        // save a couple of customers
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));
 
        // fetch all customers
        Iterable<Customer> customers = repository.findAll();
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : customers) {
            System.out.println(customer);
        }
        System.out.println();
 
        // fetch an individual customer by ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();
 
        // fetch customers by last name
        List<Customer> bauers = repository.findByLastName("Bauer");
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : bauers) {
            System.out.println(bauer);
        }
 
        context.close();
    }
 
}
  • Linha 10: indica que a classe é utilizada para configurar o Spring. As versões recentes do Spring podem, de facto, ser configuradas em Java em vez de em XML. Ambos os métodos podem ser utilizados simultaneamente. No código de uma classe anotada com [Configuration], encontramos normalmente beans do Spring, ou seja, definições de classes a instanciar. Aqui, não estão definidos quaisquer beans. É importante notar aqui que, ao trabalhar com um SGBD, devem ser definidos vários beans do Spring:
    • um [EntityManagerFactory] que define a implementação JPA a utilizar,
    • um [DataSource] que define a fonte de dados a utilizar,
    • um [TransactionManager] que define o gestor de transações a utilizar;

Aqui, nenhum destes beans está definido.

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

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

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


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

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

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

2014-12-19 11:13:46.612  INFO 10932 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 10932 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\etude-de-cas\gs-accessing-data-jpa-complete)
2014-12-19 11:13:46.658  INFO 10932 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@279ad2e3: startup date [Fri Dec 19 11:13:46 CET 2014]; root of context hierarchy
2014-12-19 11:13:48.234  INFO 10932 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2014-12-19 11:13:48.258  INFO 10932 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2014-12-19 11:13:48.337  INFO 10932 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.7.Final}
2014-12-19 11:13:48.339  INFO 10932 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2014-12-19 11:13:48.341  INFO 10932 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2014-12-19 11:13:48.620  INFO 10932 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2014-12-19 11:13:48.689  INFO 10932 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2014-12-19 11:13:48.853  INFO 10932 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2014-12-19 11:13:49.143  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2014-12-19 11:13:49.151  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2014-12-19 11:13:49.692  INFO 10932 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-12-19 11:13:49.709  INFO 10932 --- [           main] hello.Application                        : Started Application in 3.461 seconds (JVM running for 4.435)
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']
2014-12-19 11:13:49.931  INFO 10932 --- [           main] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@279ad2e3: startup date [Fri Dec 19 11:13:46 CET 2014]; root of context hierarchy
2014-12-19 11:13:49.933  INFO 10932 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2014-12-19 11:13:49.934  INFO 10932 --- [           main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2014-12-19 11:13:49.935  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2014-12-19 11:13:49.938  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
  • linhas 1-8: o logótipo do projeto Spring Boot;
  • linha 9: a classe [hello.Application] é executada;
  • linha 10: [AnnotationConfigApplicationContext] é uma classe que implementa a interface [ApplicationContext] do Spring. É um contentor de beans;
  • linha 11: o bean [entityManagerFactory] é implementado utilizando a classe [LocalContainerEntityManagerFactory], uma classe do Spring;
  • linha 15: aparece [Hibernate]. Esta é a implementação JPA que foi escolhida;
  • linha 19: um dialeto Hibernate é a variante SQL a ser utilizada com o SGBD. Aqui, o dialeto [H2Dialect] indica que o Hibernate irá funcionar com o SGBD H2;
  • linhas 21–22: a base de dados é criada. A tabela [CUSTOMER] é criada. Isto significa que o Hibernate foi configurado para gerar tabelas a partir de definições JPA, neste caso a definição JPA da classe [Customer];
  • linhas 27–31: os cinco clientes são inseridos;
  • linhas 33–35: resultado do método [findOne] da interface;
  • linhas 37–40: resultados do método [findByLastName];
  • Linhas 41 e seguintes: registos do encerramento do contexto Spring.

8.4.1.5. Configuração manual do projeto Spring Data

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

  

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

Primeiro, vamos especificar as dependências necessárias no ficheiro [pom.xml]:


...
    <dependencies>
        <!-- Spring Core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring transactions -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring ORM -->        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.7.1.RELEASE</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>1.1.10.RELEASE</version>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.4.Final</version>
        </dependency>
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.178</version>
        </dependency>
        <!-- Commons DBCP -->
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
            <version>1.6</version>
        </dependency>
    </dependencies>
...
 
</project>
  • linhas 2–18: Bibliotecas principais do Spring;
  • linhas 19–29: Bibliotecas Spring para gerir transações de bases de dados;
  • linhas 30–35: a biblioteca Spring para trabalhar com um ORM (Mapeador Objeto-Relacional);
  • linhas 36–41: Spring Data utilizado para aceder à base de dados;
  • linhas 42–47: Spring Boot para iniciar a aplicação;
  • linhas 54–59: o SGBD H2;
  • linhas 60–70: as bases de dados são frequentemente utilizadas com pools de ligações, o que evita a abertura e o encerramento repetidos de ligações. Aqui, a implementação utilizada é [commons-dbcp];

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


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

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

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

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


package demo.console;
 
import java.util.List;
 
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
 
public class Main {
 
    public static void main(String[] args) {
 
        ConfigurableApplicationContext context = SpringApplication.run(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
...
 
        context.close();
    }
 
}
  • linha 12: a classe [Main] já não tem quaisquer anotações de configuração;
  • linha 16: a aplicação é iniciada com o Spring Boot. O parâmetro [Config.class] é a nova classe de configuração do projeto;

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


package demo.config;
 
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
 
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
    // h2 data source
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }
 
    // the provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("demo.entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • linha 22: a anotação [@Configuration] torna a classe [Config] uma classe de configuração do Spring;
  • linha 21: a anotação [@EnableJpaRepositories] especifica os diretórios onde se encontram as interfaces [CrudRepository] do Spring Data. Estas interfaces tornar-se-ão componentes do Spring e estarão disponíveis no seu contexto;
  • linha 20: a anotação [@EnableTransactionManagement] indica que os métodos das interfaces [CrudRepository] devem ser executados dentro de uma transação;
  • linha 19: a anotação [@EntityScan] especifica os diretórios onde as entidades JPA devem ser pesquisadas. Aqui, ela foi comentada porque esta informação foi explicitamente fornecida na linha 50. Esta anotação deve estar presente se estiver a utilizar o modo [@EnableAutoConfiguration] e as entidades JPA não estiverem no mesmo diretório que a classe de configuração;
  • linha 18: a anotação [@ComponentScan] permite indicar os diretórios onde os componentes Spring devem ser procurados. Os componentes Spring são classes marcadas com anotações Spring, tais como @Service, @Component, @Controller, etc. Aqui, não existem outros além dos definidos na classe [Config], pelo que a anotação foi comentada;
  • Linhas 25–33: definem a fonte de dados, a base de dados H2. É a anotação @Bean na linha 25 que torna o objeto criado por este método um componente gerido pelo Spring. O nome do método aqui pode ser qualquer um. No entanto, deve ser nomeado [dataSource] se o EntityManagerFactory na linha 47 estiver ausente e for definido através da configuração automática;
  • linha 29: a base de dados será denominada [demo] e será gerada na pasta do projeto;
  • Linhas 36–43: definem a implementação JPA utilizada, neste caso uma implementação Hibernate. O nome do método pode ser qualquer um aqui;
  • linha 39: sem registos SQL;
  • linha 30: a base de dados será criada se não existir;
  • linhas 46–54: definem o EntityManagerFactory que irá gerir a persistência JPA. O método deve ser denominado [entityManagerFactory];
  • linha 47: o método recebe dois parâmetros dos tipos dos dois beans definidos anteriormente. Estes serão então construídos e injetados pelo Spring como parâmetros do método;
  • linha 49: define a implementação JPA a ser utilizada;
  • linha 50: especifica os diretórios onde as entidades JPA podem ser encontradas;
  • linha 51: define a fonte de dados a ser gerida;
  • linhas 57–62: o gestor de transações. O método deve ser denominado [transactionManager]. Recebe o bean das linhas 46–54 como parâmetro;
  • linha 60: o gestor de transações é associado ao EntityManagerFactory;

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

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

  

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

  

A classe [Main2] tem o seguinte código:


package demo.console;
 
import java.util.List;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
 
public class Main2 {

    public static void main(String[] args) {
 
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
....
 
        context.close();
    }
 
}
  • Linha 15: A classe de configuração [Config] é agora utilizada pela classe Spring [AnnotationConfigApplicationContext]. Como se pode ver na linha 5, já não existem dependências do Spring Boot.

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

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

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

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

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

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

O arquivo é executado da seguinte forma:


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

Os resultados apresentados na consola são os seguintes:

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

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

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

8.4.1.7. Criar um novo projeto Spring Data

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

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

O ficheiro [pom.xml] inclui as dependências necessárias para um projeto JPA:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
  • linhas 9–12: dependências necessárias para o JPA — incluirão [Spring Data];
  • linhas 13–17: dependências necessárias para testes JUnit integrados com o Spring;

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


package istia.st;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

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


package istia.st;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
 
    @Test
    public void contextLoads() {
    }
 
}
  • Linha 9: A anotação [@SpringApplicationConfiguration] permite que o ficheiro de configuração [Application] seja utilizado. A classe de teste irá, assim, beneficiar de todos os beans definidos neste ficheiro;
  • linha 8: a anotação [@RunWith] permite a integração do Spring com o JUnit: a classe poderá ser executada como um teste JUnit. [@RunWith] é uma anotação do JUnit (linha 4), enquanto a classe [SpringJUnit4ClassRunner] é uma classe do Spring (linha 6);

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

8.4.2. O projeto de servidor do Eclipse

  

Os principais componentes do projeto são os seguintes:

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

8.4.3. 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.2.6.RELEASE</version>
        </parent>
        <dependencies>
                <!-- Spring Data JPA -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-data-jpa</artifactId>
                </dependency>
                <!-- Spring test -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <!-- Spring security -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-security</artifactId>
                </dependency>
                <!-- driver JDBC / MySQL -->
                <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                </dependency>
                <!-- Tomcat JDBC -->
                <dependency>
                        <groupId>org.apache.tomcat</groupId>
                        <artifactId>tomcat-jdbc</artifactId>
                </dependency>
                <!-- mapper jSON -->
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-databind</artifactId>
                </dependency>
                <!-- Googe Guava -->
                <dependency>
                        <groupId>com.google.guava</groupId>
                        <artifactId>guava</artifactId>
                        <version>16.0.1</version>
                </dependency>
        </dependencies>
        <properties>
                <!-- use UTF-8 for everything -->
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
                <start-class>rdvmedecins.boot.Boot</start-class>
                <java.version>1.8</java.version>
        </properties>
        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>
        <repositories>
                <repository>
                        <id>spring-milestones</id>
                        <name>Spring Milestones</name>
                        <url>http://repo.spring.io/libs-milestone</url>
                        <snapshots>
                                <enabled>false</enabled>
                        </snapshots>
                </repository>
                <repository>
                        <id>org.jboss.repository.releases</id>
                        <name>JBoss Maven Release Repository</name>
                        <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
                        <snapshots>
                                <enabled>false</enabled>
                        </snapshots>
                </repository>
        </repositories>
        <pluginRepositories>
                <pluginRepository>
                        <id>spring-milestones</id>
                        <name>Spring Milestones</name>
                        <url>http://repo.spring.io/libs-milestone</url>
                        <snapshots>
                                <enabled>false</enabled>
                        </snapshots>
                </pluginRepository>
        </pluginRepositories>
</project>
  • linhas 8–12: O projeto depende do projeto pai [spring-boot-starter-parent]. Para as dependências já presentes no projeto pai, não é especificada nenhuma versão. Será utilizada a versão definida no projeto pai. As outras dependências são declaradas como habitualmente;
  • linhas 15–18: para o Spring Data;
  • linhas 20–24: para testes JUnit;
  • linhas 26–29: para a biblioteca Spring Security, cuja camada [DAO] utiliza uma das classes de encriptação de palavras-passe;
  • linhas 31–34: controlador JDBC para o SGBD MySQL5;
  • linhas 36–39: Pool de ligações JDBC do Tomcat. Um pool de ligações reúne as ligações abertas a uma base de dados. Quando o código pretende abrir uma ligação, solicita-a ao pool. Quando o código fecha a ligação, esta não é encerrada, mas devolvida ao pool. Tudo isto acontece de forma transparente ao nível do código. O desempenho é melhorado porque abrir e fechar repetidamente uma ligação demora tempo. Aqui, o conjunto de ligações estabelece um determinado número de ligações à base de dados aquando da instância. Depois disso, não há abertura nem encerramento de ligações, a menos que o número de ligações armazenadas no conjunto se revele insuficiente. Nesse caso, o conjunto cria automaticamente novas ligações;
  • linhas 41–44: biblioteca Jackson para tratamento de JSON;
  • linhas 46–50: biblioteca Google Collections;

8.4.4. Entidades JPA

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

  

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


package rdvmedecins.entities;
 
import java.io.Serializable;
 
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
 
@MappedSuperclass
public class AbstractEntity implements Serializable {
 
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;
    @Version
    protected Long version;
 
    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }
 
    // initialization
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }
 
        @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1) || entity==null) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id.longValue() == other.id.longValue();
    }
 
 
    // getters and setters
    ..
}
  • linha 11: a anotação [@MappedSuperclass] indica que a classe anotada é uma superclasse das entidades JPA [@Entity];
  • linhas 15–17: definem a chave primária [id] para cada entidade. É a anotação [@Id] que torna o campo [id] uma chave primária. A anotação [@GeneratedValue(strategy = GenerationType.IDENTITY)] indica que o valor desta chave primária é gerado pelo SGBD e que o modo de geração [IDENTITY] é aplicado. Para o SGBD MySQL, isto significa que as chaves primárias serão geradas pelo SGBD com o atributo [AUTO_INCREMENT]
  • Linhas 18–19: definem a versão de cada entidade. A implementação JPA incrementará este número de versão sempre que a entidade for modificada. Este número é utilizado para impedir atualizações simultâneas da entidade por dois utilizadores diferentes: dois utilizadores, U1 e U2, leem a entidade E com um número de versão igual a V1. U1 modifica E e persiste esta alteração na base de dados: o número de versão passa então a V1+1. U2, por sua vez, modifica E e persiste essa alteração na base de dados: receberá uma exceção porque a sua versão (V1) difere da que se encontra na base de dados (V1+1);
  • linhas 29–33: o método [build] inicializa os dois campos de [AbstractEntity]. Este método devolve uma referência à instância de [AbstractEntity] assim inicializada;
  • linhas 36–44: O método [equals] da classe é reescrito: duas entidades são consideradas iguais se tiverem o mesmo nome de classe e o mesmo identificador id;
  • linhas 21–26: ao substituir o método [equals] de uma classe, o seu método [hashCode] também deve ser substituído (linhas 21–26). A regra é que duas entidades consideradas iguais pelo método [equals] também devem ter o mesmo [hashCode]. Aqui, o [hashCode] de uma entidade é igual à sua chave primária [id]. O [hashCode] de uma classe é utilizado, em particular, na gestão de dicionários cujos valores são instâncias da classe;

A entidade [Person] é a classe pai das entidades [Doctor] e [Client]:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
 
@MappedSuperclass
public class Personne extends AbstractEntity {
    private static final long serialVersionUID = 1L;
    // attributes of a person
    @Column(length = 5)
    private String titre;
    @Column(length = 20)
    private String nom;
    @Column(length = 20)
    private String prenom;
 
    // default builder
    public Personne() {
    }
 
    // builder with parameters
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }
 
    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }
 
    // getters and setters
    ...
}
  • linha 6: a anotação [@MappedSuperclass] indica que a classe anotada é uma superclasse das entidades JPA [@Entity];
  • linhas 10–15: uma pessoa tem um título (Sra.), um nome próprio (Jacqueline) e um apelido (Tatou). Não é fornecida qualquer informação sobre as colunas da tabela. Por predefinição, estas terão, portanto, os mesmos nomes que os campos;

A entidade [Medecin] é a seguinte:


package rdvmedecins.entities;
 
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
 
    private static final long serialVersionUID = 1L;
 
    // default builder
    public Medecin() {
    }
 
    // builder with parameters
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }
 
    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }
 
}
  • linha 6: a classe é uma entidade JPA;
  • linha 7: associada à tabela [DOCTORS] na base de dados;
  • linha 8: a entidade [Doctor] deriva da entidade [Person];

Um médico pode ser inicializado da seguinte forma:

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

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

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

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

A entidade [Client] é a seguinte:


package rdvmedecins.entities;
 
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "clients")
public class Client extends Personne {
 
    private static final long serialVersionUID = 1L;
 
    // default builder
    public Client() {
    }
 
    // builder with parameters
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }
 
    // identity
    public String toString() {
        return String.format("Client[%s]", super.toString());
    }
 
}
  • linha 6: a classe é uma entidade JPA;
  • linha 7: associada à tabela [CLIENTS] na base de dados;
  • linha 8: a entidade [Client] deriva da entidade [Person];

A entidade [TimeSlot] é a seguinte:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
 
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
 
    // default builder
    public Creneau() {
    }
 
    // builder with parameters
    public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
        this.medecin = medecin;
        this.hdebut = hdebut;
        this.mdebut = mdebut;
        this.hfin = hfin;
        this.mfin = mfin;
    }
 
    // toString
    public String toString() {
        return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
    }
 
    // foreign key
    public long getIdMedecin() {
        return idMedecin;
    }
 
    // setters - getters
    ...
}
  • linha 10: a classe é uma entidade JPA;
  • linha 11: associada à tabela [CRENEAUX] na base de dados;
  • linha 12: a entidade [Creneau] deriva da entidade [AbstractEntity] e, portanto, herda os campos [id] e [version];
  • linha 16: hora de início do intervalo (14);
  • linha 17: minutos de início do intervalo (20);
  • linha 18: hora de fim do intervalo (14);
  • linha 19: minutos de fim do intervalo (40);
  • linhas 22–24: o médico responsável pelo horário. A tabela [CRENEAUX] possui uma chave estrangeira na tabela [MEDECINS]. Esta relação é representada pelas linhas 22–24;
  • linha 22: a anotação [@ManyToOne] indica uma relação muitos-para-um (espaços para médico). O atributo [fetch=FetchType.LAZY] indica que, quando uma entidade [Slot] é solicitada a partir do contexto de persistência e tem de ser recuperada da base de dados, a entidade [Doctor] não é devolvida com ela. A vantagem deste modo é que a entidade [Doctor] só é recuperada se o programador a solicitar. Isto poupa memória e melhora o desempenho;
  • linha 23: especifica o nome da coluna da chave estrangeira na tabela [CRENEAUX];
  • linhas 27–28: a chave estrangeira na tabela [MEDECINS];
  • linha 27: a coluna [ID_MEDECIN] já foi utilizada na linha 23. Isto significa que pode ser modificada de duas formas diferentes, o que não é permitido pela norma JPA. Por isso, adicionamos os atributos [insertable = false, updatable = false], garantindo que a coluna é de leitura única;

A entidade [Rv] é a seguinte:


package rdvmedecins.entities;
 
import java.util.Date;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
 
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // characteristics of an Rv
    @Temporal(TemporalType.DATE)
    private Date jour;
 
    // an appointment is linked to a customer
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;
 
    // an appointment is linked to a time slot
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;
 
    // foreign keys
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;
 
    // default builder
    public Rv() {
    }
 
    // with parameters
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }
 
    // toString
    public String toString() {
        return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
    }
 
    // foreign keys
    public long getIdCreneau() {
        return idCreneau;
    }
 
    public long getIdClient() {
        return idClient;
    }
 
    // getters and setters
...
}
  • linha 14: a classe é uma entidade JPA;
  • linha 15: associada à tabela [RV] na base de dados;
  • linha 16: a entidade [Rv] deriva da entidade [AbstractEntity] e, portanto, herda os campos [id] e [version];
  • linha 21: a data do compromisso;
  • linha 20: o tipo Java [Date] contém tanto uma data como uma hora. Aqui especificamos que apenas a data é utilizada;
  • linhas 24–26: o cliente para quem esta marcação foi feita. A tabela [RV] tem uma chave estrangeira na tabela [CLIENTS]. Esta relação é representada pelas linhas 24–26;
  • linhas 29–31: o intervalo de tempo da marcação. A tabela [RV] possui uma chave estrangeira na tabela [CRENEAUX]. Esta relação é representada pelas linhas 29–31;
  • linhas 34–35: a chave estrangeira [idClient];
  • linhas 36–37: a chave estrangeira [idCreneau];

8.4.5. A camada [DAO]

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

  

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

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

A interface [MedecinRepository] é a seguinte:


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

A interface [ClientRepository] é a seguinte:


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

A interface [CreneauRepository] é a seguinte:


package rdvmedecins.repositories;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Creneau;
 
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
    // liste des créneaux horaires d'un médecin
    @Query("select c from Creneau c where c.medecin.id=?1")
    Iterable<Creneau> getAllCreneaux(long idMedecin);
}
  • linha 8: a interface [CreneauRepository] herda os métodos da interface [CrudRepository];
  • linhas 10-11: o método [getAllCreneaux] recupera os horários disponíveis de um médico;
  • linha 11: o parâmetro é o ID do médico. O resultado é uma lista de horários na forma de um objeto [Iterable<Creneau>];
  • linha 10: a anotação [@Query] é utilizada para especificar a consulta JPQL (Java Persistence Query Language) que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método;

A interface [RvRepository] é a seguinte:


package rdvmedecins.repositories;
 
import java.util.Date;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Rv;
 
public interface RvRepository extends CrudRepository<Rv, Long> {
 
    @Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
    Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
  • linha 10: a interface [RvRepository] herda os métodos da interface [CrudRepository];
  • linhas 12–13: o método [getRvMedecinJour] recupera as consultas de um médico para um determinado dia;
  • linha 13: os parâmetros são o ID do médico e o dia. O resultado é uma lista de consultas na forma de um objeto [Iterable<Rv>];
  • linha 12: a anotação [@Query] permite especificar a consulta JPQL que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método, e o parâmetro [?2] será substituído pelo parâmetro [jour] do método. A seguinte consulta JPQL não é suficiente:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

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

8.4.6. A camada [business]

  
  • [IMetier] é a interface para a camada [business], e [Metier] é a sua implementação;
  • [Doctor'sDailySchedule] e [Doctor'sDailyTimeSlot] são duas entidades de negócio;

8.4.6.1. As entidades

A entidade [CreneauMedecinJour] associa um intervalo de tempo a qualquer consulta marcada dentro desse intervalo:


package rdvmedecins.domain;
 
import java.io.Serializable;
 
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
 
public class CreneauMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Creneau creneau;
    private Rv rv;
 
    // manufacturers
    public CreneauMedecinJour() {
 
    }
 
    public CreneauMedecinJour(Creneau creneau, Rv rv) {
        this.creneau=creneau;
        this.rv=rv;
    }
 
    // toString
    @Override
    public String toString() {
        return String.format("[%s %s]", creneau, rv);
    }
 
    // getters and setters
...
}
  • linha 12: o intervalo de tempo;
  • linha 13: a consulta, se houver – nulo caso contrário;

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


package rdvmedecins.domain;
 
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
 
import rdvmedecins.entities.Medecin;
 
public class AgendaMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;
 
    // manufacturers
    public AgendaMedecinJour() {
 
    }
 
    public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
        this.medecin = medecin;
        this.jour = jour;
        this.creneauxMedecinJour = creneauxMedecinJour;
    }
 
    public String toString() {
        StringBuffer str = new StringBuffer("");
        for (CreneauMedecinJour cr : creneauxMedecinJour) {
            str.append(" ");
            str.append(cr.toString());
        }
        return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
    }
 
    // getters and setters
...
}
  • linha 13: o médico;
  • linha 14: o dia no calendário;
  • linha 15: os horários disponíveis, com ou sem marcação;

8.4.6.2. O serviço

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


package rdvmedecins.metier;
 
import java.util.Date;
import java.util.List;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
 
public interface IMetier {
 
    // customer list
    public List<Client> getAllClients();
 
    // list of doctors
    public List<Medecin> getAllMedecins();
 
    // list of physician slots
    public List<Creneau> getAllCreneaux(long idMedecin);
 
    // list of doctor's appointments on a given day
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
 
    // find a customer identified by its id
    public Client getClientById(long id);
 
    // find a customer identified by its id
    public Medecin getMedecinById(long id);
 
    // find an Rv identified by its id
    public Rv getRvById(long id);
 
    // find a time slot identified by its id
    public Creneau getCreneauById(long id);
 
    // add a RV
    public Rv ajouterRv(Date jour, Creneau créneau, Client client);
 
    // delete a RV
    public void supprimerRv(Rv rv);
 
    // job
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
 
}

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

A implementação da interface [IMetier] é a seguinte classe [Metier]:


package rdvmedecins.metier;
 
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
 
import com.google.common.collect.Lists;
 
@Service("métier")
public class Metier implements IMetier {
 
    // repositories
    @Autowired
    private MedecinRepository medecinRepository;
    @Autowired
    private ClientRepository clientRepository;
    @Autowired
    private CreneauRepository creneauRepository;
    @Autowired
    private RvRepository rvRepository;
 
    // interface implementation
    @Override
    public List<Client> getAllClients() {
        return Lists.newArrayList(clientRepository.findAll());
    }
 
    @Override
    public List<Medecin> getAllMedecins() {
        return Lists.newArrayList(medecinRepository.findAll());
    }
 
    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
    }
 
    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
    }
 
    @Override
    public Client getClientById(long id) {
        return clientRepository.findOne(id);
    }
 
    @Override
    public Medecin getMedecinById(long id) {
        return medecinRepository.findOne(id);
    }
 
    @Override
    public Rv getRvById(long id) {
        return rvRepository.findOne(id);
    }
 
    @Override
    public Creneau getCreneauById(long id) {
        return creneauRepository.findOne(id);
    }
 
    @Override
    public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
        return rvRepository.save(new Rv(jour, client, créneau));
    }
 
    @Override
    public void supprimerRv(Rv rv) {
        rvRepository.delete(rv.getId());
    }
 
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
    ...
    }
 
}
  • linha 24: a anotação [@Service] é uma anotação Spring que torna a classe anotada um componente gerido pelo Spring. Pode ou não atribuir um nome a um componente. Este tem o nome [business];
  • linha 25: a classe [Metier] implementa a interface [IMetier];
  • linha 28: a anotação [@Autowired] é uma anotação do Spring. O valor do campo anotado desta forma será inicializado (injetado) pelo Spring com a referência a um componente do Spring do tipo ou nome especificado. Aqui, a anotação [@Autowired] não especifica um nome. Portanto, será realizada uma injeção baseada no tipo;
  • linha 29: o campo [medecinRepository] será inicializado com a referência a um componente Spring do tipo [MedecinRepository]. Esta será a referência à classe gerada pelo Spring Data para implementar a interface [MedecinRepository] que já apresentámos;
  • linhas 30–35: este processo é repetido para as outras três interfaces discutidas;
  • linhas 39–41: implementação do método [getAllClients];
  • linha 40: utilizamos o método [findAll] da interface [ClientRepository]. Este método retorna um tipo [Iterable<Client>], que convertemos para um [List<Client>] utilizando o método estático [Lists.newArrayList]. A classe [Lists] está definida na biblioteca Google Guava. No [pom.xml], esta dependência foi importada:

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

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


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

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

  • recuperar todos os horários disponíveis para o médico especificado;
  • recuperar todas as suas consultas para o dia especificado;
  • com estas duas informações, podemos determinar se um horário está livre ou reservado;

8.4.7. A configuração do projeto Spring

  

A classe [DomainAndPersistenceConfig] configura todo o projeto:


package rdvmedecins.config;
 
import javax.persistence.EntityManagerFactory;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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;
 
@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {
 
    // JPA entity packages
    public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
 
    // the MySQL data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration JDBC
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
 
    // provider JPA is Hibernate
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }
 
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(ENTITIES_PACKAGES);
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • linha 17: a classe é uma classe de configuração Spring;
  • linha 18: os pacotes que contêm as interfaces [CrudRepository] do Spring Data. Estas serão adicionadas ao contexto Spring;
  • linha 19: adiciona todas as classes do pacote [rdvmedecins] e suas subclasses que possuem uma anotação Spring ao contexto Spring. No pacote [rdvmedecins.metier], a classe [Metier] com a sua anotação [@Service] será encontrada e adicionada ao contexto Spring;
  • linhas 26–39: configuram o pool de conexões JDBC do Tomcat (linha 5);
  • linha 36: o pool de conexões terá 5 conexões abertas por padrão. Esta linha é mostrada para fins ilustrativos. No nosso caso, 1 conexão seria suficiente. Se a camada [DAO] fosse usada por múltiplas threads, esta linha seria necessária. Este será o caso mais adiante, quando a camada [DAO] servir de base para uma aplicação web que, por natureza, suporta múltiplos utilizadores a serem atendidos simultaneamente;
  • Linhas 42–49: A implementação JPA utilizada é uma implementação Hibernate;
  • linha 45: sem registos SQL;
  • linha 46: sem regeneração de tabelas;
  • linha 47: o SGBD utilizado é o MySQL;
  • Linhas 53–61: definem a EntityManagerFactory para a camada JPA. A partir deste objeto, obtemos o objeto [EntityManager], que é utilizado para realizar operações JPA;
  • Linha 57: Especifica o(s) pacote(s) onde as entidades JPA estão localizadas;
  • linha 58: especifica a fonte de dados a ser conectada à camada JPA;
  • linhas 64–69: o gestor de transações associado ao EntityManagerFactory anterior. Por predefinição, os métodos das interfaces [CrudRepository] do Spring Data são executados dentro de uma transação. A transação é iniciada antes de entrar no método e é concluída (através de um commit ou rollback) após sair do mesmo;

8.4.8. Testes para a camada [business]

  

A classe [rdvmedecins.tests.Metier] é uma classe de teste Spring/JUnit 4:


package rdvmedecins.tests;
 
import java.text.ParseException;
import java.util.Date;
import java.util.List;
 
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
 
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
 
    @Autowired
    private IMetier métier;
 
    @Test
    public void test1(){
        // customer display
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // physician display
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // display doctor's slots
        Medecin médecin = medecins.get(0);
        List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
        display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
        // list of doctor's appointments on a given day
        Date jour = new Date();
        display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // add a RV to the list
        Rv rv = null;
        Creneau créneau = creneaux.get(2);
        Client client = clients.get(0);
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        rv = métier.ajouterRv(jour, créneau, client);
        // check
        Rv rv2 = métier.getRvById(rv.getId());
        Assert.assertEquals(rv, rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // add a RV in the same slot on the same day
        // must trigger an exception
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        Boolean erreur = false;
        try {
            rv = métier.ajouterRv(jour, créneau, client);
            System.out.println("Rv ajouté");
        } catch (Exception ex) {
            Throwable th = ex;
            while (th != null) {
                System.out.println(ex.getMessage());
                th = th.getCause();
            }
            // we note the error
            erreur = true;
        }
        // check for errors
        Assert.assertTrue(erreur);
        // RV list
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // calendar display
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // delete a RV
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // check
        rv2 = métier.getRvById(rv.getId());
        Assert.assertNull(rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
    }
 
    // utility method - displays items in a collection
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
 
}
  • linha 22: a anotação [@SpringApplicationConfiguration] permite que o ficheiro de configuração [DomainAndPersistenceConfig], discutido anteriormente, seja utilizado. A classe de teste beneficia, assim, de todos os beans definidos por este ficheiro;
  • linha 23: a anotação [@RunWith] permite a integração do Spring com o JUnit: a classe pode ser executada como um teste JUnit. [@RunWith] é uma anotação do JUnit (linha 9), enquanto a classe [SpringJUnit4ClassRunner] é uma classe do Spring (linha 12);
  • Linhas 26–27: Injeção de uma referência à camada [business] na classe de teste;
  • muitos testes são simplesmente testes visuais:
    • linhas 32–33: lista de clientes;
    • linhas 35–36: lista de médicos;
    • linhas 39-40: lista dos horários disponíveis de um médico;
    • linha 43: lista de consultas de um médico;
  • linha 50: adição de uma nova consulta. O método [addAppt] devolve a consulta com informações adicionais, a sua chave primária id;
  • linha 53: esta chave primária é utilizada para pesquisar a consulta na base de dados;
  • linha 54: verificamos se a consulta que está a ser pesquisada e a consulta encontrada são a mesma. Recorde-se que o método [equals] da entidade [Rv] foi redefinido: duas consultas são iguais se tiverem o mesmo id. Aqui, isto mostra-nos que a consulta adicionada foi de facto inserida na base de dados;
  • Linhas 61–73: Tentamos adicionar o mesmo compromisso uma segunda vez. Isto deve ser rejeitado pelo SGBD porque existe uma restrição de unicidade:

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

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

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

O teste é executado com sucesso:

 

8.4.9. O programa de consola

  

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


package rdvmedecins.boot;
 
import java.text.SimpleDateFormat;
import java.util.Date;
 
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
 
public class Boot {
    // the boot
    public static void main(String[] args) {
        // prepare the configuration
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // launch it
        ConfigurableApplicationContext context = app.run(args);
        // business
        IMetier métier = context.getBean(IMetier.class);
        try {
            // add a RV to the list
            Date jour = new Date();
            System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
            Client client = (Client) new Client().build(1L, 1L);
            Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
            Rv rv = métier.ajouterRv(jour, créneau, client);
            System.out.println(String.format("Rv ajouté = %s", rv));
            // check
            créneau = métier.getCreneauById(1L);
            long idMedecin = créneau.getIdMedecin();
            display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
        } catch (Exception ex) {
            System.out.println("Exception : " + ex.getCause());
        }
        // closing the Spring context
        context.close();
    }
 
    // utility method - displays items in a collection
    private static <T> void display(String message, Iterable<T> elements) {
        System.out.println(message);
        for (T element : elements) {
            System.out.println(element);
        }
    }
 
}

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

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

A saída da consola é a seguinte:

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

8.4.10. Gestão de registos

Os registos da consola são configurados através de dois ficheiros: [application.properties] e [logback.xml] [1]:

O ficheiro [application.properties] é utilizado pela estrutura Spring Boot. Permite definir uma vasta gama de configurações para substituir os valores predefinidos utilizados pelo Spring Boot (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Aqui está o seu conteúdo:


logging.level.org.hibernate=OFF
spring.main.show-banner=false
  • Linha 1: controla o nível de registo do Hibernate — neste caso, não há registos
  • linha 2: controla a exibição do banner do Spring Boot — aqui, sem banner

O ficheiro [logback.xml] é o ficheiro de configuração da estrutura de registo [logback] [2]:


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- log level control -->
        <root level="info"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>
  • O nível geral de registo é controlado pela linha 9 — aqui, registos de nível [info];

Isto produz o seguinte resultado:

1
2
3
4
5
6
7
14:20:35.634 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@345965f2: startup date [Wed Oct 14 14:20:35 CEST 2015]; root of context hierarchy
14:20:36.118 [main] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
Ajout d'un Rv le [14/10/2015] dans le créneau 1 pour le client 1
Rv ajouté = Rv[191, Wed Oct 14 14:20:38 CEST 2015, 1, 1]
Liste des rendez-vous
Rv[191, 2015-10-14, 1, 1]
14:20:38.211 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@345965f2: startup date [Wed Oct 14 14:20:35 CEST 2015]; root of context hierarchy

Se definirmos o nível de registo do Hibernate como [info] (sem alterar mais nada):


logging.level.org.hibernate=INFO
spring.main.show-banner=false

isto produz o seguinte resultado:

10:33:12.198 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5a4aa2f2: startup date [Wed Oct 14 10:33:12 CEST 2015]; root of context hierarchy
10:33:12.681 [main] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
10:33:12.702 [main] INFO  o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
10:33:12.773 [main] INFO  org.hibernate.Version - HHH000412: Hibernate Core {4.3.11.Final}
10:33:12.775 [main] INFO  org.hibernate.cfg.Environment - HHH000206: hibernate.properties not found
10:33:12.776 [main] INFO  org.hibernate.cfg.Environment - HHH000021: Bytecode provider name : javassist
10:33:13.011 [main] INFO  o.h.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
10:33:13.434 [main] INFO  org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
10:33:13.621 [main] INFO  o.h.h.i.a.ASTQueryTranslatorFactory - HHH000397: Using ASTQueryTranslatorFactory
Ajout d'un Rv le [14/10/2015] dans le créneau 1 pour le client 1
Rv ajouté = Rv[181, Wed Oct 14 10:33:14 CEST 2015, 1, 1]
Liste des rendez-vous
Rv[181, 2015-10-14, 1, 1]
10:33:14.782 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5a4aa2f2: startup date [Wed Oct 14 10:33:12 CEST 2015]; root of context hierarchy

Se definirmos o nível de registo como [debug] (sem alterar mais nada):


logging.level.org.hibernate=DEBUG
spring.main.show-banner=false

Isto produz o seguinte resultado:


10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'clientRepository' to allow for resolving potential circular references
10:35:13.522 [main] DEBUG o.s.b.f.annotation.InjectionMetadata - Processing injected element of bean 'clientRepository': PersistenceElement for public void org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.setEntityManager(javax.persistence.EntityManager)
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'entityManagerFactory'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'jpaMappingContext'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name 'clientRepository'
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$ThreadBoundTargetSource@723ed581
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [org.springframework.data.jpa.repository.support.SimpleJpaRepository@796065aa]
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'clientRepository'
10:35:13.522 [main] DEBUG o.s.b.f.a.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'métier' to bean named 'clientRepository'
...

8.4.11. A camada [web / JSON]

  

Vamos construir a camada [web / JSON] em várias etapas:

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

8.4.11.1. 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" 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>istia.st.spring4.mvc</groupId>
        <artifactId>rdvmedecins-webjson-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
 
        <name>rdvmedecins-webjson-server</name>
        <description>Gestion de RV Médecins</description>
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
        </parent>
        <dependencies>
                <!-- spring mvc web layer -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <!-- test layer -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <!-- layer DAO -->
                <dependency>
                        <groupId>istia.st.spring4.rdvmedecins</groupId>
                        <artifactId>rdvmedecins-metier-dao</artifactId>
                        <version>0.0.1-SNAPSHOT</version>
                </dependency>
        </dependencies>
...
</project>
  • linhas 12–15: o projeto Maven pai;
  • linhas 19–22: dependências para um projeto Spring MVC;
  • linhas 24–28: dependências para testes JUnit/Spring;
  • linhas 30–34: dependências das camadas do projeto [lógica de negócio, DAO, JPA];

8.4.11.2. A interface do serviço web

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

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


package rdvmedecins.web.models;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}
  • linha 7: código de erro da resposta 0: OK, qualquer outra coisa: KO;
  • linha 11: uma lista de mensagens de erro, se houver algum erro;
  • linha 13: o corpo da resposta;

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

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

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

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

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

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

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

Adicionar um compromisso [/addAppointment]

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

A resposta é então a seguinte:

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

A presença do novo compromisso pode ser verificada:

Anote o ID do compromisso [50]. Vamos eliminar este.

Eliminar um compromisso [/deleteApp]

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

A resposta é então a seguinte:

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

A eliminação do compromisso pode ser verificada:

Acima, a consulta da paciente [Sra. GERMAIN] já não consta.

O serviço web também permite que as entidades sejam recuperadas pelo seu ID:

Todas estas URLs são geridas pelo controlador [RdvMedecinsController], que apresentaremos em breve.

8.4.11.3. Configuração do serviço web

  

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


package rdvmedecins.web.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
 
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
 
}
  • Linha 12: A classe [AppConfig] configura toda a aplicação;
  • linha 9: a classe [AppConfig] é uma classe de configuração Spring;
  • linha 10: especificamos que os componentes Spring devem ser procurados no pacote [rdvmedecins.web] e nos seus subpacotes. É assim que os seguintes componentes serão encontrados:
    • [@RestController RdvMedecinsController] no pacote [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] no pacote [rdvmedecins.web.models];
  • Linha 11: Importamos a classe [DomainAndPersistenceConfig], que configura o projeto [rdvmedecins-metier-dao] para fornecer acesso aos beans desse projeto;
  • linha 11: a classe [SecurityConfig] configura a segurança da aplicação web. Vamos ignorá-la por enquanto;
  • linha 11: a classe [WebConfig] configura a camada [web / JSON];

A classe [WebConfig] é a seguinte:


package rdvmedecins.web.config;
 
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
@Configuration
@EnableWebMvc
public class WebConfig {
 
    // dispatcherservlet configuration for CORS headers
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
 
    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }
 
    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8080);
    }
 
    // mappers jSON
    @Bean
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
    }
 
    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(
                new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
        return jsonMapperLongRv;
    }
 
    @Bean
    public ObjectMapper jsonMapperShortRv() {
        ObjectMapper jsonMapperShortRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
        jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
        return jsonMapperShortRv;
    }
 
}
  • linhas 20–25: definem o bean [dispatcherServlet]. A classe [DispatcherServlet] é o servlet da estrutura Spring MVC. Funciona como um [FrontController]: intercepta os pedidos enviados para o site Spring MVC e encaminha-os para um dos controladores do site;
  • linha 22: instanciação da classe;
  • linha 23: esta linha pode ser ignorada por enquanto;
  • linhas 27–30: o servlet [dispatcherServlet] trata de todas as URLs;
  • linhas 27–30: ativam o servidor Tomcat incorporado nas dependências do projeto. Ele será executado na porta 8080;
  • linhas 38–67: quatro mapeadores JSON configurados com diferentes filtros JSON;
  • linhas 38–41: um mapeador JSON sem filtros;
  • linhas 43–49: o mapeador JSON [jsonMapperShortCreneau] serializa/deserializa um objeto [Creneau] ignorando o campo [Creneau.medecin];
  • linhas 51–59: o mapeador JSON [jsonMapperLongRv] serializa/deserializa um objeto [Rv] ignorando o campo [Rv.creneau.medecin];
  • linhas 61-67: o mapeador JSON [jsonMapperShortRv] serializa/deserializa um objeto [Rv] ignorando os campos [Rv.creneau] e [Rv.client];

8.4.11.4. A classe [ApplicationModel]

  

A classe [ApplicationModel] terá duas finalidades:

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

package rdvmedecins.web.models;
 
import java.util.Date;
import java.util.List;
 
import javax.annotation.PostConstruct;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;
 
@Component
public class ApplicationModel implements IMetier {
 
    // the [business] layer
    @Autowired
    private IMetier métier;
 
    // data from the [business] layer
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    // configuration data
    private boolean CORSneeded = false;
    private boolean secured = false;
 
    @PostConstruct
    public void init() {
        // we get the doctors and the customers
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }
 
    // getter
    public List<String> getMessages() {
        return messages;
    }
 
    // ------------------------- [business] layer interface
    @Override
    public List<Client> getAllClients() {
        return clients;
    }
 
    @Override
    public List<Medecin> getAllMedecins() {
        return médecins;
    }
 
    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return métier.getAllCreneaux(idMedecin);
    }
 
    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return métier.getRvMedecinJour(idMedecin, jour);
    }
 
    @Override
    public Client getClientById(long id) {
        return métier.getClientById(id);
    }
 
    @Override
    public Medecin getMedecinById(long id) {
        return métier.getMedecinById(id);
    }
 
    @Override
    public Rv getRvById(long id) {
        return métier.getRvById(id);
    }
 
    @Override
    public Creneau getCreneauById(long id) {
        return métier.getCreneauById(id);
    }
 
    @Override
    public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
        return métier.ajouterRv(jour, creneau, client);
    }
 
    @Override
    public void supprimerRv(long idRv) {
        métier.supprimerRv(idRv);
    }
 
    @Override
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        return métier.getAgendaMedecinJour(idMedecin, jour);
    }
 
     // getters and setters
public boolean isCORSneeded() {
        return CORSneeded;
    }
 
    public boolean isSecured() {
        return secured;
    }
 
}
  • linha 19: a anotação [@Component] torna a classe [ApplicationModel] um componente Spring. Tal como todos os componentes Spring vistos até agora (com exceção de @Controller), apenas um único objeto deste tipo será instanciado (singleton);
  • linha 20: a classe [ApplicationModel] implementa a interface [IMetier];
  • linhas 23–24: uma referência à camada [business] é injetada pelo Spring;
  • linha 34: a anotação [@PostConstruct] garante que o método [init] será executado imediatamente após a instância da classe [ApplicationModel];
  • linhas 38–39: Recupera as listas de médicos e clientes da camada [business];
  • Linha 41: Se ocorrer uma exceção, as mensagens da pilha de exceções são armazenadas no campo da linha 17;

A arquitetura da camada web evolui da seguinte forma:

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

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

8.4.11.5. A Classe Static

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

  

O código é o seguinte:


package rdvmedecins.web.helpers;
 
import java.util.ArrayList;
import java.util.List;
 
public class Static {
 
    public Static() {
    }
 
    // list of exception error messages
    public static List<String> getErreursForException(Exception exception) {
        // retrieve the list of exception error messages
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }
}
  • linha 12: o método [Static.getErrorsForException] que foi utilizado (linha 8 abaixo) no método [init] da classe [ApplicationModel]:

    @PostConstruct
    public void init() {
        // we get the doctors and the customers
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
}

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

8.4.11.6. O esqueleto do controlador [RdvMedecinsController]

  

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

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

O controlador [RdvMedecinsController] é o seguinte:


package rdvmedecins.web.controllers;
 
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;
 
@Controller
public class RdvMedecinsController {
 
    @Autowired
    private ApplicationModel application;
 
    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;
 
    // message list
    private List<String> messages;
 
    // mappers jSON
    @Autowired
    private ObjectMapper jsonMapper;
 
    @Autowired
    private ObjectMapper jsonMapperShortCreneau;
 
    @Autowired
    private ObjectMapper jsonMapperLongRv;
 
    @Autowired
    private ObjectMapper jsonMapperShortRv;
 
    @PostConstruct
    public void init() {
        // application error messages
        messages = application.getMessages();
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {...}
 
    // customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllClients() throws JsonProcessingException {...}
 
    // list of physician slots
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
 
    // list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
                    throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
                    throws JsonProcessingException {...}
 
    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {...}
}
  • linha 35: a anotação [@Controller] torna a classe [RdvMedecinsController] um controlador Spring, o C em MVC;
  • linhas 38–39: Um objeto do tipo [ApplicationModel] será injetado aqui pelo Spring. Já o apresentámos;
  • linhas 41-42: um objeto do tipo [RdvMedecinsCorsController] será injetado aqui pelo Spring. Apresentaremos este objeto mais tarde;
  • linhas 48–58: os mapeadores JSON definidos na classe de configuração [WebConfig];
  • linha 60: a anotação [@PostConstruct] marca um método a ser executado imediatamente após a instância da classe. Quando este método é executado, os objetos injetados pelo Spring estão disponíveis;
  • linha 63: recuperamos quaisquer mensagens de erro do objeto [ApplicationModel]. Este objeto foi instanciado quando a aplicação foi iniciada e tentou armazenar em cache os médicos e os clientes. Se falhou, então [messages!=null]. Isto permitirá que os métodos do controlador determinem se a aplicação foi inicializada corretamente;
  • linhas 67–118: as URLs expostas pelo serviço [web/jSON]. Todos os métodos devolvem uma cadeia JSON do seguinte tipo [Response<T>]:
 

package rdvmedecins.web.models;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}
  • linha 9: um código de erro: 0 significa que não há erro;
  • linha 11: se [status!=0], então [messages] é uma lista de mensagens de erro;
  • linha 13: um objeto T encapsulado na resposta. T é nulo em caso de erro;

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

  • linha 67: a URL exposta é [/getAllDoctors]. O cliente deve utilizar um método [GET] para efetuar o seu pedido (method = RequestMethod.GET). Se esta URL fosse solicitada via POST, seria rejeitada e o Spring MVC enviaria um código de erro HTTP ao cliente web. O próprio método devolve a resposta ao cliente (linha 68). Esta será uma string (linha 67). O cabeçalho HTTP [Content-type: application/json; charset=UTF-8] será enviado ao cliente para indicar que este receberá uma string JSON (linha 67);
  • linha 77: a URL está configurada com {idMedecin}. Este parâmetro é recuperado utilizando a anotação [@PathVariable] na linha 79;
  • Linha 79: O parâmetro [long idMedecin] obtém o seu valor do parâmetro {idMedecin} na URL [@PathVariable("idMedecin")]. O parâmetro na URL e o do método podem ter nomes diferent . Note que [@PathVariable("idMedecin")] é do tipo String (a URL completa é uma String), enquanto o parâmetro [long idMedecin] é do tipo [long]. A conversão de tipos é realizada automaticamente. É devolvido um código de erro HTTP se esta conversão de tipos falhar;
  • linha 105: a anotação [@RequestBody] refere-se ao corpo da solicitação. Numa solicitação GET, quase nunca há um corpo (mas é possível incluir um). Numa solicitação POST, geralmente há um (mas é possível omiti-lo). Para a URL [ajouterRv], o cliente web envia a seguinte string JSON na sua solicitação POST:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

A sintaxe [@RequestBody PostAjouterRv post] (linha 105), combinada com o facto de o método esperar JSON [consumes = "application/json; charset=UTF-8"] (linha 103), significa que a cadeia JSON enviada pelo cliente web será deserializada num objeto do tipo [PostAjouterRv]. Isto é feito da seguinte forma:


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

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

  • As linhas 107–109 contêm 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 {
 
    // pOST DATA
    private long idRv;
 
    // getters and setters
    ...
}

8.4.11.7. A URL [/getAllDoctors]

A URL [/getAllMedecins] é tratada pelo seguinte método no controlador [RdvMedecinsController]:


// list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {
        // the answer
        Response<List<Medecin>> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // list of doctors
            try {
                response = new Response<>(0, null, application.getAllMedecins());
            } catch (RuntimeException e) {
                response = new Response<>(1, Static.getErreursForException(e), null);
            }
        }
        // answer
        return jsonMapper.writeValueAsString(response);
    }
  • linhas 9-10: verificamos se a aplicação foi inicializada corretamente (messages==null). Caso contrário, devolvemos uma resposta com status=-1 e corpo=messages;
  • linha 13: caso contrário, solicitamos a lista de médicos à classe [ApplicationModel];
  • linha 19: enviamos a cadeia JSON da resposta utilizando o mapeador JSON [jsonMapper], uma vez que a classe [Medecin] não possui um filtro JSON. A resposta pode estar isenta de erros (linha 14) ou conter um erro (linha 16). O método [application.getAllMedecins()] não lança uma exceção porque simplesmente devolve uma lista 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 foi inicializada incorretamente. Vamos parar o SGBD MySQL5, iniciar o serviço web e, em seguida, solicitar a URL [/getAllMedecins]:

Image

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

8.4.11.8. A URL [/getAllClients]

A URL [/getAllClients] é tratada pelo seguinte método no [RdvMedecinsController]:


// customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllClients() throws JsonProcessingException {
        // the answer
        Response<List<Client>> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        }
        // customer list
        try {
            response = new Response<>(0, null, application.getAllClients());
        } catch (RuntimeException e) {
            response = new Response<>(1, Static.getErreursForException(e), null);
        }
        // answer
        return jsonMapper.writeValueAsString(response);
    }

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

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

A URL [/getAllSlots/{doctorId}] é tratada pelo seguinte método do controlador [RdvMedecinsController]:


// list of physician slots
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
        // the answer
        Response<List<Creneau>> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        }
        // we get the doctor back
        Response<Medecin> responseMedecin = getMedecin(idMedecin);
        if (responseMedecin.getStatus() != 0) {
            response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
        } else {
            Medecin médecin = responseMedecin.getBody();
            // doctor's slots
            try {
                response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperShortCreneau.writeValueAsString(response);
    }
  • linha 12: o médico identificado pelo parâmetro [id] é solicitado a partir de um método local:

private Response<Medecin> getMedecin(long id) {
        // we get the doctor back
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (RuntimeException e1) {
            return new Response<Medecin>(1, Static.getErreursForException(e1), null);
        }
        // existing doctor?
        if (médecin == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
            return new Response<Medecin>(2, messages, null);
        }
        // ok
        return new Response<Medecin>(0, null, médecin);
    }

Este método devolve um valor de estado no intervalo [0,1,2]. Voltemos ao código do método [getAllCreneaux]:

  • linhas 13-14: se status!=0, construímos uma resposta com um erro;
  • linha 16: recuperamos o médico;
  • linha 19: recuperamos os horários deste médico;
  • linha 25: enviamos um objeto [List<Creneau>] como resposta. Vamos relembrar a definição da classe [Creneau]:

@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • linha 13: o médico é recuperado no modo [FetchType.LAZY];

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


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

A notação [c.medecin.id] força uma junção entre as tabelas [CRENEAUX] e [MEDECINS]. Como resultado, a consulta devolve todos os horários do médico, com o médico incluído em cada um deles. Quando serializamos esses horários para JSON, a cadeia de caracteres JSON do médico aparece em cada um deles. Isto é desnecessário. Para controlar a serialização, precisamos de duas coisas:

  1. acesso ao objeto que está a ser serializado;
  2. configurar o objeto a ser serializado;

O ponto 1 é tratado através da injeção do conversor JSON apropriado para o objeto no controlador:


@Autowired
private ObjectMapper jsonMapperShortCreneau;

O ponto 2 é alcançado adicionando uma anotação à classe [Creneau] definida no projeto [rdvmedecins-metier-dao]:

  

@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
  • Linha 3: uma anotação da biblioteca Jackson JSON. Ela cria um filtro chamado [creneauFilter]. Usando este filtro, poderemos definir programaticamente quais campos devem ou não ser serializados;

A serialização do objeto [Creneau] ocorre na seguinte linha do método [getAllCreneaux]:


        // réponse
        return jsonMapperShortCreneau.writeValueAsString(response);

O mapeador JSON [jsonMapperShortCreneau] foi definido na classe [WebConfig] da seguinte forma:


    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
}
  • Linha 5: O filtro denominado [creneauFilter] está associado ao filtro [creneauFilter] da linha 4. Este filtro serializa o objeto [Creneau] sem o seu campo [medecin];

O resultado devolvido pelo método [getAllCreneaux] é uma cadeia JSON do tipo [Response<List<Creneau>].

Os resultados obtidos são os seguintes:

ou estes, caso o slot não exista:

A partir deste exemplo, podemos deduzir a seguinte regra:

  • Os métodos do servidor Web / JSON devolvem um objeto do tipo [Response<T>] que é serializado para JSON;
  • se o tipo T tiver um ou mais filtros JSON, será utilizado um mapeador com esses mesmos filtros para o serializar;

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

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


// list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
                    throws JsonProcessingException {
        // the answer
        Response<List<Rv>> response=null;
        boolean erreur = false;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // check the date
        Date jourAgenda = null;
        if (!erreur) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<List<Rv>>(3, messages, null);
                erreur = true;
            }
        }
        Response<Medecin> responseMedecin = null;
        if (!erreur) {
            // we get the doctor back
            responseMedecin = getMedecin(idMedecin);
            if (responseMedecin.getStatus() != 0) {
                response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            Medecin médecin = responseMedecin.getBody();
            // list of appointments
            try {
                response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
            } catch (RuntimeException e1) {
                response = new Response<>(4, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • Temos de devolver a cadeia JSON do tipo [Response<List<Rv>>]. A classe [Rv] tem um campo [Rv.creneau]. Se este campo for serializado, encontraremos o filtro JSON [creneauFilter];
  • linha 47: o objeto do tipo [Response<List<Rv>>] da linha 7 é serializado para JSON;

Vamos examinar o caso em que a lista de consultas foi obtida na linha 42. A classe [Rv] no projeto [rdvmedecins-metier-dao] é definida da seguinte forma:


@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // characteristics of an Rv
    @Temporal(TemporalType.DATE)
    private Date jour;
 
    // an appointment is linked to a customer
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;
 
    // an appointment is linked to a time slot
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;
 
    // foreign keys
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;
 
...
 
}
  • linha 11: o cliente é recuperado utilizando o modo [FetchType.LAZY];
  • linha 18: o slot é recuperado utilizando o modo [FetchType.LAZY];

Vamos relembrar a consulta JPQL que recupera os compromissos:


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

As junções são realizadas explicitamente para recuperar os campos [client] e [slot]. Além disso, devido à junção [cr.doctor.id=?1], também obteremos o médico. O médico aparecerá, portanto, na cadeia JSON de cada consulta. No entanto, esta informação duplicada é desnecessária. Vimos como resolver este problema utilizando um filtro JSON no objeto [Creneau]. Devido aos modos [FetchType.LAZY] dos campos [client] e [slot] na classe [Rv], em breve descobriremos a necessidade de aplicar um filtro JSON à classe [RV] no projeto [rdvmedecins-metier-dao]:


@Entity
@Table(name = "rv")
@JsonFilter("rvFilter")
public class Rv extends AbstractEntity {
...

Iremos controlar a serialização do objeto [Rv] utilizando o filtro [rvFilter]. Aparentemente, neste caso, não precisamos de filtrar porque precisamos de todos os campos do objeto [Rv]. No entanto, como especificámos que a classe tem um filtro JSON, temos de o definir para qualquer serialização de um objeto do tipo [Rv]; caso contrário, obteremos uma exceção. Para tal, utilizamos o seguinte mapeador JSON definido na classe [rdvMedecinsController]:


    @Autowired
    private ObjectMapper jsonMapperLongRv;

Este mapeador é definido da seguinte forma na classe de configuração [WebConfig]:


    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
        return jsonMapperLongRv;
}
  • Linha 4: Especificamos que todos os campos do objeto [Rv] devem ser serializados;
  • linha 5: especificamos que, no objeto [Creneau], o campo [medecin] não deve ser serializado;
  • linha 6: adicionamos os dois filtros [rvFilter] e [creneauFilter] aos filtros JSON do objeto [jsonMapperLongRv];

Os resultados obtidos são os seguintes:

ou estes, num dia sem marcação:

ou estes com um dia incorreto:

ou estes com um médico incorreto:

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

A URL [/getAgendaMedecinJour/{idMedecin}/{jour}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
                    throws JsonProcessingException {
        // the answer
        Response<AgendaMedecinJour> response = null;
        boolean erreur = false;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // check the date
        Date jourAgenda = null;
        if (!erreur) {
            // check the date
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                erreur = true;
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<>(3, messages, null);
            }
        }
        // we get the doctor back
        Medecin médecin = null;
        if (!erreur) {
            // we get the doctor back
            Response<Medecin> responseMedecin = getMedecin(idMedecin);
            if (responseMedecin.getStatus() != 0) {
                response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
            } else {
                médecin = responseMedecin.getBody();
            }
        }
        // get your diary back
        if (!erreur) {
            try {
                response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
            } catch (RuntimeException e1) {
                erreur = true;
                response = new Response<>(4, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • linhas 6, 49: devolvemos a cadeia JSON do tipo [AgendaMedecinJour] encapsulada num objeto [Response];

O tipo [AgendaMedecinJour] é o seguinte:


public class AgendaMedecinJour implements Serializable {
    // fields
    private Medecin medecin;
    private Date jour;
   private CreneauMedecinJour[] creneauxMedecinJour;

O tipo [CreneauMedecinJour] é o seguinte:


public class CreneauMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Creneau creneau;
   private Rv rv;

Os campos [creneau] e [rv] têm filtros JSON que precisam de ser configurados. É isso que a linha 49 do método [getAgendaMedecinJour] faz, utilizando o mapeador JSON [jsonMapperLongRv] que vimos anteriormente:


    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(
                new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
        return jsonMapperLongRv;
}

Os resultados obtidos são os seguintes:

Acima, vemos que, em 28/01/2015, o Dr. PELISSIER tem uma consulta com a Sra. Brigitte BISTROU às 8h20;

ou estes, caso a data esteja incorreta:

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

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

A URL [/getMedecinById/{id}] é tratada pelo seguinte método no [RdvMedecinsController]:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Medecin> response;
        // application status
        if (messages != null) {
            response = new Response<Medecin>(-1, messages, null);
        } else {
            response = getMedecin(id);
        }
        // answer
        return jsonMapper.writeValueAsString(response);
}
  • Linhas 5, 13: O método devolve uma cadeia JSON do tipo [Doctor]. Este tipo não tem qualquer anotação de filtro JSON. Por conseguinte, na linha 14, o mapeador JSON é utilizado sem filtros;

Linha 10: o método [getMedecin] é o seguinte:


    private Response<Medecin> getMedecin(long id) {
        // we get the doctor back
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (RuntimeException e1) {
            return new Response<Medecin>(1, Static.getErreursForException(e1), null);
        }
        // existing doctor?
        if (médecin == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
            return new Response<Medecin>(2, messages, null);
        }
        // ok
        return new Response<Medecin>(0, null, médecin);
}

Os resultados são os seguintes:

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

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

A URL [/getClientById/{id}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Client> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            response = getClient(id);
        }
        // answer
        return jsonMapper.writeValueAsString(response);
}
  • Linhas 5, 13: O método devolve uma cadeia JSON do tipo [Client]. Este tipo não possui anotações de filtro JSON. Por conseguinte, na linha 13, o mapeador JSON é utilizado sem filtros;

Linha 11: o método [getClient] é o seguinte:


    private Response<Client> getClient(long id) {
        // we get the customer back
        Client client = null;
        try {
            client = application.getClientById(id);
        } catch (RuntimeException e1) {
            return new Response<Client>(1, Static.getErreursForException(e1), null);
        }
        // existing customer?
        if (client == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le client d'id [%s] n'existe pas", id));
            return new Response<Client>(2, messages, null);
        }
        // ok
        return new Response<Client>(0, null, client);
}

Os resultados são os seguintes:

ou estes, se o ID do cliente estiver incorreto:

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

A URL [/getCreneauById/{id}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Creneau> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // we give back the slot
            response = getCreneau(id);
        }
        // answer
        return jsonMapperShortCreneau.writeValueAsString(response);
}
  • Linhas 5, 14: O método devolve uma cadeia JSON do tipo [Response<Creneau>];

Linha 8: o método [getCreneau] é o seguinte:


    private Response<Creneau> getCreneau(long id) {
        // we get the slot back
        Creneau créneau = null;
        try {
            créneau = application.getCreneauById(id);
        } catch (RuntimeException e1) {
            return new Response<Creneau>(1, Static.getErreursForException(e1), null);
        }
        // existing niche?
        if (créneau == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le créneau d'id [%s] n'existe pas", id));
            return new Response<Creneau>(2, messages, null);
        }
        // ok
        return new Response<Creneau>(0, null, créneau);
    }

Vamos rever o código da entidade [Creneau]:


@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
  • linhas 14-16: como o campo [doctor] está no modo [fetch = FetchType.LAZY], não é recuperado ao buscar um slot através do seu [id]. É, portanto, necessário excluí-lo da serialização. Sem esta exclusão, ocorre uma exceção. Isto deve-se ao facto de o objeto de serialização [mapper] chamar o método [getMedecin] para recuperar o campo [medecin]. No entanto, com uma implementação JPA/Hibernate, o modo [fetch = FetchType.LAZY] do campo [medecin] devolve um objeto [Creneau] cujo método [getMedecin] está programado para ir buscar o médico a partir do contexto JPA. Isto é designado por objeto [proxy]. Agora, recordemos a arquitetura da aplicação web:

O controlador está localizado no bloco [Controllers / Actions]. Uma vez dentro deste bloco, o conceito de contexto JPA deixa de se aplicar. O contexto JPA é criado durante as operações na camada [DAO] e não persiste para além disso. Assim, quando o controlador tenta aceder ao contexto JPA, ocorre uma exceção indicando que este está fechado. Para evitar esta exceção, deve impedir a serialização do campo [medecin] da classe [Rv]. É isto que o mapeador JSON [jsonMapperShortCreneau] faz:


    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
}

Os resultados obtidos são os seguintes:

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

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

A URL [/getRvById/{id}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Rv> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // we recover rv
            response = getRv(id);
        }
        // answer
        return jsonMapperShortRv.writeValueAsString(response);
}
  • Linhas 5, 14: O método devolve uma cadeia JSON do tipo [Response<Rv>];

Linha 11: o método [getRv] é o seguinte:


    private Response<Rv> getRv(long id) {
        // we recover the Rv
        Rv rv = null;
        try {
            rv = application.getRvById(id);
        } catch (RuntimeException e1) {
            return new Response<Rv>(1, Static.getErreursForException(e1), null);
        }
        // Existing Rv?
        if (rv == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le rendez-vous d'id [%s] n'existe pas", id));
            return new Response<Rv>(2, messages, null);
        }
        // ok
        return new Response<Rv>(0, null, rv);
}

A classe [Rv] tem dois campos anotados com [fetch = FetchType.LAZY]: os campos [creneau] e [client]. Estes campos não são, portanto, recuperados ao buscar um [Rv] através da sua chave primária. Pelas mesmas razões que anteriormente, devem, portanto, ser excluídos da serialização. É isso que o seguinte mapeador [jsonMapperShortRv], definido na classe [WebConfig], faz:


    @Bean
    public ObjectMapper jsonMapperShortRv() {
        ObjectMapper jsonMapperShortRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
        jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
        return jsonMapperShortRv;
}

Os resultados obtidos são os seguintes:

ou estes, se o número da marcação estiver incorreto:

8.4.11.16. A URL [/ajouterRv]

A URL [/addAppt] é tratada pelo seguinte método no controlador [RdvMedecinsController]:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
        // the answer
        Response<Rv> response = null;
        boolean erreur = false;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // retrieve posted values
        String jour;
        long idCreneau = -1;
        long idClient = -1;
        Date jourAgenda = null;
        if (!erreur) {
            // retrieve posted values
            jour = post.getJour();
            idCreneau = post.getIdCreneau();
            idClient = post.getIdClient();
            // check the date
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<>(6, messages, null);
                erreur = true;
            }
        }
        // we get the slot back
        Response<Creneau> responseCréneau = null;
        if (!erreur) {
            // we get the slot back
            responseCréneau = getCreneau(idCreneau);
            if (responseCréneau.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
            }
        }
        // we get the customer back
        Response<Client> responseClient = null;
        Creneau créneau = null;
        if (!erreur) {
            créneau = (Creneau) responseCréneau.getBody();
            // we get the customer back
            responseClient = getClient(idClient);
            if (responseClient.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
            }
        }
        if (!erreur) {
            Client client = responseClient.getBody();
            // we add the Rv
            try {
                response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
            } catch (RuntimeException e1) {
                erreur = true;
                response = new Response<>(5, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • linhas 5, 67: o método deve devolver uma cadeia JSON do tipo [Response<Rv>];
  • linha 3: a anotação [@RequestBody PostAjouterRv post] recupera o corpo da solicitação POST e coloca-o no parâmetro [PostAjouterRv post]. Este corpo é JSON [consumes = "application/json; charset=UTF-8"], que será automaticamente deserializado para o seguinte tipo [PostAjouterRv]:

public class PostAjouterRv {
 
    // pOST DATA
    private String jour;
    private long idClient;
    private long idCreneau;
...
  • depois há código que já foi encontrado de uma forma ou de outra;
  • linha 67: configuração dos filtros JSON [creneauFilter] e [rvFilter]. O método retorna uma string JSON do tipo [Response<Rv>], onde Rv foi obtido na linha 61. O objeto [Rv] encapsula um objeto [Creneau], bem como um objeto [Client]. O objeto [Creneau] tem uma dependência [FetchType.LAZY] de um objeto [Medecin] e foi recuperado nas linhas 36–44. Foi obtido do contexto JPA através da sua chave primária e foi recuperado sem a sua dependência [FetchType.LAZY]. Em última análise,
    • o objeto [Rv] possui todas as suas dependências. Estas podem ser serializadas;
    • o objeto [Creneau] não possui a sua dependência [medecin]. Portanto, esta dependência não deve ser serializada;

O mapeador JSON [jsonMapperLongRv] definido na classe [WebConfig] cumpre estas restrições:


    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
        return jsonMapperLongRv;
}

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

  • em [1], a URL POST;
  • em [2], o pedido POST;
  • em [3], o valor enviado;
  • em [4a], este valor enviado é JSON;
  • em [4b], o cliente indica que está a enviar JSON;
  • em [5], o servidor indica que está a devolver JSON;
  • em [6], a resposta JSON do servidor representando o compromisso adicionado. Mostra o ID [id] do compromisso adicionado;

Obtemos o seguinte com um número de slot inexistente:

8.4.11.17. A URL [/deleteAppointment]

A URL [/deleteAppointment] é tratada pelo seguinte método no controlador [RdvMedecinsController]:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
        // the answer
        Response<Void> response = null;
        boolean erreur = false;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // retrieve posted values
        long idRv = post.getIdRv();
        // recovering the rv
        if (!erreur) {
            Response<Rv> responseRv = getRv(idRv);
            if (responseRv.getStatus() != 0) {
                response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            // rv deletion
            try {
                application.supprimerRv(idRv);
                response = new Response<Void>(0, null, null);
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapper.writeValueAsString(response);
    }
  • linha 5: o tipo [Void] é a classe correspondente ao tipo primitivo [void];
  • linhas 5, 34: o método devolve uma cadeia JSON do tipo [Response<Void>] que não possui filtros JSON. Por isso, na linha 34, utilizamos o mapeador JSON sem filtros;
  • linha 3: o método recebe o corpo POST como parâmetro, ou seja, o valor enviado. Este é recebido no formato JSON [content-type="application/json; charset=UTF-8"] e automaticamente deserializado para o seguinte tipo [PostSupprimerRv]:

public class PostSupprimerRv {
 
    // pOST DATA
    private long idRv;
 
  • linha 28: quando a eliminação é bem-sucedida, é enviada uma resposta com [status=0];

Os resultados obtidos são os seguintes:

  • em [5], o campo [status=0] indica que a eliminação foi bem-sucedida;

Com um ID de compromisso que não existe, obtemos o seguinte:

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

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

A classe [Boot] [1] é a seguinte:


package rdvmedecins.web.boot;
 
import org.springframework.boot.SpringApplication;
 
import rdvmedecins.web.config.AppConfig;
 
public class Boot {
 
    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}

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

Os registos são controlados pelos seguintes ficheiros [2]:

[logback.xml]


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- log level control -->
        <root level="info"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>
  • linha 9: o nível geral de registo está definido como [info];

[application.properties]


logging.level.org.springframework.web=INFO
logging.level.org.hibernate=OFF
spring.main.show-banner=false

As linhas 1-2 definem um nível de registo específico para determinadas partes da aplicação:

  • linha 1: queremos registos da camada [web];
  • linha 2: não queremos registos da camada [JPA];
  • linha 3: sem banner do Spring Boot;

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


11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,342 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:06:04,357 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:06:04,404 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:06:04,420 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point
 
11:06:04.732 [main] INFO  rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 420 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:06:04.775 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:05.538 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:06:05.688 [main] INFO  o.a.catalina.core.StandardService - Starting service Tomcat
11:06:05.689 [main] INFO  o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:06:05.833 [localhost-startStop-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:06:05.833 [localhost-startStop-1] INFO  o.s.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1061 ms
11:06:06.231 [localhost-startStop-1] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:06:09.234 [localhost-startStop-1] INFO  o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@12d14fa, org.springframework.security.web.context.SecurityContextPersistenceFilter@29823fb6, org.springframework.security.web.header.HeaderWriterFilter@662d93b2, org.springframework.security.web.authentication.logout.LogoutFilter@2d81ee0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52aa47ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@60bd7a74, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a374232, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ddb4452, org.springframework.security.web.session.SessionManagementFilter@2cd9855f, org.springframework.security.web.access.ExceptionTranslationFilter@2263f0a2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@192ce7f6]
11:06:09.255 [localhost-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:06:09.255 [localhost-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Medecin> rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Client> rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Client>> rdvmedecins.web.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Medecin>> rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
11:06:09.677 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:09.770 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:06:09.786 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:06:09.802 [main] INFO  o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:06:09.817 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:06:09.817 [main] INFO  rdvmedecins.web.boot.Boot - Started Boot in 5.319 seconds (JVM running for 6.053)
  • linha 18: o servidor Tomcat está ativo;
  • linha 21: o contexto Spring está a ser inicializado;
  • linhas 27–38: as URLs expostas pelo serviço web estão a ser descobertas;
  • linha 44: o servidor Tomcat está pronto e à espera de pedidos na porta 8080;

Se modificarmos o ficheiro [application.properties] da seguinte forma:


logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false

obtemos os seguintes registos:

11:12:12,107 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:12:12,108 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:12:12,108 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:12:12,108 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:12:12,108 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:12:12,108 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:12:12,172 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:12:12,174 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:12:12,186 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:12:12,205 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:12:12,255 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:12:12,255 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:12:12,256 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:12:12,257 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point

11:12:12.567 [main] INFO  rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 5856 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:12:12.602 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:12:12 CEST 2015]; root of context hierarchy
11:12:13.363 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:12:13.503 [main] INFO  o.a.catalina.core.StandardService - Starting service Tomcat
11:12:13.503 [main] INFO  o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:12:13.644 [localhost-startStop-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:12:14.044 [localhost-startStop-1] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:12:17.229 [localhost-startStop-1] INFO  o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@141859ba, org.springframework.security.web.context.SecurityContextPersistenceFilter@19925f3b, org.springframework.security.web.header.HeaderWriterFilter@3083c83b, org.springframework.security.web.authentication.logout.LogoutFilter@7c22ac3b, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@126fe543, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@8eecab2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@91b42ad, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e33581f, org.springframework.security.web.session.SessionManagementFilter@10abfbc1, org.springframework.security.web.access.ExceptionTranslationFilter@3e933729, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3c8f6f86]
11:12:17.259 [localhost-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:12:17.259 [localhost-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:12:17.837 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:12:17.853 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:12:17.869 [main] INFO  o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:12:17.900 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:12:17.902 [main] INFO  rdvmedecins.web.boot.Boot - Started Boot in 5.545 seconds (JVM running for 6.305)

Além disso, se modificarmos o ficheiro [logback.xml] da seguinte forma:


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- log level control -->
        <root level="off"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>

São obtidos os seguintes registos:

11:14:53,862 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:14:53,862 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:14:53,862 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:14:53,862 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:14:53,862 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:14:53,862 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:14:53,924 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:14:53,924 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:14:53,940 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:14:53,956 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:14:54,002 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to OFF
11:14:54,002 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:14:54,002 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:14:54,002 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point

Podemos ver, portanto, que temos algum controlo sobre os registos que aparecem na consola. O nível [info] é frequentemente o nível de registo adequado.

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

8.4.12. 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], encontrará as páginas HTML do projeto;
  • [Application]: é a classe executável do projeto;
  • [MvcConfig]: é a classe de configuração do Spring MVC;
  • [WebSecurityConfig]: é a classe de configuração do Spring Security;

8.4.12.1. Configuração do Maven

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


<?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-securing-web</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- tag::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- end::security[] -->
    </dependencies>
 
    <properties>
        <start-class>hello.Application</start-class>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>
  • linhas 10–14: o projeto é um projeto Spring Boot;
  • linhas 17–20: dependência da estrutura [Thymeleaf];
  • linhas 22–25: dependência da estrutura Spring Security;

8.4.12.2. visualizações Thymeleaf

  

A vista [home.html] é a seguinte:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>
 
    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • linha 12: o atributo [th:href="@{/hello}"] irá gerar o atributo [href] da tag <a>. O valor [@{/hello}] irá gerar o caminho [<context>/hello], em que [context] é o contexto da aplicação web;

O código HTML gerado é o seguinte:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>
 
        <p>
            Click
            <a href="/hello">here</a>
            to see a greeting.
        </p>
    </body>
</html>

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

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • linha 9: O atributo [th:inline="text"] irá gerar o texto da tag <h1>. Este texto contém uma expressão $ que deve ser avaliada. O elemento [[${#httpServletRequest.remoteUser}]] é o valor do atributo [RemoteUser] do pedido HTTP atual. Este é o nome do utilizador que está a iniciar sessão;
  • linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da tag [form]. O valor [@{/logout}] irá gerar o caminho [<context>/logout], onde [context] é o contexto da aplicação web;

O código HTML gerado é o seguinte:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello user!</h1>
        <form method="post" action="/logout">
            <input type="submit" value="Sign Out" />
            <input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
        </form>
    </body>
</html>
  • linha 8: a tradução de Olá [[${#httpServletRequest.remoteUser}]]!;
  • linha 9: a tradução de @{/logout};
  • linha 11: um campo oculto denominado (atributo name) _csrf;

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

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • linha 9: o atributo [th:if="${param.error}"] garante que a tag <div> só será gerada se o URL que exibe a página de login contiver o parâmetro [error] (http://context/login?error);
  • linha 10: o atributo [th:if="${param.logout}"] garante que a tag <div> só será gerada se a URL que exibe a página de login contiver o parâmetro [logout] (http://context/login?logout);
  • linhas 11–23: um formulário HTML;
  • linha 11: o formulário será enviado para o URL [<context>/login], em que <context> é o contexto da aplicação web;
  • linha 13: um campo de entrada denominado [username];
  • linha 17: um campo de entrada denominado [password];

O código HTML gerado é o seguinte:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
 
        <div>
            You have been logged out.
        </div>
        <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="ef809b0a-88b4-4db9-bc53-342216b77632" />
        </form>
    </body>
</html>

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

8.4.12.3. Configuração do Spring MVC

  

A classe [MvcConfig] configura a estrutura Spring MVC:


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
 
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }
 
}
  • linha 7: a anotação [@Configuration] torna a classe [MvcConfig] uma classe de configuração;
  • linha 8: a classe [MvcConfig] estende a classe [WebMvcConfigurerAdapter] para substituir determinados métodos;
  • linha 10: redefinição de um método da classe pai;
  • linhas 11–16: o método [addViewControllers] permite que URLs sejam associadas a visualizações HTML. As seguintes associações são feitas aqui:
URL
visualização
/, /home
/templates/home.html
/olá
/templates/hello.html
/login
/modelos/login.html

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

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

8.4.12.4. Configuração do Spring Security

  

A classe [WebSecurityConfig] configura a estrutura Spring Security:


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
 
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • linha 9: a anotação [@Configuration] torna a classe [WebSecurityConfig] uma classe de configuração;
  • linha 10: a anotação [@EnableWebSecurity] torna a classe [WebSecurityConfig] uma classe de configuração do Spring Security;
  • linha 11: a classe [WebSecurity] estende a classe [WebSecurityConfigurerAdapter] para substituir determinados métodos;
  • linha 12: redefinição de um método da classe pai;
  • linhas 13–16: o método [configure(HttpSecurity http)] é substituído para definir direitos de acesso para os vários URLs da aplicação;
  • linha 14: o método [http.authorizeRequests()] permite que as URLs sejam associadas a direitos de acesso. São feitas as seguintes associações:
URL
regra
código
/, /home
acesso sem autenticação

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

8.4.12.5. Classe executável

  

A classe [Application] é a seguinte:


package hello;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
 
    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }
 
}
  • Linha 8: A anotação [@EnableAutoConfiguration] instrui o Spring Boot (linha 3) a realizar a configuração que o programador não definiu explicitamente;
  • linha 9: torna a classe [Application] uma classe de configuração do Spring;
  • linha 10: instrui o sistema a analisar o diretório que contém a classe [Application] para procurar componentes Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão, assim, encontradas porque possuem a anotação [@Configuration];
  • linha 13: o método [main] da classe executável;
  • linha 14: o método estático [SpringApplication.run] é executado com a classe de configuração [Application] como parâmetro. Já nos deparámos com este processo e sabemos que o servidor Tomcat incorporado nas dependências Maven do projeto será iniciado e o projeto implementado nele. Vimos que quatro URLs foram tratadas [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.

8.4.12.6. Testar a aplicação

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

 

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

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

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

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

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

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


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

Assim, obtemos [1]:

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

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • na linha 7, aparece um campo oculto que não consta na página [login.html] original. Foi adicionado pelo Thymeleaf. Este código, conhecido como CSRF (Cross-Site Request Forgery), foi concebido para eliminar uma vulnerabilidade de segurança. Este token deve ser reenviado ao Spring Security juntamente com a autenticação para que seja aceite;

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


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

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

  • em [4], fazemos o login;
  • Em [5], o Spring Security redireciona-nos para a URL [/hello], pois essa foi a URL que solicitámos quando fomos redirecionados para a página de início de sessão. A identidade do utilizador foi apresentada pela seguinte linha do ficheiro [hello.html]:

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

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


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

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


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

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

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

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

8.4.12.7. Conclusão

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

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

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

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

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

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

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

8.4.13.1. A base de dados

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

Image

Tabela [USERS]: utilizadores

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

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

 

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

Tabela [ROLES]: funções

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

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

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

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

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

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

O projeto [rdvmedecins-business-dao] evolui da seguinte forma:

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

8.4.13.3. As novas entidades [JPA]

A camada JPA define três novas entidades:

  

A classe [User] representa a tabela [USERS]:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // properties
    private String identity;
    private String login;
    private String password;
 
    // manufacturer
    public User() {
    }
 
    public User(String identity, String login, String password) {
        this.identity = identity;
        this.login = login;
        this.password = password;
    }
 
    // identity
    @Override
    public String toString() {
        return String.format("User[%s,%s,%s]", identity, login, password);
    }
 
    // getters and setters
....
}
  • linha 9: a classe estende a classe [AbstractEntity] já utilizada para as outras entidades;
  • linhas 13–15: não são especificados nomes de colunas porque têm os mesmos nomes que os seus campos associados;

A classe [Role] reflete a tabela [ROLES]:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
 
    // properties
    private String name;
 
    // manufacturers
    public Role() {
    }
 
    public Role(String name) {
        this.name = name;
    }
 
    // identity
    @Override
    public String toString() {
        return String.format("Role[%s]", name);
    }
 
    // getters and setters
...
}

A classe [UserRole] representa a tabela [USERS_ROLES]:


package rdvmedecins.entities;
 
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
 
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
 
    // a UserRole refers to a User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // a UserRole refers to a Role
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;
 
    // getters and setters
...
}
  • linhas 15-17: definir a chave estrangeira da tabela [USERS_ROLES] para a tabela [USERS];
  • linhas 19-21: implementam a chave estrangeira da tabela [USERS_ROLES] para a tabela [ROLES];

8.4.13.4. Alterações na camada [DAO]

A camada [DAO] foi melhorada com três novos [Repositórios]:

  

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> {
 
    // liste des rôles d'un utilisateur identifié par son id
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);
 
    // liste des rôles d'un utilisateur identifié par son login et son mot de 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);
 
    // recherche d'un utilisateur via son login
    User findUserByLogin(String login);
}
  • linha 9: a interface [UserRepository] estende a interface [CrudRepository] do Spring Data (linha 4);
  • linhas 12-13: o método [getRoles(User user)] recupera todas as funções de um utilizador identificado pelo seu [id]
  • linhas 16-17: igual ao anterior, mas para um utilizador identificado pelo seu nome de utilizador e palavra-passe;
  • Linha 20: para encontrar um utilizador pelo seu nome de utilizador;

A interface [RoleRepository] gere o acesso às entidades [Role]:


package rdvmedecins.security;
 
import org.springframework.data.repository.CrudRepository;
 
public interface RoleRepository extends CrudRepository<Role, Long> {
 
    // search for a role by name
    Role findRoleByName(String name);
 
}
  • linha 5: a interface [RoleRepository] estende a interface [CrudRepository];
  • linha 8: pode pesquisar uma função pelo seu nome;

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


package rdvmedecins.security;
 
import org.springframework.data.repository.CrudRepository;
 
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
 
}
  • linha 5: a interface [UserRoleRepository] limita-se a herdar da interface [CrudRepository] sem adicionar quaisquer novos métodos;

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

  

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

 

Esta interface é implementada aqui pela classe [AppUserDetails]:


package rdvmedecins.security;
 
import java.util.ArrayList;
import java.util.Collection;
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // properties
    private User user;
    private UserRepository userRepository;
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }
 
    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
 
    @Override
    public String getPassword() {
        return user.getPassword();
    }
 
    @Override
    public String getUsername() {
        return user.getLogin();
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
 
    // getters and setters
    ...
}
  • linha 10: a classe [AppUserDetails] implementa a interface [UserDetails];
  • linhas 15–16: a classe encapsula um utilizador (linha 15) e o repositório que fornece detalhes sobre esse utilizador (linha 16);
  • linhas 22–25: o construtor que instancia a classe com um utilizador e o seu repositório;
  • linhas 28–35: implementação do método [getAuthorities] da interface [UserDetails]. Deve construir uma coleção de elementos do tipo [GrantedAuthority] ou de um tipo derivado. Aqui, usamos o tipo derivado [SimpleGrantedAuthority] (linha 32), que encapsula o nome de uma das funções do utilizador da linha 15;
  • linhas 31–33: percorremos a lista de funções do utilizador da linha 15 para construir uma lista de elementos do tipo [SimpleGrantedAuthority];
  • linhas 38–40: implementamos o método [getPassword] da interface [UserDetails]. Devolvemos a palavra-passe do utilizador da linha 15;
  • linhas 38–40: implementamos o método [getUserName] da interface [UserDetails]. Devolvemos o nome de utilizador do utilizador da linha 15;
  • linhas 47–50: a conta do utilizador nunca expira;
  • linhas 52–55: a conta do utilizador nunca é bloqueada;
  • linhas 57–60: as credenciais do utilizador nunca expiram;
  • linhas 62–65: a conta do utilizador está sempre ativa;

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

 

Esta interface é implementada pela seguinte classe [AppUserDetailsService]:


package rdvmedecins.security;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
 
@Service
public class AppUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // search for user via login
        User user = userRepository.findUserByLogin(login);
        // found?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(user, userRepository);
    }
 
}
  • linha 9: a classe será um componente Spring, pelo que estará disponível no seu contexto;
  • linhas 12–13: o componente [UserRepository] será injetado aqui;
  • linhas 16–25: implementação do método [loadUserByUsername] da interface [UserDetailsService] (linha 10). O parâmetro é o nome de utilizador do utilizador;
  • linha 18: o utilizador é procurado através do seu nome de utilizador;
  • linhas 20–22: se o utilizador não for encontrado, é lançada uma exceção;
  • linha 24: um objeto [AppUserDetails] é construído e devolvido. É, de facto, do tipo [UserDetails] (linha 16);

8.4.13.6. Testes da camada [DAO]

  

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


package rdvmedecins.security;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
 
public class CreateUser {
 
    public static void main(String[] args) {
        // syntax: login password roleName
 
        // three parameters are required
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // parameters are retrieved
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // does the role already exist?
        Role role = roleRepository.findRoleByName(roleName);
        // if it doesn't exist, we create it
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // does the user already exist?
        User user = userRepository.findUserByLogin(login);
        // if it doesn't exist, we create it
        if (user == null) {
            // hash the password with bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // save user
            user = userRepository.save(new User(login, login, crypt));
            // we create the relationship with the role
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // the user already exists - does he/she have the required role?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // if not found, we create the relationship with the role
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }
 
        // closing Spring context
        context.close();
    }
 
}
  • linha 17: a classe espera três argumentos que definem um utilizador: o seu nome de utilizador, palavra-passe e função;
  • linhas 25–27: os três parâmetros são recuperados;
  • linha 29: o contexto Spring é construído a partir da classe de configuração [DomainAndPersistenceConfig]. Esta classe já existia no projeto inicial. Deve ser atualizada da seguinte forma:

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

Vamos voltar ao código para criar um utilizador:

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

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

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

Tabela [USERS]

Tabela [FUNÇÕES]

 

Tabela [USERS_ROLES]

 

Agora, vamos considerar a segunda classe [UsersTest], que é um teste JUnit:

  

package rdvmedecins.security;
 
import java.util.List;
 
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
 
import com.google.common.collect.Lists;
 
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
 
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;
 
    @Test
    public void findAllUsersWithTheirRoles() {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(user);
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }
 
    @Test
    public void findUserByLogin() {
        // user [admin] is retrieved
        User user = userRepository.findUserByLogin("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // check admin / admin role
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }
 
    @Test
    public void loadUserByUsername() {
        // user [admin] is retrieved
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // check admin / admin role
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }
 
    // utility method - displays items in a collection
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • linhas 27–34: teste visual. Exibimos todos os utilizadores juntamente com as suas funções;
  • linhas 36–46: verificamos se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN] utilizando o [UserRepository];
  • linha 41: [admin] é a palavra-passe em texto simples. Na base de dados, esta é encriptada utilizando o algoritmo BCrypt. O método [BCrypt.checkpw] verifica se a palavra-passe em texto simples encriptada corresponde à que se encontra na base de dados;
  • linhas 48–59: verificamos se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN] utilizando o [appUserDetailsService];

Os testes foram executados com sucesso com os seguintes registos:

User[admin,admin,$2a$10$FN1LMKjPU46aPffh9Zaw4exJOLo51JJPWrxqzak/eJrbt3CO9WzVG]
Roles :
Role[ROLE_ADMIN]
User[user,user,$2a$10$SJehR9Mv2VdyRZo9F0rXa.hKAoGLhJg6kSdyfExi40mEJrNOj0BTq]
Roles :
Role[ROLE_USER]
User[guest,guest,$2a$10$ubyWJb/vg2XZnUOAUjspZuz9jpHP3fIbPTbwQU115EtLdeSZ2PB7q]
Roles :
Role[ROLE_GUEST]
User[x,x,$2a$10$kEXA56wpKHFReVqwQTyWguKguK8I4uhA2zb6t3wGxag8Dyv7AhLom]
Roles :
Role[ROLE_GUEST]

8.4.13.7. Conclusão provisória

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

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

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

8.4.13.8. O projeto STS para a camada [web]

O projeto [rdvmedecins-webjson] está a evoluir da seguinte forma[1]:

As principais alterações devem ser feitas no ficheiro [rdvmedecins.web.config], onde o Spring Security deve ser configurado. Existem outras alterações menores nas classes [AppConfig] e [ApplicationModel]. Já nos deparámos com uma classe de configuração do Spring Security:


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
 
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

Seguiremos o mesmo procedimento:

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

A configuração do Spring Security é gerida pela classe [SecurityConfig]:


package rdvmedecins.web.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;
 
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;
    @Autowired
    private ApplicationModel application;
 
    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // authentication is performed by bean [appUserDetailsService]
        // the password is encrypted using the BCrypt hash algorithm
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // secure application?
        if (application.isSecured()) {
            // the password is transmitted by the header Authorization: Basic xxxx
            http.httpBasic();
            // the HTTP OPTIONS method must be authorized for all
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // only the ADMIN role can use the application
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // all URL
                    .hasRole("ADMIN");
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • linha 15: a classe [SecurityConfig] é uma classe de configuração do Spring;
  • linha 16: para configurar a segurança do projeto;
  • linhas 19–20: a classe [AppUserDetails], que fornece acesso aos utilizadores da aplicação, é injetada;
  • linhas 21–22: a classe [ApplicationModel], que funciona como um cache para a aplicação web, é injetada. Optamos por utilizá-la também aqui para configurar a aplicação web num único local. Ela define o booleano [isSecured] na linha 36. Este booleano protege (true) ou não protege (false) a aplicação web;
  • linhas 25–29: o método [configure(HttpSecurity http)] define os utilizadores e as suas funções. Aceita um tipo [AuthenticationManagerBuilder] como parâmetro. Este parâmetro é enriquecido com duas informações (linha 28):
    • uma referência ao [appUserDetailsService] da linha 20, que fornece acesso aos utilizadores registados. Note-se aqui que o facto de estarem armazenados numa base de dados não é explicitamente mencionado. Podem, portanto, estar num cache, fornecidos por um serviço web, etc.
    • o tipo de encriptação utilizado para a palavra-passe. Recorde-se que utilizámos o algoritmo BCrypt;
  • linhas 38–47: o método [configure(HttpSecurity http)] define os direitos de acesso às URLs do serviço web;
  • linha 34: vimos no projeto introdutório que, por predefinição, o Spring Security gere um token CSRF (Cross-Site Request Forgery) que o utilizador que tenta autenticar-se deve enviar de volta ao servidor. Aqui, este mecanismo está desativado. Combinado com o booleano (isSecured=false), isto permite que a aplicação web seja utilizada sem segurança;
  • linha 38: Ativamos a autenticação através de cabeçalhos HTTP. O cliente deve enviar o seguinte cabeçalho HTTP:
Authorization:Basic code

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

Authorization:Basic YWRtaW46YWRtaW4=
  • Linhas 40–42: indicam que todos os URLs do serviço web estão acessíveis a utilizadores com a função [ROLE_ADMIN]. Isto significa que um utilizador sem esta função não pode aceder ao serviço web;
  • Linha 47: A palavra-passe do utilizador pode ou não ser armazenada numa sessão. Se for armazenada, o utilizador só precisa de se autenticar na primeira vez. Em pedidos subsequentes, as suas credenciais não são solicitadas. Aqui, optámos por um modo sem sessão. Cada pedido deve ser acompanhado de credenciais de segurança;

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

  

package rdvmedecins.web.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
 
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
 
}
  • A alteração ocorre na linha 11: a classe de configuração [SecurityConfig] é adicionada;

Por fim, a classe [ApplicationModel] é melhorada com um valor booleano:


@Component
public class ApplicationModel implements IMetier {
 
...
    // configuration data
    private boolean secured = false;
 
    public boolean isSecured() {
        return secured;
}
  • Linha 6: Defina o booleano [secured] como [true / false], dependendo se pretende ativar a segurança.

8.4.13.9. Teste do serviço web

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

Authorization:Basic code

onde [código] é a cadeia codificada em Base64 [login:password]. Para gerar este código, pode utilizar o seguinte programa:

  

package rdvmedecins.helpers;
 
import org.springframework.security.crypto.codec.Base64;
 
public class Base64Encoder {
 
    public static void main(String[] args) {
        // we expect two arguments: login password
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // we retrieve the two arguments
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // encode the string
        byte[] data = Base64.encode(chaîne.getBytes());
        // displays its Base64 encoding
        System.out.println(new String(data));
    }
 
}

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

  

obtemos o seguinte resultado:

YWRtaW46YWRtaW4=

Agora que sabemos como gerar o cabeçalho de autenticação HTTP, lançamos o serviço web agora seguro:


@Component
public class ApplicationModel implements IMetier {
...
private boolean secured = true;

Em seguida, utilizando o cliente Chrome [Advanced Rest Client], solicitamos a lista de todos os médicos:

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

A resposta do servidor é a seguinte:

  • em [1], o cabeçalho de autenticação HTTP;
  • em [2], o servidor devolve uma resposta JSON;
  • em [3], uma lista de cabeçalhos HTTP relacionados com a segurança da aplicação web;

Conseguimos obter a lista de médicos:

 

Agora vamos 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 tentar o utilizador «user / user». Ele existe, mas não tem acesso ao serviço web. Se executarmos o programa de codificação Base64 com os dois argumentos [user user]:

  

obtemos o seguinte resultado:

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

Um serviço web seguro está agora operacional. Vamos ampliá-lo para permitir pedidos entre domínios. Este requisito foi mencionado no documento [Tutorial AngularJS / Spring 4] e, embora não se aplique aqui, vamos abordá-lo na mesma.

8.4.14. Implementação de pedidos entre domínios

Vamos examinar a questão das solicitações entre domínios. No documento [Tutorial AngularJS / Spring 4], estamos a desenvolver uma aplicação cliente/servidor em que o cliente é uma aplicação AngularJS:

  • as páginas HTML/CSS/JS da aplicação Angular provêm do servidor [1];
  • em [2], o serviço [dao] faz uma solicitação a outro servidor, o servidor [2]. Ora, isso é proibido pelo navegador que executa a aplicação Angular, pois constitui uma vulnerabilidade de segurança. A aplicação só pode consultar o servidor de onde provém, ou seja, o servidor [1];

Na verdade, não é correto dizer que o navegador impede a aplicação Angular de consultar o servidor [2]. Na realidade, consulta-o para perguntar se este permite que um cliente que não tenha origem nele o consulte. Esta técnica de partilha é chamada CORS (Cross-Origin Resource Sharing). O servidor [2] concede permissão enviando cabeçalhos HTTP específicos.

Para demonstrar os problemas que podem surgir, vamos criar uma aplicação cliente/servidor em que:

  • o servidor será o nosso servidor web/JSON;
  • o cliente será uma página HTML simples equipada com código JavaScript que fará pedidos ao servidor web/JSON;

8.4.14.1. O projeto do cliente

  

O projeto é um projeto Maven com o seguinte ficheiro [pom.xml]:


<?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>istia.st</groupId>
        <artifactId>rdvmedecins-webjson-client-cors</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
 
        <name>rdvmedecins-webjson-client-cors</name>
        <description>Client for webjson server</description>
 
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
                <relativePath /> <!-- lookup parent from repository -->
        </parent>
 
        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <start-class>istia.st.rdvmedecins.Client</start-class>
                <java.version>1.8</java.version>
        </properties>
 
        <dependencies>
                <!-- spring MVC -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
        </dependencies>
</project>
  • linhas 14–19: este é um projeto Spring Boot;
  • linhas 29–32: utilizamos a dependência [spring-boot-starter-web], que inclui um servidor Tomcat e o Spring MVC;

A página HTML é a seguinte:

 

É gerado pelo seguinte código:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
    <h2>Client du service web / jSON</h2>
    <form id="formulaire">
        <!--  method HTTP -->
        Méthode HTTP :
        <!--  -->
        <input type="radio" id="get" name="method" value="get" checked="checked" />GET
        <!--  -->
        <input type="radio" id="post" name="method" value="post" />POST
        <!--  URL -->
        <br /> <br />URL cible : <input type="text" id="url" size="30"><br />
        <!-- posted value -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
        <!-- validation button -->
        <br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
    </form>
    <hr />
    <h2>Réponse du serveur</h2>
    <div id="response"></div>
</body>
</html>
  • linha 6: importamos a biblioteca jQuery;
  • linha 7: importamos o código que iremos escrever;

O código [client.js] é o seguinte:


// global data
var url;
var posted;
var response;
var method;
 
function requestServer() {
    // retrieve information from the form
    var urlValue = url.val();
    var postedValue = posted.val();
    method = document.forms[0].elements['method'].value;
    // make a manual Ajax call
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}
 
function doGet(url) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8080' + url,
        type : 'GET',
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(jqXHR.responseText);
        }
    })
}
 
function doPost(url, posted) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8080' + url,
        type : 'POST',
        contentType : 'application/json',
        data : posted,
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(jqXHR.responseText);
        }
    })
}
 
// document loading
$(document).ready(function() {
    // retrieve page component references
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});

Deixaremos que seja o leitor a compreender este código. Tudo já foi abordado em algum momento. No entanto, algumas linhas merecem uma explicação:

  • linha 11:
    • [document] refere-se ao documento carregado pelo navegador, conhecido como DOM (Document Object Model),
    • [document.forms[0]] refere-se ao primeiro formulário no documento; um documento pode conter vários formulários. Aqui, existe apenas um,
    • [document.forms[0].elements['method']] refere-se ao elemento do formulário com o atributo [name='method']. Existem dois deles:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • linha 11:
    • [document.forms[0].elements['method'].value] é o valor que será enviado para o componente com o atributo [name='method']. Sabemos que o valor enviado é o valor do atributo [value] do botão de opção selecionado. Aqui, será, portanto, uma das cadeias de caracteres ['get', 'post'];
  • linhas 23–25: estamos a comunicar com um servidor que requer um cabeçalho HTTP [Authorization: Basic code]. Criamos este cabeçalho para o utilizador [admin / admin], que é o único autorizado a consultar o servidor;
  • linha 26: o utilizador irá introduzir URLs do tipo [/getAllDoctors, /deleteAppointment, ...]. Estas URLs devem, portanto, ser preenchidas;
  • linha 28: o servidor devolve JSON, que é um formato de texto. Especificamos o tipo [text/plain] como tipo de resposta para que seja apresentado exatamente como recebido;
  • linha 33: exibe a resposta de texto do servidor;
  • linha 39: exibe quaisquer mensagens de erro em formato de texto;
  • linha 52: para indicar que o cliente está a enviar JSON;

Na aplicação cliente/servidor que estamos a construir:

  • o cliente é uma aplicação web disponível no URL [http://localhost:8081]. Esta é a aplicação que estamos atualmente a construir;
  • o servidor é uma aplicação web disponível no URL [http://localhost:8080]. Este é o nosso servidor web/JSON;

Como o cliente não está a ser executado na mesma porta que o servidor, surge a questão das solicitações entre domínios. [http://localhost:8080] e [http://localhost:8081] são dois domínios diferentes.

A aplicação Spring Boot é uma aplicação de consola lançada pela seguinte classe executável [Client]:


package istia.st.rdvmedecins;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {
 
    public static void main(String[] args) {
        SpringApplication.run(Client.class, args);
    }
 
    // static pages
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
    }
 
    // configuration dispatcherServlet
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }
 
    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }
 
    // embedded Tomcat server
    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8081);
    }
 
}
  • Linha 14: A classe [Client] é uma classe de configuração Spring;
  • linha 15: é configurada uma aplicação Spring MVC. Esta anotação desencadeia uma série de configurações automáticas;
  • linha 16: para substituir determinados valores padrão da estrutura Spring MVC, deve-se estender a classe [WebMvcConfigurerAdapter];
  • linhas 23–26: o método [addResourceHandlers] permite especificar os diretórios onde se encontram os recursos estáticos da aplicação (HTML, CSS, JS, etc.). Aqui, especificamos o diretório [static] localizado no classpath do projeto:
  
  • linhas 29–37: configuração do bean [dispatcherServlet], que designa o servlet Spring MVC;
  • linhas 40-43: o servidor Tomcat incorporado será executado na porta 8081;

8.4.14.2. A URL [/getAllMedecins]

Lançamos:

  • o servidor web/JSON na porta 8080;
  • o cliente para este servidor na porta 8081;

depois solicitamos a URL [http://localhost:8081/client.html] [1]:

  • em [2], efetuamos uma solicitação GET na URL [http://localhost:8080/getAllMedecins];

Não recebemos uma resposta do servidor. Quando consultamos a consola do programador (Ctrl-Shift-I), vemos um erro:

  • em [1], estamos no separador [Rede];
  • Em [2], vemos que o pedido HTTP efetuado não é [GET], mas sim [OPTIONS]. No caso de um pedido entre domínios, o navegador verifica junto do servidor se determinadas condições estão preenchidas, enviando um pedido HTTP [OPTIONS]. Neste caso, os pedidos são os indicados pelos círculos [5-6];
  • Em [5], o navegador pergunta se o URL de destino pode ser alcançado com um GET. O cabeçalho de solicitação [Access-Control-Request-Method] solicita uma resposta com um cabeçalho HTTP [Access-Control-Allow-Methods] indicando que o método solicitado é aceito;
  • em [5], o navegador envia o cabeçalho HTTP [Origin: http://localhost:8081]. Este cabeçalho solicita uma resposta num cabeçalho HTTP [Access-Control-Allow-Origin] indicando que a origem especificada é aceite;
  • Em [6], o navegador pergunta se os cabeçalhos HTTP [Accept] e [Authorization] são aceites. O cabeçalho de pedido [Access-Control-Request-Headers] espera uma resposta com um cabeçalho HTTP [Access-Control-Allow-Headers] indicando que os cabeçalhos solicitados são aceites;
  • ocorre um erro em [3]. Clicar no ícone resulta no erro [4];
  • em [4], a mensagem indica que o servidor não enviou o cabeçalho HTTP [Access-Control-Allow-Origin], que especifica se a origem da solicitação é aceita;
  • Em [7], podemos ver que o servidor, de facto, não enviou este cabeçalho. Como resultado, o navegador recusou-se a efetuar a solicitação HTTP GET que foi inicialmente solicitada;

Precisamos de modificar o servidor web/JSON. Fazemos uma alteração inicial em [ApplicationModel], que é um dos elementos de configuração do serviço web:

 

@Component
public class ApplicationModel implements IMetier {
 
    ...
    // configuration data
    private boolean corsAllowed = true;
    private boolean secured = true;
 
...
    public boolean isCorsAllowed() {
        return corsAllowed;
}
  • linha 6: criamos uma variável booleana que indica se os clientes fora do domínio do servidor são aceites ou não;
  • linhas 10–12: o método para aceder a esta informação;

Em seguida, criamos um novo controlador Spring MVC:

  

A classe [RdvMedecinsCorsController] é a seguinte:


package rdvmedecins.web.controllers;
 
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
import rdvmedecins.web.models.ApplicationModel;
 
@Controller
public class RdvMedecinsCorsController {
 
    @Autowired
    private ApplicationModel application;
 
    // sending options to the customer
    public void sendOptions(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // we authorize GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        sendOptions(origin, response);
    }
}
  • linhas 12-13: a classe [RdvMedecinsCorsController] é um controlador Spring;
  • linhas 33–36: definem uma ação que trata a URL [/getAllMedecins] quando esta é solicitada com o método HTTP [OPTIONS];
  • linha 34: o método [getAllMedecins] aceita os seguintes parâmetros:
    • o objeto [@RequestHeader(value = "Origin", required = false)] que recupera o cabeçalho HTTP [Origin] da solicitação. Este cabeçalho foi enviado pelo remetente da solicitação:
Origin:http://localhost:8081

Especificamos que o cabeçalho HTTP [Origin] é opcional [required = false]. Neste caso, se o cabeçalho estiver em falta, o parâmetro [String origin] terá o valor null. Com [required = true], que é o valor padrão, é lançada uma exceção se o cabeçalho estiver em falta. Queríamos evitar este cenário;

  • linha 34:
    • o objeto [HttpServletResponse response] que será enviado ao cliente que efetuou o pedido;

Estes dois parâmetros são injetados pelo Spring;

  • linha 35: delegamos o processamento da solicitação ao método nas linhas 19–30;
  • linhas 15–16: o objeto [ApplicationModel] é injetado;
  • linhas 21–23: se a aplicação estiver configurada para aceitar pedidos entre domínios, e se o remetente tiver enviado o cabeçalho HTTP [Origin], e se essa origem começar por [http://localhost], então aceitamos o pedido entre domínios; caso contrário, rejeitamo-lo;
  • linha 25: se o cliente estiver no domínio [http://localhost:port], o cabeçalho HTTP é enviado:

Access-Control-Allow-Origin:  http://localhost:port

o que significa que o servidor aceita a origem do cliente;

  • Linha 25: Especificámos dois cabeçalhos HTTP específicos na solicitação HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

Em resposta ao cabeçalho HTTP [Access-Control-Request-X], o servidor responde com um cabeçalho HTTP [Access-Control-Allow-X] especificando o que é permitido. As linhas 23–26 limitam-se a repetir o pedido do cliente para indicar que este foi aceite;

Estamos agora prontos para mais testes. Lançamos a nova versão do serviço web e verificamos que o problema permanece inalterado. Nada mudou. Se adicionarmos uma saída de consola na linha 35 acima, esta nunca é apresentada, indicando que o método [getAllMedecins] na linha 34 nunca é chamado.

Após alguma pesquisa, descobrimos que o Spring MVC lida com os pedidos HTTP [OPTIONS] por si próprio, utilizando o seu tratamento padrão. Por conseguinte, é sempre o Spring que responde, e nunca o método [getAllMedecins] na linha 34. Este comportamento padrão do Spring MVC pode ser alterado. Modificamos a classe [WebConfig] existente:

  

package rdvmedecins.web.config;
 
...
import org.springframework.web.servlet.DispatcherServlet;
 
@Configuration
public class WebConfig {
 
    // dispatcherservlet configuration for CORS headers
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
 
    // mapping jSON
...
  • linhas 10-11: o bean [dispatcherServlet] é utilizado para definir o servlet que trata dos pedidos dos clientes. Aqui, é do tipo [DispatcherServlet], o servlet da estrutura Spring MVC;
  • linha 12: criamos uma instância do tipo [DispatcherServlet];
  • linha 13: instruímos o servlet a reencaminhar as solicitações HTTP [OPTIONS] para a aplicação;
  • linha 14: apresentamos o servlet configurado desta forma;

Executamos novamente os testes com esta nova configuração. Obtemos o seguinte resultado:

  • em [1], vemos que existem duas solicitações HTTP para a URL [http://localhost:8080/getAllMedecins];
  • em [2], a solicitação [OPTIONS];
  • em [3], os três cabeçalhos HTTP que acabámos de configurar na resposta do servidor;

Agora, vamos examinar a segunda solicitação:

  • em [1], a solicitação que está a ser analisada;
  • em [2], trata-se da solicitação GET. Graças à primeira solicitação [OPTIONS], o navegador recebeu as informações que solicitou. Agora, está a executar a solicitação [GET] que foi inicialmente solicitada;
  • em [3], a resposta do servidor;
  • em [4], o servidor envia JSON;
  • em [5], ocorreu um erro;
  • em [6], a mensagem de erro;

É mais difícil explicar o que aconteceu aqui. A resposta do servidor [3] é normal [HTTP/1.1 200 OK]. Devemos, portanto, ter o documento solicitado. É possível que o servidor tenha de facto enviado o documento, mas que o navegador esteja a impedir a sua utilização porque exige que, também para o pedido GET, a resposta inclua o cabeçalho HTTP [Access-Control-Allow-Origin:http://localhost:8081].

Modificamos o controlador [RdvMedecinsController] da seguinte forma:


    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;
...
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins(HttpServletResponse httpServletResponse,
            @RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
        // the answer
        Response<List<Medecin>> response;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
...
  • linhas 1-2: o controlador [RdvMedecinsCorsController] é injetado;
  • linhas 7-8: o objeto HttpServletResponse, que encapsula a resposta a ser enviada ao cliente, e o cabeçalho HTTP [Origin] são injetados nos parâmetros do método [getAllMedecins];
  • linha 12: o método [sendOptions] do controlador [RdvMedecinsCorsController] é chamado — o mesmo método que foi chamado para tratar a solicitação HTTP [OPTIONS]. Ele enviará, portanto, os mesmos cabeçalhos HTTP dessa solicitação;

Após esta modificação, os resultados são os seguintes:

 

Conseguimos obter a lista de médicos.

8.4.14.3. Os outros URLs [GET]

Vamos agora analisar as outras URLs consultadas através de um pedido GET. Nos controladores, o código das ações que as tratam segue o mesmo padrão das ações que anteriormente tratavam da URL [/getAllMedecins]. O leitor pode verificar o código nos exemplos fornecidos com este documento. Aqui está um exemplo:

em [RdvMedecinsCorsController]


    // list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
    public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin,    HttpServletResponse response) {
        sendOptions(origin, response);
}

em [RdvMedecinsController]


    // list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour,
            HttpServletResponse httpServletResponse, @RequestHeader(value = "Origin", required = false) String origin)
                    throws JsonProcessingException {
        // the answer
        Response<List<Rv>> response = null;
        boolean erreur = false;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
...

Aqui estão algumas capturas de ecrã da execução:

 
 
 
 
 
 

8.4.14.4. Os URLs [POST]

Vamos analisar o seguinte cenário:

  • Enviamos um POST [1] para a URL [2];
  • em [3], o valor enviado. Trata-se de uma cadeia JSON;
  • Em suma, estamos a tentar eliminar a marcação com [id] 100;

Não estamos a modificar nenhum código nesta fase. O resultado obtido é o seguinte:

  • em [1], tal como nas solicitações [GET], o navegador efetua uma solicitação [OPTIONS];
  • em [2], solicita autorização de acesso para uma solicitação [POST]. Anteriormente, era [GET];
  • Em [3], solicita autorização para enviar os cabeçalhos HTTP [accept, authorization, content-type]. Anteriormente, só tínhamos os dois primeiros cabeçalhos;

Modificamos o método [RdvMedecinsCorsController.sendOptions] da seguinte forma:


    public void sendOptions(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // we authorize GET
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
  • linha 9: adicionámos o cabeçalho HTTP [Content-Type] (não distingue maiúsculas de minúsculas);
  • linha 11: adicionámos o método HTTP [POST];

Isto significa que os métodos [POST] são tratados da mesma forma que os pedidos [GET]. Aqui está um exemplo da URL [/deleteAppointment]:

em [RdvMedecinsController]


    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse httpServletResponse,
            @RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
        // the answer
        Response<Void> response = null;
        boolean erreur = false;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
        if (messages != null) {
...

em [RdvMedecinsCorsController]


    @RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
    public void supprimerRv(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        sendOptions(origin, response);
}

O resultado é o seguinte:

 

Para o URL [/addRv], obtém-se o seguinte resultado:

 

8.4.14.5. Conclusão

A nossa aplicação suporta agora pedidos entre domínios. Estes podem ser ativados ou desativados através da configuração na classe [ApplicationModel]:


    // données de configuration
    private boolean corsAllowed = false;

8.5. Cliente de serviço web / JSON

Voltemos à arquitetura geral da aplicação que pretendemos construir:

A parte superior do diagrama já foi escrita. Trata-se do servidor web/JSON. Vamos agora abordar a parte inferior, começando pela sua camada [DAO]. Vamos escrever esta camada e, em seguida, testá-la com um cliente de consola. A arquitetura de teste será a seguinte:

8.5.1. O projeto do cliente de consola

O projeto STS para o cliente de consola será o seguinte:

  

8.5.2. Configuração do Maven

O ficheiro [pom.xml] para o cliente de consola é o seguinte:


<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>istia.st.rdvmedecins</groupId>
        <artifactId>rdvmedecins-webjson-client-console</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>rdvmedecins-webjson-client-console</name>
        <description>Client console du serveur web / jSON</description>
 
        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <java.version>1.8</java.version>
        </properties>
 
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
                <relativePath /> <!-- lookup parent from repository -->
        </parent>
 
        <dependencies>
                <!-- Spring -->
                <dependency>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-web</artifactId>
                </dependency>
                <!-- jSON library used by Spring -->
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-core</artifactId>
                </dependency>
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-databind</artifactId>
                </dependency>
                <!-- component used by Spring RestTemplate -->
                <dependency>
                        <groupId>org.apache.httpcomponents</groupId>
                        <artifactId>httpclient</artifactId>
                </dependency>
        </dependencies>
</project>
  • linhas 15–20: o projeto Spring Boot pai;
  • linhas 24–27: o servidor web/cliente de consola JSON baseia-se num componente chamado [RestTemplate] fornecido pela dependência [spring-web];
  • linhas 29–36: a serialização e deserialização de objetos JSON requerem uma biblioteca JSON. Utilizamos uma variante da biblioteca Jackson utilizada pelo Spring Web;
  • linhas 38–41: no nível mais baixo, o componente [RestTemplate] comunica com o servidor através de sockets TCP/IP. Pretendemos definir o [timeout] para estes, ou seja, o tempo máximo de espera por uma resposta do servidor. O componente [RestTemplate] não nos permite definir isto. Para o fazer, iremos passar um componente de baixo nível fornecido pela dependência [org.apache.httpcomponents.httpclient] para o construtor [RestTemplate]. É esta dependência que nos permitirá definir o [timeout] de comunicação;

8.5.3. O pacote [rdvmedecins.client.entities]

  

O pacote [rdvmedecins.client.entities] contém todas as entidades que o serviço web / JSON envia através das suas várias URLs. Não vamos entrar em detalhes sobre elas novamente. Basta dizer que as entidades JPA [Client, Slot, Doctor, Appointment, Person] foram despojadas de todas as suas anotações JPA, bem como das suas anotações JSON. Aqui, por exemplo, está a classe [Appointment]:


package rdvmedecins.client.entities;
 
import java.util.Date;
 
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // day of appointment
    private Date jour;
 
    // an appointment is linked to a customer
    private Client client;
 
    // an appointment is linked to a time slot
    private Creneau creneau;
 
    // foreign keys
    private long idClient;
    private long idCreneau;
 
    // default builder
    public Rv() {
    }
 
    // with parameters
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }
 
    // toString
    public String toString() {
        return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
    }
 
// getters and setters
...
}

8.5.4. O pacote [rdvmedecins.client.requests]

  

O pacote [rdvmedecins.client.requests] contém as duas classes cujos valores JSON são enviados para as URLs [/ajouterRv] e [supprimerRv]. São idênticas às suas contrapartes do lado do servidor.

8.5.5. O pacote [rdvmedecins.client.responses]

  

[Response] é o tipo de todas as respostas de serviços web / JSON. É um tipo genérico:


package rdvmedecins.client.responses;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}
  • linha 5: o tipo [T] varia dependendo do URL do serviço web / JSON;

8.5.6. O pacote [rdvmedecins.client.dao]

  
  • [IDao] é a interface da camada [DAO] e [Dao] é a sua implementação. Voltaremos a esta implementação;

8.5.7. O pacote [rdvmedecins.client.config]

  

A classe [DaoConfig] configura a aplicação. O seu código é o seguinte:


package rdvmedecins.client.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
 
    @Bean
    public RestTemplate restTemplate() {
        // creation of the RestTemplate component
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // result
        return restTemplate;
    }
 
    // mappers jSON
 
    @Bean
    public ObjectMapper jsonMapper(){
        return new ObjectMapper();
    }
 
    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
    }
 
    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",
                creneauFilter));
        return jsonMapperLongRv;
    }
 
    @Bean
    public ObjectMapper jsonMapperShortRv() {
        ObjectMapper jsonMapperShortRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
        jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
        return jsonMapperShortRv;
    }
 
}
  • linha 13: a classe [DaoConfig] é uma classe de configuração do Spring;
  • linha 14: o pacote [rdvmedecins.client.dao] será pesquisado em busca de componentes Spring. O componente [Dao] será encontrado nesse local;
  • Linhas 17–24: definem um singleton Spring denominado [restTemplate] (o nome do método). Este método devolve uma instância [RestTemplate], que é a ferramenta básica que o Spring fornece para comunicar com um serviço web ou JSON;
  • Linha 21: Poderíamos escrever [RestTemplate restTemplate = new RestTemplate();]. Isto é suficiente na maioria dos casos. Mas aqui, queremos definir os [timeouts] do cliente. Para isso, injetamos um componente de baixo nível do tipo [HttpComponentsClientHttpRequestFactory] (linha 20) no componente [RestTemplate], o que nos permitirá definir esses [timeouts]. A dependência Maven necessária já foi fornecida;
  • linhas 28–57: definem mapeadores JSON. Estes são os mapeadores JSON utilizados no lado do servidor (ver secção 8.4.11.3) para serializar o tipo T da resposta [Response<T>]. Estes mesmos conversores serão agora utilizados no lado do cliente para deserializar o tipo T;

8.5.8. A interface [IDao]

Voltemos à arquitetura da aplicação:

A camada [DAO] funciona como um adaptador entre a camada [console] e os URLs expostos pelo serviço web / JSON. A sua interface [IDao] será a seguinte:


package rdvmedecins.client.dao;
 
import java.util.List;
 
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
 
public interface IDao {
    // Web service url
    public void setUrlServiceWebJson(String url);
 
    // timeout
    public void setTimeout(int timeout);
 
    // authentication
    public void authenticate(User user);
 
    // customer list
    public List<Client> getAllClients(User user);
 
    // list of doctors
    public List<Medecin> getAllMedecins(User user);
 
    // list of physician slots
    public List<Creneau> getAllCreneaux(User user, long idMedecin);
 
    // find a customer identified by its id
    public Client getClientById(User user, long id);
 
    // find a customer identified by its id
    public Medecin getMedecinById(User user, long id);
 
    // find an Rv identified by its id
    public Rv getRvById(User user, long id);
 
    // find a time slot identified by its id
    public Creneau getCreneauById(User user, long id);
 
    // add a RV to the list
    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
 
    // delete a RV
    public void supprimerRv(User user, long idRv);
 
    // list of doctor's appointments on a given day
    public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
 
    // agenda
    public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
 
}
  • linha 14: o método para definir a URL raiz do serviço web / JSON, por exemplo [http://localhost:8080];
  • linha 17: o método utilizado para definir os [tempos de espera] do lado do cliente. Queremos controlar este parâmetro porque alguns clientes HTTP podem demorar muito tempo à espera de uma resposta que nunca chegará;
  • linha 20: o método para autenticar um utilizador [login, passwd]. Lança uma exceção se o utilizador não for reconhecido;
  • linhas 22–53: Cada URL exposta pelo serviço web / JSON está associada a um método da interface, cuja assinatura é derivada da assinatura do método do lado do servidor que trata da URL exposta. Tomemos, por exemplo, a seguinte URL do servidor:

    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Response<String> getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin,    @PathVariable("jour") String jour, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
  • linha 1: vemos que [idMedecin] e [jour] são os parâmetros da URL. Estes serão os parâmetros de entrada para o método associado a esta URL no lado do cliente;
  • linha 2: vemos que o método do servidor retorna um tipo [Response<String>]. Este tipo [String] é o tipo do valor JSON de tipo [AgendaMedecinJour]. O tipo de resultado do método associado a esta URL no lado do cliente será [AgendaMedecinJour];

No lado do cliente, declaramos o seguinte método:


public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);

Esta assinatura funciona quando o servidor envia uma resposta do tipo [int status, List<String> messages, String body] com [status==0]. Neste caso, temos [messages==null && body!=null]. Não funciona quando [status!=0]. Neste caso, temos [mensagens!=null && corpo==null]. Precisamos de sinalizar de alguma forma que ocorreu um erro. Para tal, lançaremos uma exceção do tipo [RdvMedecinsException] da seguinte forma:


package rdvmedecins.client.dao;
 
import java.util.List;
 
public class RdvMedecinsException extends RuntimeException {
 
    private static final long serialVersionUID = 1L;
    // error code
    private int status;
    // list of error messages
    private List<String> messages;
 
    public RdvMedecinsException() {
    }
 
    public RdvMedecinsException(int code, List<String> messages) {
        super();
        this.status = code;
        this.messages = messages;
    }
 
    // getters and setters
...
}
  • linhas 9 e 11: a exceção irá assumir os valores dos campos [status, messages] do objeto [Response<T>] enviado pelo servidor;
  • linha 5: a classe [RdvMedecinsException] estende a classe [RuntimeException]. Trata-se, portanto, de uma exceção não tratada, o que significa que não é necessário tratá-la com um bloco try/catch nem declará-la nas assinaturas dos métodos da interface;

Além disso, todos os métodos da interface [IDao] que consultam o serviço web/JSON têm o seguinte tipo [User] como parâmetro:


package rdvmedecins.client.entities;
 
public class User {
 
    // data
    private String login;
    private String passwd;
 
    // manufacturers
    public User() {
    }
 
    public User(String login, String passwd) {
        this.login = login;
        this.passwd = passwd;
    }
 
    // getters and setters
    ...
}

Na verdade, todas as trocas com o serviço web / JSON devem ser acompanhadas por um cabeçalho de autenticação HTTP.

8.5.9. O pacote [rdvmedecins.clients.console]

Agora que já estamos familiarizados com a interface da camada [DAO], podemos apresentar a aplicação de consola.

  

A classe [Main] é a seguinte:


package rdvmedecins.clients.console;
 
import java.io.IOException;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Main {
 
    // serializer jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // connection timeout in milliseconds
    static private int TIMEOUT = 1000;
 
    public static void main(String[] args) throws IOException {
        // we retrieve a reference on the [DAO] layer
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // set the URL of the web/json service
        dao.setUrlServiceWebJson("http://localhost:8080");
        // set timeouts in milliseconds
        dao.setTimeout(TIMEOUT);
 
        // Authentication
        String message = "/authenticate [admin,admin]";
        try {
            dao.authenticate(new User("admin", "admin"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [user,user]";
        try {
            dao.authenticate(new User("user", "user"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [user,x]";
        try {
            dao.authenticate(new User("user", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [x,x]";
        try {
            dao.authenticate(new User("x", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [admin,x]";
        try {
            dao.authenticate(new User("admin", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // customer list
        message = "/getAllClients";
        try {
            showResponse(message, dao.getAllClients(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // list of doctors
        message = "/getAllMedecins";
        try {
            showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // list of slots for doctor 2
        message = "/getAllCreneaux/2";
        try {
            showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // customer no. 1
        message = "/getClientById/1";
        try {
            showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor no. 2
        message = "/getMedecinById/2";
        try {
            showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // slot no. 3
        message = "/getCreneauById/3";
        try {
            showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // rv n° 4
        message = "/getRvById/4";
        try {
            showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // adding an appointment
        message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
        long idRv = 0;
        try {
            Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
            idRv = response.getId();
            showResponse(message, response);
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor's appointment list 1 on 2015-01-08
        message = "/getRvMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor's agenda 1 on 2015-01-08
        message = "/getAgendaMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
        // delete added rv
        message = String.format("/supprimerRv [idRv=%s]", idRv);
        try {
            dao.supprimerRv(new User("admin", "admin"), idRv);
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor's appointment list 1 on 2015-01-08
        message = "/getRvMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
        // closing context
        context.close();
    }
 
    private static void showException(String message, RdvMedecinsException e) {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
        for (String msg : e.getMessages()) {
            System.out.println(msg);
        }
    }
 
    private static <T> void showResponse(String message, T response) throws JsonProcessingException {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(mapper.writeValueAsString(response));
    }
}
  • linha 19: o serializador JSON que nos permitirá exibir a resposta do servidor, linha 184;
  • linha 25: o componente [AnnotationConfigApplicationContext] é um componente Spring capaz de utilizar anotações de configuração de uma aplicação Spring. Passamos a classe [AppConfig], que configura a aplicação, para o seu construtor;
  • linha 26: recuperamos uma referência à camada [DAO];
  • linhas 27–30: configuramo-la;
  • linhas 32–169: testamos todos os métodos da interface [IDao];

Os resultados obtidos são os seguintes:


09:20:56.935 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
/authenticate [admin,admin] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [user,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/authenticate [x,x]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [admin,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/getAllClients]
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]
URL [/getAllMedecins]
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]
URL [/getAllCreneaux/2]
[{"id":25,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":2},{"id":26,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":2},{"id":27,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":2},{"id":28,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":2},{"id":29,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":2},{"id":30,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":2},{"id":31,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":2},{"id":32,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":2},{"id":33,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":2},{"id":34,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":2},{"id":35,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":2},{"id":36,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":2}]
URL [/getClientById/1]
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}
URL [/getMedecinById/2]
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"}
URL [/getCreneauById/3]
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1}
URL [/getRvById/4]
L'erreur n° [2] s'est produite :
Le rendez-vous d'id [4] n'existe pas
URL [/ajouterRv [idClient=4,idCreneau=8,jour=2015-01-08]]
{"id":144,"version":0,"jour":1420671600000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":0,"idCreneau":0}
URL [/getRvMedecinJour/1/2015-01-08]
[{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}]
URL [/getAgendaMedecinJour/1/2015-01-08]
{"medecin":{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},"jour":1420671600000,"creneauxMedecinJour":[{"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"rv":{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"medecin":null,"idMedecin":1},"rv":null}]}
URL [/getRvMedecinJour/1/2015-01-08]
[]
09:21:00.258 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy

Deixamos ao leitor a tarefa de correlacionar os resultados com o código. O código mostra como chamar cada método da camada [DAO]. Vamos apenas destacar alguns pontos:

  • linhas 2–14: mostram que, em caso de erro de autenticação, o servidor retorna um status HTTP [403 Forbidden] ou [401 Unauthorized], conforme apropriado;
  • linhas 30–31: é adicionada uma consulta para o Médico n.º 1;
  • linhas 32–33: vemos este compromisso. É o único do dia;
  • linhas 34–35: também está visível no calendário do médico;
  • linhas 36-37: a consulta desapareceu. O código eliminou-a entretanto;

Os registos da consola são controlados pelos seguintes ficheiros:

 

[application.properties]


logging.level.org.springframework.web=OFF
logging.level.org.hibernate=OFF
spring.main.show-banner=false
logging.level.httpclient.wire=OFF

[logback.xml]


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- log level control -->
        <root level="info"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>

8.5.10. Implementação da camada [DAO]

Precisamos agora de apresentar o núcleo da camada [DAO]: a implementação da sua interface [IDao]. Faremos isso passo a passo.

 

A interface [IDao] é implementada pela classe abstrata [AbstractDao] e pela sua classe derivada [Dao].

A classe pai [AbstractDao] é a seguinte:


package rdvmedecins.client.dao;
 
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
 
import rdvmedecins.client.entities.User;
 
public abstract class AbstractDao implements IDao {
 
    // data
    @Autowired
    protected RestTemplate restTemplate;
    protected String urlServiceWebJson;
 
    // URL web service / jSON
    public void setUrlServiceWebJson(String url) {
        this.urlServiceWebJson = url;
    }
 
    public void setTimeout(int timeout) {
        // set the timeout for web client requests
        HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
                .getRequestFactory();
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
    }
 
    private String getBase64(User user) {
        // encodes user and password in base 64 - requires
        // java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }
 
    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
...
    }
 
}
  • linha 20: a classe é abstrata, o que nos impede de a designar como um componente Spring. A sua classe filha será designada como tal;
  • linhas 23–24: injetamos o bean [restTemplate] que definimos na classe de configuração [AppConfig];
  • linha 25: a URL raiz do serviço web / JSON;
  • linhas 32–38: definimos o tempo de espera do cliente enquanto aguardamos uma resposta do servidor;
  • linha 34: recuperamos o componente [HttpComponentsClientHttpRequestFactory] que injetámos no bean [restTemplate] quando este foi criado (ver [AppConfig]);
  • linha 36: definimos o tempo máximo de espera do cliente ao estabelecer uma ligação com o servidor;
  • linha 37: definimos o tempo máximo de espera do cliente enquanto este aguarda uma resposta a uma das suas solicitações;

A implementação dos métodos para comunicação com o servidor será incorporada no seguinte método genérico:


    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
...
    }
  • Linha 2: Os parâmetros de [getResponse] são os seguintes:
    • [User user]: o utilizador que estabelece a ligação;
    • [String url]: a URL a consultar. Esta é a parte final da URL; a primeira parte é fornecida pelo campo [urlServiceWebJson] da classe,
    • [String jsonPost]: a string JSON a enviar. Se este valor estiver presente, a URL será solicitada com um POST; caso contrário, será com um GET;

Vamos continuar:


// generic request
    protected String getResponse(User user, String url, String jsonPost) {
        // url : URL to contact
        // jsonPost: the jSON value to be posted
        try {
            // request execution
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }
            // execute the query
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e) {
            throw new RdvMedecinsException(20, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(21, getMessagesForException(e));
        }
    }
  • Linhas 23–24: A instrução que envia o pedido ao servidor e recebe a sua resposta. O componente [RestTemplate] oferece uma vasta gama de métodos para interagir com o servidor. Poderíamos ter escolhido um método diferente de [exchange]. O segundo parâmetro da chamada especifica o tipo da resposta esperada, neste caso uma cadeia de caracteres JSON. O primeiro parâmetro é o pedido [RequestEntity] (linha 7). O resultado do método [exchange] é do tipo [ResponseEntity<String>]. O tipo [ResponseEntity] encapsula a resposta completa do servidor, incluindo cabeçalhos HTTP e o documento enviado pelo servidor. Da mesma forma, o tipo [RequestEntity] encapsula toda a solicitação do cliente, incluindo cabeçalhos HTTP e quaisquer dados enviados;
  • linha 23: este é o corpo do objeto [ResponseEntity<String>] que é devolvido ao método de chamada, ou seja, a cadeia JSON enviada pelo servidor;
  • Linhas 9–21: Precisamos de construir a solicitação [RequestEntity]. Ela difere dependendo se usamos uma solicitação GET ou POST;
  • linha 9: a solicitação para um GET. A classe [RequestEntity] fornece métodos estáticos para criar solicitações GET, POST, HEAD e outras. O método [RequestEntity.get] permite criar uma solicitação GET encadeando os vários métodos que a constroem:
    • O método [RequestEntity.get] recebe a URL de destino como parâmetro na forma de uma instância URI;
    • o método [accept] permite definir os elementos do cabeçalho HTTP [Accept]. Aqui, especificamos que aceitamos o tipo [application/json] que o servidor irá enviar;
    • o resultado deste encadeamento de métodos é um tipo [HeadersBuilder];
  • linhas 10–12: se o parâmetro [User user] não for nulo, incluímos o cabeçalho HTTP [Authorization] na solicitação;
  • linha 13: o método [HeadersBuilder.build] utiliza esta informação para construir o tipo [RequestEntity] da solicitação;
  • linha 15: a solicitação é um POST. O método [RequestEntity.post] permite criar uma solicitação POST encadeando os vários métodos que a constroem:
    • o método [RequestEntity.post] recebe a URL de destino como parâmetro na forma de uma instância URI,
    • o método [header] permite definir os cabeçalhos HTTP que deseja utilizar, neste caso o cabeçalho de autorização,
    • o método [header] seguinte inclui o cabeçalho [Content-Type: application/json] na solicitação para indicar que os dados enviados chegarão como uma string JSON;
    • o método [accept] indica que aceitamos o tipo [application/json] que o servidor irá enviar;
  • linhas 17–19: se o parâmetro [User user] não for nulo, o cabeçalho HTTP [Authorization] é incluído na solicitação;
  • linha 20: o método [BodyBuilder.body] define o valor enviado. Este é o segundo parâmetro do método genérico [getResponse] (linha 2);
  • linhas 25–28: se ocorrer algum erro, é lançada uma [RdvMedecinsException];

O método [getMessagesForException] nas linhas 26 e 28 é o seguinte:


    // list of exception error messages
    protected static List<String> getMessagesForException(Exception exception) {
        // retrieve the list of exception error messages
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            // the message is retrieved only if it is !=null and not blank
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return erreurs;
}

O método privado [getBase64] devolve a codificação Base64 da cadeia «login:passwd» para o cabeçalho de autenticação HTTP:


    private String getBase64(User user) {
        // encodes user and password in base 64 - requires java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}

A classe [Dao] estende a classe [AbstractDao] da seguinte forma:


package rdvmedecins.client.dao;
 
import java.io.IOException;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
 
@Service
public class Dao extends AbstractDao implements IDao {
 
    // mappers jSON
    @Autowired
    ObjectMapper jsonMapper;
 
    @Autowired
    private ObjectMapper jsonMapperShortCreneau;
 
    @Autowired
    private ObjectMapper jsonMapperLongRv;
 
    @Autowired
    private ObjectMapper jsonMapperShortRv;
 
    public List<Client> getAllClients(User user) {
        ...
    }
 
    public List<Medecin> getAllMedecins(User user) {
...
    }
...
}
  • linha 22: a classe [Dao] é um componente Spring. A anotação [@Service] foi utilizada aqui. Poderíamos ter continuado a utilizar a anotação [@Component] utilizada até este ponto;
  • linhas 26–36: injeção dos quatro mapeadores JSON definidos na classe de configuração [DaoConfig];

Os métodos da classe [Dao] seguem todos o mesmo padrão. Iremos detalhar uma operação GET e uma operação POST.

Primeiro, um pedido [GET]:


public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
        // the answer
        Response<AgendaMedecinJour> response;
        // the diary
        String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
        try {
            // diary AgendaMedecinJour
            response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
            });
        } catch (IOException e) {
            throw new RdvMedecinsException(401, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(402, getMessagesForException(e));
        }
        // response analysis
        int status = response.getStatus();
        if (status != 0) {
            throw new RdvMedecinsException(status, response.getMessages());
        } else {
            return response.getBody();
        }
}
  • Linha 5: O método genérico [getResponse] é chamado. Os parâmetros reais utilizados são os seguintes:
    • 1: o utilizador;
    • 2: o URL de destino;
    • 3: o valor a ser enviado. Neste caso, não há nenhum;
  • linha 5: a chamada não foi envolvida num bloco try/catch. O método [getResponse] pode lançar uma [RdvMedecinsException]. Se lançada, esta exceção propagar-se-á para o método que chamou o método [getAgendaMedecinJour] acima;
  • linha 8: a URL [/getAgendaMedecinJour] retorna um [Response<AgendaMedecinJour>] que foi serializado para JSON no lado do servidor pelo mapeador JSON [jsonMapperLongRv]. Usamos este mesmo mapeador para desserializar a string JSON recebida;
  • linhas 10–13: se ocorrer um erro na linha 9, é lançada uma [RdvMedecinsException];
  • linhas 16–21: a resposta enviada pelo servidor é analisada;
  • linhas 17–18: se o servidor reportou um erro, é lançada uma exceção com as informações fornecidas pelo servidor;
  • linhas 19–21: caso contrário, devolve a agenda do médico;

O pedido POST em análise será o seguinte:


    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
        // the answer
        Response<Rv> response;
        try {
            // the Rv
            String jsonResponse = getResponse(user, "/ajouterRv",
                    jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
            // the Rv Rv
            response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
            });
        } catch (RdvMedecinsException e) {
            throw e;
        } catch (IOException e) {
            throw new RdvMedecinsException(381, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(382, getMessagesForException(e));
        }
        // response analysis
        int status = response.getStatus();
        if (status != 0) {
            throw new RdvMedecinsException(status, response.getMessages());
        } else {
            return response.getBody();
        }
}
  • Linha 6: O método [getResponse] é chamado com os seguintes parâmetros:
    • 1: o utilizador;
    • 2: o URL de destino,
    • 3: o valor enviado: passamos o valor JSON do tipo [PostAjouter] construído com as informações recebidas como parâmetros pelo método. Utilizamos um mapeador JSON sem filtros;
  • linha 9: no lado do servidor, o mapeador JSON [jsonMapperLongRv] serializou a resposta do servidor. No lado do cliente, usamos este mesmo mapeador para a deserializar;
  • linha 6: a URL [/ajouterRv] retorna o valor JSON do tipo [Response<Rv>];
  • linhas 4–11: aqui, o método [getResponse] foi colocado num bloco try/catch porque a serialização do valor enviado pode lançar uma exceção. É provável que o método [getResponse] lance uma [RdvMedecinsException]. Neste caso, simplesmente repetimos a tentativa (linhas 11–12);

O código seguinte (linhas 13–24) é semelhante ao que acabámos de discutir. A única diferença em relação a uma operação GET é, portanto, o segundo parâmetro do método [getResponse], que deve ser a representação JSON do valor a ser enviado.

Os outros métodos seguem o mesmo modelo.

8.5.11. Exceção

Ao executar vários testes, deparámo-nos com uma anomalia resumida na seguinte classe [Anomalie]:


package rdvmedecins.clients.console;
 
import java.io.IOException;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Anomalie {
 
    // serializer jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // connection timeout in milliseconds
    static private int TIMEOUT = 1000;
 
    public static void main(String[] args) throws IOException {
        // we retrieve a reference on the [DAO] layer
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // set the URL of the web/json service
        dao.setUrlServiceWebJson("http://localhost:8080");
        // set timeouts in milliseconds
        dao.setTimeout(TIMEOUT);
 
        // Authentication
        String message = "/authenticate [admin,admin]";
        try {
            dao.authenticate(new User("admin", "admin"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // Authentication
        message = "/authenticate [admin,x]";
        try {
            dao.authenticate(new User("admin", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // Authentication
        message = "/authenticate [user,user]";
        try {
            dao.authenticate(new User("user", "user"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // closing context
        context.close();
    }
 
    private static void showException(String message, RdvMedecinsException e) {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
        for (String msg : e.getMessages()) {
            System.out.println(msg);
        }
    }
}
  • linhas 31–38: o utilizador [admin, admin] é autenticado;
  • linhas 40-47: autenticar o utilizador [admin, x], cuja palavra-passe está incorreta;
  • linhas 49-56: o utilizador [user, user] é autenticado; este utilizador existe, mas não está autorizado;

Eis os resultados:

1
2
3
4
5
/authenticate [admin,admin] : OK
/authenticate [admin,x] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
  • linha 2: contrariamente ao esperado, o utilizador [admin, x] foi aceite;

Se comentarmos as linhas 33–38 do código, obtemos o seguinte resultado:

1
2
3
4
5
6
URL [/authenticate [admin,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden

o que é o resultado esperado. Parece que, assim que o utilizador [admin, admin] inicia sessão com sucesso pela primeira vez, a sua palavra-passe deixa de ser necessária para inícios de sessão subsequentes. É efetivamente esse o caso. Por predefinição, o Spring Security utiliza um mecanismo de sessão que garante que, uma vez que um utilizador tenha sido autenticado, não precisa de o fazer novamente em pedidos subsequentes. Pode modificar a configuração do [Spring Security] no servidor web / JSON para que isso deixe de acontecer:

  

O ficheiro [SecurityConfig] deve ser modificado da seguinte forma:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
  • A linha 5 especifica que não deve haver nenhuma sessão de segurança;

Isto resolveu o problema.

8.6. Renderização do lado do servidor com Spring / Thymeleaf

8.6.1. Introdução

Voltemos à arquitetura da aplicação cliente/servidor a ser construída:

  • o servidor web/JSON [Web2] já foi construído;
  • a camada [DAO] do cliente [Web1] foi construída;

A relação entre o servidor [Web1] e os navegadores dos clientes é uma relação cliente/servidor, em que o servidor é um servidor web/JSON. Na verdade, o [Web1] irá fornecer fluxos HTML encapsulados numa cadeia de caracteres JSON. A arquitetura cliente/servidor é a seguinte:

  • temos uma arquitetura cliente [2] / servidor [1] em que o cliente e o servidor comunicam através de JSON;
  • Em [1], a camada web Spring MVC/Thymeleaf fornece vistas, fragmentos de vista e dados em JSON. O servidor é, portanto, um servidor web/JSON semelhante ao servidor [Web1]. É também sem estado;
  • em [2]: o código JavaScript incorporado na vista carregada quando a aplicação é iniciada está estruturado em camadas:
    • A camada [de apresentação] lida com as interações do utilizador,
    • a camada [DAO] lida com o acesso aos dados através do servidor [Web2];
  • o cliente [2] armazenará em cache determinadas vistas para reduzir a carga no servidor;

Iremos construir o servidor web/JSON [Web1], implementado com Spring MVC/Thymeleaf, em várias etapas:

  • explorando o framework CSS Bootstrap;
  • escrevendo as vistas;
  • escrevendo o controlador;

Em seguida, separadamente, iremos construir o cliente JS para o servidor [Web1]. Para demonstrar claramente que este cliente tem um certo grau de independência em relação ao servidor [Web1], iremos construí-lo utilizando a ferramenta [WebStorm] em vez do STS.

Daqui em diante, alguns detalhes serão omitidos, pois podem desviar a nossa atenção do tema principal, que é a organização do código. Os leitores interessados podem encontrar o código completo no site deste documento.

8.6.2. O projeto STS

  • em [1], o código Java;
  • em [2], as vistas;

A configuração do Maven em [pom.xml] é a 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>istia.st.rdvmedecins</groupId>
    <artifactId>rdvmedecins-springthymeleaf-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rdvmedecins-springthymeleaf-server</name>
    <description>Gestion de RV Médecins</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>istia.st.rdvmedecins</groupId>
            <artifactId>rdvmedecins-webjson-client-console</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <properties>
        <start-class>rdvmedecins.springthymeleaf.server.boot.Boot</start-class>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.7</java.version>
    </properties>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    ...
</project>
  • linhas 16–19: o projeto é um projeto Thymeleaf;
  • linhas 20–24: que depende da camada [DAO] que acabámos de construir;

A configuração Java é gerida por dois ficheiros:

 

A camada [web] é configurada pelo seguinte ficheiro [WebConfig]:


package rdvmedecins.springthymeleaf.server.config;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
 
@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {
 
    // ----------------- layer configuration [web]
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
 
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }
 
    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }
 
    // dispatcherservlet configuration for CORS headers
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
 
}

Já nos deparámos com todos os elementos desta configuração em algum momento. Apenas um lembrete de que as linhas 42–47 são necessárias quando se pretende poder consultar o servidor com pedidos de origem cruzada (CORS). Esse será o caso aqui.

A classe [AppConfig] configura toda a aplicação:


package rdvmedecins.springthymeleaf.server.config;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
 
import rdvmedecins.client.config.DaoConfig;
 
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
 
    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // root web service / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout in milliseconds
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;
 
    ...
 
}
  • Linha 11: [AppConfig] importa a configuração para a camada [DAO] e a camada [web];
  • linhas 15-16: as credenciais que permitirão à aplicação aceder ao processo de inicialização da aplicação, a fim de armazenar em cache médicos e clientes;
  • linha 18: o URL do serviço web [Web1] / JSON;
  • linha 20: o tempo de espera para as chamadas HTTP da aplicação;
  • linha 22: um valor booleano para ativar ou desativar chamadas entre domínios;

Por fim, em [application.properties], o servidor Tomcat está configurado para funcionar na porta 8081:

  

server.port=8081

8.6.3. Funcionalidades da aplicação

Estas foram descritas na Secção 8.2. Vamos agora revê-las. Utilizando um navegador, solicitamos o URL [http://localhost:8081/boot.html]:

  • em [1], a página de início de sessão da aplicação;
  • em [2] e [3], o nome de utilizador e a palavra-passe do utilizador que pretende utilizar a aplicação. Existem dois utilizadores: admin/admin (nome de utilizador/palavra-passe) com a função (ADMIN) e user/user com a função (USER). Apenas a função ADMIN tem permissão para utilizar a aplicação. A função USER é incluída exclusivamente para demonstrar a resposta do servidor neste caso de utilização;
  • em [4], o botão que permite ligar-se ao servidor;
  • em [5], o idioma da aplicação. Existem dois: francês (padrão) e inglês;
  • em [6], o URL do servidor [rdvmedecins-springthymeleaf-server];
  • em [1], faz o login;
  • depois de iniciar sessão, pode escolher o médico que deseja consultar [2] e a data da consulta [3]. Assim que o médico e a data forem selecionados, o calendário é apresentado automaticamente:
  • assim que o calendário do médico for exibido, pode reservar um horário [5];
  • Em [6], selecione o paciente para a consulta e confirme a sua seleção em [7];

Assim que a consulta for confirmada, será automaticamente redirecionado para o calendário, onde a nova consulta já estará listada. Esta consulta pode ser eliminada posteriormente [8].

As principais funcionalidades já foram descritas. São simples. Vamos terminar com as definições de idioma:

  • em [1], muda-se do francês para o inglês;
  • Em [2], a visualização muda para inglês, incluindo o calendário;

8.6.4. Passo 1: Introdução ao framework CSS Bootstrap

No cliente web acima, as páginas HTML utilizarão o framework CSS Bootstrap [http://getbootstrap.com/], que iremos agora apresentar.

8.6.4.1. O projeto de exemplo

O projeto de exemplo será o seguinte:

  • em [1]: o projeto na sua totalidade;
  • em [2]: o código Java;
  • em [3]: os scripts JavaScript;
  • em [4]: as bibliotecas JavaScript;
  • em [5]: as vistas Thymeleaf;
  • em [6]: folhas de estilo;

8.6.4.1.1. Configuração do Maven

O ficheiro [pom.xml] destina-se a um projeto Thymeleaf Maven:


<?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>istia.st</groupId>
    <artifactId>rdvmedecins-webjson-client-bootstrap</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>rdvmedecins-webjson-client-bootstrap</name>
    <description>Démos Bootstrap</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
        <java.version>1.7</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>

8.6.4.1.2. Configuração Java
  

A classe [BootstrapDemo] configura a aplicação Spring/Thymeleaf:


package istia.st.rdvmedecins;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
 
@EnableAutoConfiguration
@ComponentScan({ "istia.st.rdvmedecins" })
public class BootstrapDemo extends WebMvcConfigurerAdapter {
 
    public static void main(String[] args) {
        SpringApplication.run(BootstrapDemo.class, args);
    }
 
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }
}

Já nos deparámos com este tipo de código.

8.6.4.1.3. O controlador Spring
  

O [BootstrapController] é o seguinte:


package istia.st.rdvmedecins;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
public class BootstrapController {
 
    @RequestMapping(value = "/bs-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bso1() {
        return "bs-01";
    }
 
    @RequestMapping(value = "/bs-02", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs02() {
        return "bs-02";
    }
 
    @RequestMapping(value = "/bs-03", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs03() {
        return "bs-03";
    }
 
    @RequestMapping(value = "/bs-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs04() {
        return "bs-04";
    }
 
    @RequestMapping(value = "/bs-05", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs05() {
        return "bs-05";
    }
 
    @RequestMapping(value = "/bs-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs06() {
        return "bs-06";
    }
 
    @RequestMapping(value = "/bs-07", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs07() {
        return "bs-07";
    }
 
    @RequestMapping(value = "/bs-08", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs08() {
        return "bs-08";
    }
}

As ações existem apenas para apresentar as visualizações processadas pelo Thymeleaf.

8.6.4.1.4. O ficheiro [application.properties]

O ficheiro [application.properties] configura o servidor Tomcat incorporado:


server.port=8082

8.6.4.2. Exemplo #1: o jumbotron

A ação [/bs-01] exibe a seguinte visualização [bs-01.xml]:

A visualização [bs-01.xml] é a seguinte:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
    </head>
    <body id="body">
        <div class="container">
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- error -->
            <div id="erreur" class="alert alert-danger">
                <span>Ici, un texte d'erreur</span>
            </div>
        </div>
    </body>
</html>
  • linha 7: o ficheiro CSS do framework Bootstrap;
  • linha 8: um ficheiro CSS local;
  • linha 13: displays [1];
  • linhas 19–21: exibe [2];
  • linha 11: a classe CSS [container] define uma área de exibição no navegador;
  • linha 19: a classe CSS [alert] exibe uma área colorida. A classe [alert-danger] utiliza uma cor predefinida. Existem várias destas [alert-info, alert-warning,...];

O jumbotron [1] é gerado pela seguinte vista [jumbotron.xml]:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <!-- Bootstrap Jumbotron -->
    <div class="jumbotron">
        <div class="row">
            <div class="col-md-2">
                <img src="resources/images/caduceus.jpg" alt="RvMedecins" />
            </div>
            <div class="col-md-10">
                <h1>
                    Les Médecins
                    <br />
                    associés
                </h1>
            </div>
        </div>
    </div>
</section>
  • linha 4: a área tem a classe CSS [jumbotron];
  • linha 5: a classe [row] define uma linha com 12 colunas;
  • linha 6: a classe [col-md-2] define uma área de duas colunas dentro da linha;
  • linha 7: uma imagem é colocada nessas duas colunas;
  • linhas 9–15: o texto é colocado nas restantes 10 colunas;

8.6.4.3. Exemplo #2: A barra de navegação

A ação [/bs-02] exibe a seguinte visualização [bs-02.xml]:

A nova funcionalidade é a barra de navegação [1] com o seu formulário de entrada e botões:

A visualização [bs-02.xml] é a seguinte:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- scripts JS -->
        <script src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/js/bs-02.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar1"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 10: importamos o jQuery;
  • linha 11: um script JS local;
  • linha 16: a barra de navegação;

A barra de navegação é gerada pela seguinte vista [navbar1.xml]:


<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="navbar-collapse collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- identification form -->
                <div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
                    <div class="form-group">
                        <input type="text" placeholder="Utilisateur" class="form-control" />
                    </div>
                    <div class="form-group">
                        <input type="password" placeholder="Mot de passe" class="form-control" />
                    </div>
                    <button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
                </div>
            </div>
        </div>
    </div>
</section>
  • linha 3: a classe [navbar] define o estilo da barra de navegação. A classe [navbar-inverse] atribui-lhe um fundo preto. A classe [navbar-fixed-top] garante que, quando se percorre a página apresentada pelo navegador, a barra de navegação permanece na parte superior do ecrã;
  • Linhas 5–13: definem a área [1]. Trata-se normalmente de uma série de classes que não compreendo. Utilizo o componente tal como está;
  • linhas 14–26: definem uma área «responsiva» da barra de navegação. Num smartphone, esta área recolhe-se numa área de menu;
  • linha 15: uma imagem que está atualmente oculta;
  • linhas 17–25: a classe [navbar-form] define o estilo de um formulário na barra de navegação. A classe [navbar-right] posiciona-o à direita da barra de navegação;
  • linhas 21–23: os dois campos de entrada do formulário na linha 17 [2]. Estão dentro de uma classe [form-group] que envolve os elementos de um formulário, e cada um deles tem a classe [form-control];
  • linha 24: a classe [btn], que define um botão, complementada pela classe [btn-success], que lhe confere a cor verde;
  • linha 24: quando o botão [Login] é clicado, a seguinte função JS é executada:

function connecter() {
    showInfo("Connexion demandée...");
}
 
function showInfo(message) {
    $("#info").text(message);
}

Aqui está um exemplo:

Image

8.6.4.4. Exemplo n.º 3: O botão de lista

A ação [/bs-03] apresenta a seguinte visualização [bs-03.xml]:

  • A nova funcionalidade é o botão de lista [1], também conhecido como «menu suspenso»;

O código para a visualização [bs-03.xml] é o seguinte:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script src="resources/vendor/bootstrap.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-03.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar2"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 11: o botão suspenso requer o ficheiro JS do Bootstrap;
  • linha 18: a nova barra de navegação;

A visualização [navbar2.xml] é a seguinte:


<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="navbar-collapse collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- identification form -->
                <div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
                    <div class="form-group">
                        <input type="text" placeholder="Utilisateur" class="form-control" />
                    </div>
                    <div class="form-group">
                        <input type="password" placeholder="Mot de passe" class="form-control" />
                    </div>
                    <button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
                    <!-- languages -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger">Langues</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')">Français</a>
                            </li>
                            <li>
                                <a href="javascript:setLang('en')">English</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initNavBar2();
        /*]]>*/
    </script>
</section>
  • linhas 25–40: definem o botão suspenso;
  • linha 27: a classe [btn-danger] atribui-lhe a cor vermelha;
  • linhas 32–39: os itens da lista. Cada um é um link associado a uma função JavaScript;
  • linhas 46–51: um script JavaScript executado após o carregamento do documento;

O script JS [bs-03.js] é o seguinte:


function initNavBar2() {
    // dropdown des langues
    $('.dropdown-toggle').dropdown();
}
 
function connecter() {
    showInfo("Connexion demandée...");
}
 
function setLang(lang) {
    var msg;
    switch (lang) {
    case 'fr':
        msg = "Vous avez choisi la langue française...";
        break;
    case 'en':
        msg = "You have selected english language...";
        break;
    }
    showInfo(msg);
}
 
function showInfo(message) {
    $("#info").text(message);
}
  • Linhas 1-4: A função que inicializa o [dropdown]. [$('.dropdown-toggle')] localiza o elemento com a classe [dropdown-toggle]. Este é o botão suspenso (linha 28 da vista). A função JS [dropdown()] — definida no ficheiro JS [bootstrap.js] — é aplicada a ele. Só após esta operação é que o botão se comporta como um botão de menu suspenso;
  • linhas 10–21: a função executada quando um idioma é selecionado;

Eis um exemplo:

Image

8.6.4.5. Exemplo n.º 4: um menu

A ação [/bs-04] apresenta a seguinte vista [bs-04.xml]:

Foi adicionado um menu [1].

A vista [bs-04.xml] é a seguinte:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script src="resources/vendor/bootstrap.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-04.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 18: inserir uma nova barra de navegação;

A visualização do [navbar3.xml] é a seguinte:


<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="collapse navbar-collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <ul class="nav navbar-nav">
                    <li class="active" id="lnkAfficherAgenda">
                        <a href="javascript:afficherAgenda()">Agenda </a>
                    </li>
                    <li class="active" id="lnkAccueil">
                        <a href="javascript:retourAccueil()">Retour Accueil </a>
                    </li>
                    <li class="active" id="lnkRetourAgenda">
                        <a href="javascript:retourAgenda()">Retour Agenda </a>
                    </li>
                    <li class="active" id="lnkValiderRv">
                        <a href="javascript:validerRv()">Valider </a>
                    </li>
                </ul>
                <!-- right-hand buttons -->
                <div class="navbar-form navbar-right" role="form">
                    <!-- disconnect -->
                    <button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
                    <!-- languages -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger">Langues</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')">Français</a>
                            </li>
                            <li>
                                <a href="javascript:setLang('en')">English</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initNavBar3();
        /*]]>*/
    </script>
</section>
  • linhas 16–29: criam o menu com quatro opções, cada uma ligada a um script JS;
  • linhas 55-60: um script executado quando a página é carregada;

O script JS [bs-04.js] é o seguinte:


...
function initNavBar3() {
    // dropdown des langues
    $('.dropdown-toggle').dropdown();
    // l'moving image
    loading = $("#loading");
    loading.hide();
}
 
function afficherAgenda() {
    showInfo("option [Agenda] cliquée...");
}
 
function retourAccueil() {
    showInfo("option [Retour accueil] cliquée...");
}
 
function retourAgenda() {
    showInfo("option [Retour agenda] cliquée...");
}
 
function validerRv() {
    showInfo("option [Valider] cliquée...");
}
 
function setMenu(show) {
    // les liens du menu
    var lnkAfficherAgenda = $("#lnkAfficherAgenda");
    var lnkAccueil = $("#lnkAccueil");
    var lnkValiderRv = $("#lnkValiderRv");
    var lnkRetourAgenda = $("#lnkRetourAgenda");
    // on les met dans un dictionnaire
    var options = {
        "lnkAccueil" : lnkAccueil,
        "lnkAfficherAgenda" : lnkAfficherAgenda,
        "lnkValiderRv" : lnkValiderRv,
        "lnkRetourAgenda" : lnkRetourAgenda
    }
    // on cache tous les liens
    for ( var key in options) {
        options[key].hide();
    }
    // on affiche ceux qui sont demandés
    for (var i = 0; i < show.length; i++) {
        var option = show[i];
        options[option].show();
    }
}
  • linhas 2–18: a função de inicialização da página;
  • linha 4: para exibir o botão de seleção de idioma;
  • linhas 6-7: a imagem animada é ocultada;
  • linhas 26–48: uma função [setMenu] que permite especificar quais as opções que devem ficar visíveis;

Vamos para a consola do programador (Ctrl-Shift-I) e introduzimos o seguinte código [1]:

Em seguida, volte ao navegador. O menu mudou [2]:

8.6.4.6. Exemplo n.º 5: Uma lista suspensa

A ação [/bs-05] apresenta a seguinte vista [bs-05.xml]:

A nova funcionalidade encontra-se em [1]. Aqui estamos a utilizar um componente fornecido fora do Bootstrap, [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].

O código para a vista [bs-05.xml] é o seguinte:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-05.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content" th:include="choixmedecin">
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 8: o CSS necessário para a lista suspensa;
  • linha 13: o ficheiro JS necessário para a lista suspensa;
  • linha 24: a lista suspensa;

A visualização [choixmedecin.xml] é a seguinte:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info">Veuillez choisir un médecin</div>
    <div class="row">
        <div class="col-md-3">
            <h2>Médecin</h2>
            <select id="idMedecin" class="combobox" data-style="btn-primary">
                <option value="1">Mme Marie Pélissier</option>
                <option value="2">Mr Jean Pardon</option>
                <option value="3">Mlle Jeanne Jirou</option>
                <option value="4">Mr Paul Macou</option>
            </select>
        </div>
    </div>
    <!-- local script -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initChoixMedecin();
        /*]]>*/
    </script>
</section>
  • linhas 7–12: Trata-se de um elemento [select] padrão, mas com uma classe específica [combobox]. O atributo [data-style="btn-primary"] atribui ao componente a sua cor azul;
  • linhas 16–21: um script executado quando a página é carregada;

O ficheiro JS [bs-05.js] é o seguinte:


...
function afficherAgenda() {
    var idMedecin = $('#idMedecin option:selected').val();
    showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
 
function initChoixMedecin() {
    // le select des médecins
    $('#idMedecin').selectpicker();
    // le menu
    setMenu([ "lnkAfficherAgenda" ]);
}
  • linhas 7–12: a função executada quando a página é carregada;
  • linha 9: a instrução que transforma o [select] da página numa lista suspensa Bootstrap. [$('#idMedecin')] faz referência ao [select] (linha 7 da vista [choixmedecin]) e a função JS [selectpicker] provém do ficheiro JS [bootstrap-select.js];
  • linha 11: apenas uma das opções do menu é exibida;
  • linhas 2–5: a função JavaScript executada quando a opção de menu [Agenda] é clicada;
  • linha 3: recuperamos o valor da opção selecionada na lista suspensa: [$('#idMedecin option:selected')] primeiro localiza o componente [id=idMedecin] e, em seguida, dentro desse componente, a opção selecionada. A operação [..].val() recupera então o valor do elemento encontrado, ou seja, o atributo [value] da opção selecionada;

Aqui está um exemplo de seleção de um médico:

 

8.6.4.7. Exemplo n.º 6: Um calendário

A ação [/bs-06] apresenta a seguinte visualização [bs-06.xml]:

Image

A seleção de um médico ou de uma data aciona uma função JS que exibe tanto o médico selecionado como a data selecionada. Aqui está um exemplo:

 

Usando o botão da lista de idiomas, pode mudar o calendário (e apenas o calendário) para inglês:

Image

Este é o exemplo mais complexo da série. O calendário é um componente [bootstrap-datepicker] [http://eternicode.github.io/bootstrap-datepicker].

A vista [bs-06.xml] é a seguinte:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-06.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content" th:include="choixmedecinjour">
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 8: o ficheiro CSS para o componente [bootstrap-datepicker];
  • linha 16: o ficheiro JS para o componente [bootstrap-datepicker];
  • linha 17: o ficheiro JS para gerir um calendário francês. Por predefinição, está em inglês;
  • linha 15: o ficheiro JS para uma biblioteca chamada [moment] que fornece acesso a inúmeras funções de cálculo de tempo [http://momentjs.com/];
  • linha 28: a visualização do calendário;

A vista [choixmedecinjour.xml] é a seguinte:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info">Veuillez choisir un médecin et une date</div>
    <div class="row">
        <div class="col-md-3">
            <h2>Médecin</h2>
            <select id="idMedecin" class="combobox" data-style="btn-primary">
                <option value="1">Mme Marie Pélissier</option>
                <option value="2">Mr Jean Pardon</option>
                <option value="3">Mlle Jeanne Jirou</option>
                <option value="4">Mr Paul Macou</option>
            </select>
        </div>
        <div class="col-md-3">
            <h2>Date</h2>
            <section id="calendar_container">
                <div id="calendar" class="input-group date">
                    <input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
                        <span class="input-group-addon">
                            <i class="glyphicon glyphicon-th"></i>
                        </span>
                    </input>
                </div>
            </section>
        </div>
    </div>
    <!-- local script -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initChoixMedecinJour();
        /*]]>*/
    </script>
</section>
  • linhas 17-23: o calendário;
  • linha 18: a classe [btn-primary] atribui-lhe a cor azul;
  • linha 18: o atributo [disabled="true"] impede a introdução manual da data. Deve utilizar o calendário;
  • linha 16: o calendário foi colocado numa secção [id="calendar_container"]. Para alterar o idioma do calendário, deve eliminá-lo e, em seguida, regenerá-lo. Assim, elimine o conteúdo do componente [id="calendar_container"] e, em seguida, coloque lá o novo calendário com o novo idioma;
  • Linhas 28–33: o código de inicialização da página;

O ficheiro JS [bs-06.js] é o seguinte:


...
var calendar_infos = {};
 
function initChoixMedecinJour() {
    // calendrier
    var calendar_container = $("#calendar_container");
    calendar_infos = {
        "container" : calendar_container,
        "html" : calendar_container.html(),
        "today" : moment().format('YYYY-MM-DD'),
        "langue" : "fr"
    }
    // création calendrier
    updateCalendar();
    // le select des médecins
    $('#idMedecin').selectpicker();
    $('#idMedecin').change(function(e) {
        afficherAgenda();
    })
    // le menu
    setMenu([]);
}
  • Linha 2: O calendário é gerido por várias funções JS. A variável [calendar_infos] irá recolher informações sobre o calendário. É global para que possa ser acedida pelas várias funções;
  • linha 6: identificamos o contentor do calendário;
  • linhas 7–12: as informações armazenadas para o calendário;
    • linha 8: uma referência ao seu contêiner,
    • linha 9: o código HTML do calendário. Com estas duas informações, podemos remover o calendário e regenerá-lo;
    • linha 10: a data de hoje no formato [aaaa-mm-dd],
    • linha 11: o idioma do calendário;
  • linha 14: criação do calendário;
  • linha 16: o menu suspenso dos médicos;
  • linhas 17–19: sempre que o valor selecionado neste menu suspenso mudar, o método [displayCalendar] será executado;
  • linha 21: sem menu na barra de navegação;

A função [updateCalendar] é a seguinte:


function updateCalendar(renew) {
    if (renew) {
        // régénération du calendrier actuel
        calendar_infos.container.html(calendar_infos.html);
    }
    // initialisation du calendrier
    var calendar = $("#calendar");
    var settings = {
        format : "yyyy-mm-dd",
        startDate : calendar_infos.today,
        language : calendar_infos.langue,
    };
    calendar.datepicker(settings);
    // sélection de la date courante
    if (calendar_infos.date) {
        calendar.datepicker('setDate', calendar_infos.date)
    }
    // évts
    calendar.datepicker().on('hide', function(e) {
        // affichage jour sélectionné
        displayJour();
    });
    calendar.datepicker().on('changeDate', function(e) {
        // on note la nouvelle date
        calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
        // affichage infos agenda
        afficherAgenda();
        // affichage jour sélectionné
        displayJour();
    });
    // affichage jour sélectionné
    displayJour();
}
  • linha 1: a função [updateCalendar] aceita um parâmetro que pode ou não estar presente. Se estiver presente, o calendário é regenerado (linha 4) com base nas informações contidas em [calendar_infos];
  • linha 7: o calendário é referenciado;
  • linhas 8–12: os seus parâmetros de inicialização;
    • linha 9: o formato das datas tratadas [aaaa-mm-dd],
    • linha 10: a primeira data que pode ser selecionada no calendário. Aqui, a data de hoje. Datas anteriores a esta não podem ser selecionadas;
    • linha 11: o idioma do calendário. Haverá dois: ['en'] e ['fr'];
  • linha 13: o calendário é configurado;
  • linhas 15–17: se a data de [calendar_infos] tiver sido inicializada, então esta data é definida como a data atual do calendário;
  • linhas 19–22: sempre que o calendário for fechado, a data selecionada será exibida;
  • linhas 23–30: sempre que houver uma alteração de data no calendário:
    • linha 25: a data selecionada é registada em [calendar_infos],
    • linha 27: exibimos informações sobre o calendário,
    • linha 29: exibimos o dia selecionado;
  • linha 32: exibimos o dia selecionado, se houver;

O método [displayJour] que exibe o dia selecionado é o seguinte:


// affiche le jour sélectionné
function displayJour() {
    if (calendar_infos.date) {
        var displayjour = $("#displayjour");
        moment.locale(calendar_infos.langue);
        jour = moment(calendar_infos.date).format('LL');
        displayjour.val(jour);
    }
}
  • linha 3: se já tiver sido selecionada uma data (inicialmente, o calendário não tem nenhuma data selecionada);
  • linha 4: localizamos o componente onde iremos introduzir a data;
  • linha 5: esta data pode ser escrita em inglês ou francês. Definimos o idioma da biblioteca [moment];
  • linha 6: exibimos a data selecionada no idioma escolhido e no formato longo;
  • linha 7: esta data é apresentada;

Aqui estão dois exemplos:

Quando o médico ou a data mudam, o método [displayCalendar] é executado:


function afficherAgenda() {
    // on affiche médecin et date
    var idMedecin = $('#idMedecin option:selected').val();
    if (calendar_infos.date) {
        showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin + " et le jour " + calendar_infos.date);
    }
}

8.6.4.8. Exemplo #7: Uma tabela HTML «responsiva»

Nota: «responsivo» é um termo que indica que um componente é capaz de se adaptar ao tamanho do ecrã no qual é apresentado. Vamos mostrar um exemplo disso.

A ação [/bs-07] exibe a seguinte vista [bs-07.xml] (ecrã inteiro):

A nova funcionalidade é a tabela HTML [1]. Esta tabela é gerida pela biblioteca JS [footable]: [https://github.com/fooplugins/FooTable].

Se redimensionar a janela do navegador, obtém o seguinte:

  • a tabela HTML adaptou-se ao tamanho do ecrã;
  • em [1], para ver o link [Livro], deve clicar no sinal [+];
  • em [2], o que se vê ao clicar no sinal [+];

A visualização [bs-07.xml] é a seguinte:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
        <script type="text/javascript" src="resources/vendor/footable.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-07.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3" />
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron" />
            <!-- content -->
            <div id="content" th:include="choixmedecinjour" />
            <div id="agenda" th:include="agenda" />
            <!-- info -->
            <div class="alert alert-success">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 10: o CSS para a biblioteca [footable];
  • linha 19: o JavaScript da biblioteca [footable];
  • linha 31: a tabela HTML para um calendário;

A visualização [agenda.xml] é a seguinte:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <div class="row alert alert-danger">
            <div class="col-md-6">
                <table id="creneaux" class="table">
                    <thead>
                        <tr>
                            <th data-toggle="true">
                                <span>Créneau horaire</span>
                            </th>
                            <th>
                                <span>Client</span>
                            </th>
                            <th data-hide="phone">
                                <span>Action</span>
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>
                                <span class='status-metro status-active'>
                                    9h00-9h20
                                </span>
                            </td>
                            <td>
                                <span></span>
                            </td>
                            <td>
                                <a href="javascript:reserver(14)" class="status-metro status-active">
                                    Réserver
                                </a>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                <span class='status-metro status-suspended'>
                                    9h20-9h40
                                </span>
                            </td>
                            <td>
                                <span>Mme Paule MARTIN</span>
                            </td>
                            <td>
                                <a href="javascript:supprimer(17)" class="status-metro status-suspended">
                                    Supprimer
                                </a>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
        <!-- init page -->
        <script th:inline="javascript">
            /*<![CDATA[*/
            // on initialise la page
            initAgenda();
        /*]]>*/
        </script>
    </body>
</html>
  • linha 4: coloca a tabela numa linha [row] e numa caixa colorida [alert alert-danger];
  • linha 5: a tabela ocupará 6 colunas [col-md-6];
  • linha 6: a tabela HTML é formatada pelo Bootstrap [class='table'];
  • linha 9: o atributo [data-toggle] especifica a coluna que contém o símbolo [+/-] que expande/colapsa a linha;
  • linha 15: o atributo [data-hide='phone'] especifica que a coluna deve ser ocultada se o ecrã tiver o tamanho de um ecrã de telemóvel. O valor 'tablet' também pode ser utilizado;
  • linha 31: uma função JS está associada ao link [Book];
  • linha 46: uma função JS está associada ao link [Delete];
  • linhas 56–61: inicialização da página;

Várias das classes CSS utilizadas acima provêm do ficheiro CSS [bootstrapDemo.css]:


@CHARSET "UTF-8";
 
#creneaux th {
    text-align: center;
}
 
#creneaux td {
    text-align: center;
    font-weight: bold;
}
 
.status-metro {
  display: inline-block;
  padding: 2px 5px;
  color:#fff;
}
 
.status-metro.status-active {
  background: #43c83c;
}
 
.status-metro.status-suspended {
  background: #fa3031;
}

Os estilos [status-*] provêm de um exemplo de utilização da tabela [footable] disponível no site da biblioteca.

No ficheiro JS [bs-07.js], a página é inicializada da seguinte forma:


function initAgenda() {
    // time slot table
    $("#creneaux").footable();
}

É isso. [$("#creneaux")] refere-se à tabela HTML que queremos tornar responsiva. Além disso, eis as funções JS associadas aos dois links [Reservar] e [Eliminar]:


function reserver(idCreneau) {
    showInfo("Réservation du créneau n° " + idCreneau);
}
 
function supprimer(idRv) {
    showInfo("Suppression du rv n° " + idRv);
}

8.6.4.9. Exemplo n.º 8: Uma caixa modal

A ação [/bs-08] exibe a seguinte vista [bs-08.xml]:

 

Image

Enquanto anteriormente, clicar no link [Reservar] exibia informações na caixa de informações, aqui iremos exibir uma caixa modal para selecionar um cliente para a marcação:

Image

O componente utilizado é o componente [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/].

A vista [bs-08.xml] é a seguinte:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-modal.js"></script>
        <script type="text/javascript" src="resources/vendor/footable.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-08.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3" />
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron" />
            <!-- content -->
            <div id="content" th:include="choixmedecinjour" />
            <div id="agenda" th:include="agenda-modal" />
            <div th:include="resa" />
            <!-- info -->
            <div class="alert alert-success">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 19: o ficheiro JS necessário para as caixas modais;
  • linha 32: a vista [agenda-modal] é idêntica à vista [agenda], exceto por um detalhe: a função JS que lida com o link [Reservar]:

<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>

A função [showDialogResa] é responsável por exibir a caixa modal para selecionar um cliente;

  • linha 33: a vista [resa.xml] é a caixa modal para selecionar um cliente:

<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div id="resa" class="modal fade">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">
                        </span>
                    </button>
                    <!-- <h4 class="modal-title">Modal title</h4> -->
                </div>
                <div class="modal-body">
                    <div class="alert alert-info">
                        <h3>
                            <span>Prise de rendez-vous</span>
                        </h3>
                    </div>
                    <div class="row">
                        <div class="col-md-3">
                            <h2>Clients</h2>
                            <select id="idClient" class="combobox" data-style="btn-primary">
                                <option value="1">Mme Marguerite Planton</option>
                                <option value="2">Mr Maxime Franck</option>
                                <option value="3">Mlle Elisabeth Oron</option>
                                <option value="4">Mr Gaëtan Calot</option>
                            </select>
                        </div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()">Annuler</button>
                    <button type="button" class="btn btn-primary" onclick="javascript:validateResa()">Valider</button>
                </div>
            </div><!-- /.modal-content -->
        </div><!-- /.modal-dialog -->
    </div><!-- /.modal -->
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initResa();
        /*]]>*/
    </script>
</section>
  • linhas 3-37: a caixa modal;
  • linhas 13-30: o conteúdo desta caixa (o que será exibido);
  • linhas 31-34: os botões da caixa de diálogo;
  • linha 32: um botão [Cancelar] tratado pela função JS [cancelDialogResa];
  • linha 33: um botão [Confirmar] tratado pela função JS [validateResa];
  • linhas 39–44: o script de inicialização da caixa modal;

Isto resulta na seguinte visualização:

 

Note que a caixa modal não é apresentada por predefinição. É por isso que não é visível quando a aplicação é iniciada, apesar de o seu código HTML estar presente no documento.

O ficheiro JS [bs-08.js] é o seguinte:


var idCreneau;
var idClient;
var resa;
 
function showDialogResa(idCreneau) {
    // on mémorise l'id du créneau
    this.idCreneau = idCreneau;
    // on affiche le dialogue de réservation
    var resa = $("#resa");
    resa.modal('show');
    // log
    showInfo("Réservation du créneau n° " + idCreneau);
}
 
function cancelDialogResa() {
    // on cache la boîte de dialogue
    resa.modal('hide');
}
 
// validation résa
function validateResa() {
    // on récupère les infos
    var idClient = $('#idClient option:selected').val();
    // on cache la boîte de dialogue
    resa.modal('hide');
    // infos
    showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
 
function initResa() {
    // le select des clients
    $('#idClient').selectpicker();
    // boîte modale
    resa = $("#resa");
    resa.modal({});    
}
  • linhas 30–36: a função de inicialização da caixa modal;
  • linha 32: a caixa modal contém uma lista suspensa que precisa de ser inicializada;
  • linhas 34-35: inicialização da própria caixa modal;
  • linhas 5-13: a função JS associada ao link [Livro];
  • linha 7: o parâmetro da função é armazenado na variável global da linha 1;
  • linhas 9-10: a caixa modal é tornada visível;
  • linha 12: as informações são registadas na caixa de informações;
  • linhas 15–18: tratamento do botão [Cancel]. Simplesmente ocultamos a caixa modal (linha 17);
  • linhas 21–31: a função JS associada ao botão [Submit];
  • linha 23: recuperar o atributo [value] do cliente selecionado;
  • linha 25: ocultar a caixa de diálogo;
  • linha 27: registamos as duas informações: o número do lugar reservado e o cliente a quem se destina;

8.6.5. Passo 2: Escrever as Visualizações

Vamos agora descrever as vistas devolvidas pelo servidor [Web1], bem como os seus modelos.

  

8.6.5.1. A vista [navbar-start]

Exibe a barra de navegação na página inicial:

Image

O código para [navbar-start.xml] é o seguinte:


<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="navbar-collapse collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- identification form -->
                <div class="navbar-form navbar-right" role="form" id="formulaire">
                    <div class="form-group">
                        <input type="text" th:placeholder="#{service.url}" class="form-control" id="urlService" />
                    </div>
                    <div class="form-group">
                        <input type="text" th:placeholder="#{username}" class="form-control" id="login" />
                    </div>
                    <div class="form-group">
                        <input type="password" th:placeholder="#{password}" class="form-control" id="passwd" />
                    </div>
                    <button type="button" class="btn btn-success" th:text="#{login}" onclick="javascript:connecter()">Sign in</button>
                    <!-- languages -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger" th:text="#{langues}">Action</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
                            </li>
                            <li>
                                <a href="javascript:setLang('en')" th:text="#{langues.en}" />
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initNavBarStart();
        /*]]>*/
    </script>
</section>

Esta vista não tem modelo. Possui os seguintes manipuladores de eventos:

evento
manipulador
clique no botão de login
connect() - linha 27
clique no link [Francês]
setLang('fr') - linha 37
clique no link [Inglês]
setLang('en') - linha 40

8.6.5.2. A visualização [jumbotron]

Esta é a vista apresentada abaixo da barra de navegação [navbar-start] na página inicial:

Image

O seu código [jumbotron.xml] é o seguinte:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <!-- Bootstrap Jumbotron -->
    <div class="jumbotron">
        <div class="row">
            <div class="col-md-2">
                <img src="resources/images/caduceus.jpg" alt="RvMedecins" />
            </div>
            <div class="col-md-10">
                <h1 th:utext="#{application.header}" />
            </div>
        </div>
    </div>
</section>

A vista [jumbotron] não tem nenhum modelo nem eventos.

8.6.5.3. A vista [login]

Esta é a vista apresentada abaixo do jumbotron na página inicial:

Image

O seu código [login.xml] é o seguinte:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info" th:text="#{identification}">Identification
    </div>
</section>

A vista não tem modelo nem eventos.

8.6.5.4. A vista [navbar-run]

Esta é a barra de navegação exibida quando o login é bem-sucedido:

Image

O seu código [navbar-run.xml] é o seguinte:


<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="collapse navbar-collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- right-hand buttons -->
                <form class="navbar-form navbar-right" role="form">
                    <!-- disconnect -->
                    <button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
                    <!-- languages -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger" th:text="#{langues}">Langue</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
                            </li>
                            <li>
                                <a href="javascript:setLang('en')" th:text="#{langues.en}" />
                            </li>
                        </ul>
                    </div>
                </form>
            </div>
        </div>
    </div>
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initNavBarRun();
        /*]]>*/
    </script>
</section>

Esta vista não tem modelo. Possui os seguintes manipuladores de eventos:

evento
manipulador
clicar no botão de saída
logout() - linha 19
clique no link [Francês]
setLang('fr') - linha 29
clique no link [Inglês]
setLang('en') - linha 32

8.6.5.5. A vista [home]

Esta é a vista apresentada imediatamente abaixo da barra de navegação [navbar-run]:

Image

O seu código [home.html] é o seguinte:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info" th:text="#{choixmedecinjour.title}">Veuillez choisir un médecin et une date</div>
    <div class="row">
        <div class="col-md-3">
            <h2 th:text="#{rv.medecin}">Médecin</h2>
            <select name="idMedecin" id="idMedecin" class="combobox" data-style="btn-primary">
                <option th:each="medecinItem : ${rdvmedecins.medecinItems}" th:text="${medecinItem.texte}" th:value="${medecinItem.id}"/>
            </select>
        </div>
        <div class="col-md-3">
            <h2 th:text="#{rv.jour}">Date</h2>
            <section id="calendar_container">
                <div id="calendar" class="input-group date">
                    <input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
                        <span class="input-group-addon">
                            <i class="glyphicon glyphicon-th"></i>
                        </span>
                    </input>
                </div>
            </section>
        </div>
    </div>
    <!-- agenda -->
    <div id="agenda"></div>
    <!-- local script -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initChoixMedecinJour();
        /*]]>*/
    </script>
</html>

O seu modelo é o seguinte:

  • [rdvmedecins.medecinItems] (linha 8): a lista de médicos;

Na sua forma atual, a vista não parece ter quaisquer manipuladores de eventos. Na realidade, estes estão definidos na função [initChoixMedecinJour]. Esta função foi apresentada na secção 8.6.4.7, na página 466 e, mais especificamente, na página 469. Contém os seguintes manipuladores de eventos:

evento
manipulador
seleção de médico
getAgenda
selecionar uma data
obterAgenda

8.6.5.6. A visualização [calendário]

A vista [agenda] apresenta um dia do calendário de um médico:

Image

O seu código [agenda.xml] é o seguinte:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h3 class="alert alert-info" th:text="${agenda.titre}">Agenda de Mme Pélissier le 13/10/2014</h3>
        <h4 class="alert alert-danger" th:if="${agenda.creneaux.length}==0" th:text="#{agenda.medecinsanscreneaux}">Ce médecin n'a pas encore de créneaux
            de consultation</h4>
        <th:block th:if="${agenda.creneaux.length}!=0">
            <div class="row tab-content alert alert-warning">
                <div class="tab-pane active col-md-6">
                    <table id="creneaux" class="table">
                        <thead>
                            <tr>
                                <th data-toggle="true">
                                    <span th:text="#{agenda.creneauhoraire}">Créneau horaire</span>
                                </th>
                                <th>
                                    <span th:text="#{agenda.client}">Client</span>
                                </th>
                                <th data-hide="phone">
                                    <span th:text="#{agenda.action}">Action</span>
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr th:each="creneau,iter : ${agenda.creneaux}">
                                <td>
                                    <span th:if="${creneau.action}==1" class="status-metro status-active" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
                                    <span th:if="${creneau.action}==2" class="status-metro status-suspended" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
                                </td>
                                <td>
                                    <span th:text="${creneau.client}">Client</span>
                                </td>
                                <td>
                                    <a th:if="${creneau.action}==1" th:href="@{'javascript:reserverCreneau('+${creneau.id}+')'}" th:text="${creneau.commande}"
                                        class="status-metro status-active">Réserver
                                    </a>
                                    <a th:if="${creneau.action}==2" th:href="@{'javascript:supprimerRv('+${creneau.idRv}+')'}" th:text="${creneau.commande}"
                                        class="status-metro status-suspended">Supprimer
                                    </a>
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
            <!-- reservation -->
            <section th:include="resa" />
        </th:block>
        <!-- init page -->
        <script th:inline="javascript">
            /*<![CDATA[*/
            // on initialise la page
            initAgenda();
        /*]]>*/
        </script>
    </body>
</html>

O modelo para esta vista tem apenas um elemento:

  • [agenda] (linha 4): um modelo um tanto complexo, concebido especificamente para apresentar o calendário;

Possui os seguintes manipuladores de eventos:

event
manipulador
clique no botão [Eliminar]
deleteAppt(apptId) - linha 37
clique no link [Reservar]
reserveSlot(idSlot) - linha 34

A vista [resa] na linha 47 é a vista que é apresentada quando o utilizador clica num link [Reservar]:

Image

O seu código [resa.xml] é o seguinte:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <div id="resa" class="modal fade">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">
                            </span>
                        </button>
                        <!-- <h4 class="modal-title">Modal title</h4> -->
                    </div>
                    <div class="modal-body">
                        <div class="alert alert-info">
                            <h3>
                                <span th:text="#{resa.titre}">Prise de rendez-vous</span>
                            </h3>
                        </div>
                        <div class="row">
                            <div class="col-md-3">
                                <h2 th:text="#{resa.client}">Client</h2>
                                <select name="idClient" id="idClient" class="combobox" data-style="btn-primary">
                                    <option th:each="clientItem : ${clientItems}" th:text="${clientItem.texte}" th:value="${clientItem.id}" />
                                </select>
                            </div>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()" th:text="#{resa.annuler}">Annuler</button>
                        <button type="button" class="btn btn-primary" onclick="javascript:validerRv()" th:text="#{resa.valider}">Valider</button>
                    </div>
                </div><!-- /.modal-content -->
            </div><!-- /.modal-dialog -->
        </div><!-- /.modal -->
        <!-- init page -->
        <script th:inline="javascript">
            /*<![CDATA[*/
            // on initialise la page
            initResa();
        /*]]>*/
        </script>
    </body>
</html>

O seu modelo tem apenas um elemento:

  • [clientItems] (linha 24): a lista de clientes;

Possui os seguintes manipuladores de eventos:

evento
handler
clique no botão [Cancelar]
cancelDialogResa() - linha 30
clique no botão [Confirmar]
validerRv() - linha 31

8.6.5.7. A visualização [erros]

Esta é a vista que aparece se a ação solicitada pelo utilizador não puder ser concluída:

Image

O código [errors.xml] é o seguinte:


<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-danger">
        <h4>
            <span th:text="#{erreurs.titre}">Les erreurs suivantes se sont produites :</span>
        </h4>
        <ul>
            <li th:each="message : ${erreurs}" th:text="${message}" />
        </ul>
    </div>
</section>

O seu modelo tem apenas um elemento:

  • [errors] (linha 8): a lista de erros a apresentar;

A vista não tem nenhum manipulador de eventos.

8.6.5.8. Resumo

A tabela seguinte lista as vistas e os seus modelos:

vista
Modelo
Manipuladores de eventos
navbar-start

iniciar sessão, setLang
jumbotron


iniciar sessão


barra de navegação

sair, setLang
início
rdvmedecins.medecinItems (lista de médicos)
obterCalendário
calendário
calendário (um dia do calendário)
eliminar compromisso, reservar horário
marcar
clientItems (lista de clientes)
cancelar diálogo de marcação, confirmar compromisso
erros
erros (lista de erros)

8.6.6. Passo 3: Escrever as ações

Voltemos à arquitetura do serviço web [Web1]:

Vamos agora ver quais são os URLs expostos pelo [Web1] e a sua implementação:

8.6.6.1. As URLs expostas pelo serviço [Web1]

São as seguintes:

  • uma URL para cada uma das vistas anteriores ou uma combinação delas;
  • uma URL para adicionar um compromisso;
  • uma URL para eliminar um compromisso;

Todos eles devolvem uma resposta do tipo [Response], como se segue:


public class Reponse {
 
    // ----------------- properties
    // operation status
    private int status;
    // the navigation bar
    private String navbar;
    // the jumbotron
    private String jumbotron;
    // the body of the page
    private String content;
    // the diary
    private String agenda;
...
}
  • linha 5: um estado de resposta: 1 (OK), 2 (erro);
  • linha 7: o fluxo HTML para as vistas [navbar-start] ou [navbar-run], conforme apropriado;
  • linha 9: o feed HTML para a vista [jumbotron];
  • linha 13: o feed HTML para a vista [agenda];
  • linha 9: o feed HTML para as vistas [home], [errors] ou [login], conforme aplicável;

Os URLs expostos são os seguintes

/getNavbarStart
coloca a vista [navbar-start] em [Response.navbar]
/getNavbarRun
coloca a vista [navbar-run] em [Response.navbar]
/getHome
coloca a vista [home] em [Response.content]
/getJumbotron
coloca a vista [jumbotron] em [Response.jumbotron]
/getAgenda
coloca a vista [agenda] em [Response.agenda]
/getLogin
coloca a vista [login] em [Response.content]
/getNavbarRunJumbotronHome
  • Se a ligação for bem-sucedida, coloque a vista [navbar-run] em [Response.navbar], a vista [jumbotron] em [Response.jumbotron] e a vista [home] em [Response.content]
  • Se a ligação falhar, coloque a vista [errors] em [Response.content] e defina [Response.status] como 2
/getNavbarRunJumbotronHomeCalendar
coloca a vista [navbar-run] em [Response.navbar], a vista [jumbotron] em [Response.jumbotron], a vista [home] em [Response.content] e a vista [calendar] em [Response.calendar]
/addAppointment
adiciona o compromisso selecionado e coloca a nova agenda em [Response.agenda]
/deleteAppointment
elimina o compromisso selecionado e coloca o novo calendário em [Response.calendar]

8.6.6.2. O singleton [ApplicationModel]

 

A classe [ApplicationModel] é instanciada como uma única instância e injetada no controlador da aplicação. O seu código é o seguinte:


package rdvmedecins.springthymeleaf.server.models;
 
import java.util.ArrayList;
...
 
@Component
public class ApplicationModel implements IDao {
 
....
}
  • linha 6: [ApplicationModel] é um componente Spring;
  • linha 7: que implementa a interface da camada [DAO]. Fazemos isto para que as ações não precisem de saber sobre a camada [DAO], mas apenas sobre o singleton [ApplicationModel]. A arquitetura do [Web1] passa então a ser a seguinte:

Voltemos ao código da classe [ApplicationModel]:


package rdvmedecins.springthymeleaf.server.models;
 
import java.util.ArrayList;
...
 
@Component
public class ApplicationModel implements IDao {
 
    // the [DAO] layer
    @Autowired
    private IDao dao;
    // configuration
    @Autowired
    private AppConfig appConfig;
 
    // data from the [DAO] layer
    private List<ClientItem> clientItems;
    private List<MedecinItem> medecinItems;
    // configuration data
    private String userInit;
    private String mdpUserInit;
    private boolean corsAllowed;
    // exception
    private RdvMedecinsException rdvMedecinsException;
 
    // manufacturer
    public ApplicationModel() {
    }
 
    @PostConstruct
    public void init() {
        // config
        userInit = appConfig.getUSER_INIT();
        mdpUserInit = appConfig.getMDP_USER_INIT();
        dao.setTimeout(appConfig.getTIMEOUT());
        dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
        corsAllowed = appConfig.isCORS_ALLOWED();
        // caching of physician and customer drop-down lists
        List<Medecin> medecins = null;
        List<Client> clients = null;
        try {
            medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
            clients = dao.getAllClients(new User(userInit, mdpUserInit));
        } catch (RdvMedecinsException ex) {
            rdvMedecinsException = ex;
        }
        if (rdvMedecinsException == null) {
            // create drop-down list items
            medecinItems = new ArrayList<MedecinItem>();
            for (Medecin médecin : medecins) {
                medecinItems.add(new MedecinItem(médecin));
            }
            clientItems = new ArrayList<ClientItem>();
            for (Client client : clients) {
                clientItems.add(new ClientItem(client));
            }
        }
    }
 
    // getters and setters
    ...
 
    // interface implementation [IDao]
    @Override
    public void setUrlServiceWebJson(String url) {
        dao.setUrlServiceWebJson(url);
    }
 
    @Override
    public void setTimeout(int timeout) {
        dao.setTimeout(timeout);
    }
 
    @Override
    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
        return dao.ajouterRv(user, jour, idCreneau, idClient);
    }
 
    ...
}
  • linha 11: injeção da referência à implementação da camada [DAO]. Esta referência é então utilizada para implementar a interface [IDao] (linhas 64–80);
  • linha 14: injeção da configuração da aplicação;
  • linhas 33–37: utilização desta configuração para configurar vários elementos da arquitetura da aplicação;
  • Linhas 38–46: Armazenamos em cache as informações que irão preencher as listas suspensas de médicos e clientes. Partimos, portanto, do princípio de que, se um médico ou cliente for alterado, a aplicação terá de ser reiniciada. A ideia aqui é mostrar que um singleton do Spring pode funcionar como cache para a aplicação web;

As classes [MedecinItem] e [ClientItem] derivam ambas da seguinte classe [PersonneItem]:


package rdvmedecins.springthymeleaf.server.models;
 
import rdvmedecins.client.entities.Personne;
 
public class PersonneItem {
 
    // element of a list
    private Long id;
    private String texte;
 
    // manufacturer
    public PersonneItem() {
 
    }
 
    public PersonneItem(Personne personne) {
        id = personne.getId();
        texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
    }
 
    // getters and setters
...
}
  • linha 8: o campo [id] será o valor do atributo [value] de uma opção da lista suspensa;
  • linha 9: o campo [text] será o texto exibido por uma opção da lista suspensa;

8.6.6.3. A classe [BaseController]

 

A classe [BaseController] é a classe pai dos controladores [RdvMedecinsController] e [RdvMedecinsCorsController]. Não era obrigatório criar esta classe pai. Agrupámos aqui métodos utilitários da classe [RdvMedecinsController], nenhum dos quais é essencial, exceto um. Podem ser classificados em três grupos:

  1. métodos utilitários;
  2. métodos que renderizam vistas combinadas com os seus modelos;
  3. o método para inicializar uma ação

protected List<String>
getErrorsForException(Exception exception)

Lista protegida de <String>
getErrorsForModel(BindingResult result,
Locale locale,
WebApplicationContext ctx)
dois métodos utilitários que fornecem uma lista de mensagens de erro. Já os encontrámos e utilizámos;

protected String getPartialViewHome(WebContext
thymeleafContext)
retorna a vista [home] sem um modelo

protected String getPartialViewAgenda(ActionContext
actionContext,
AgendaMedecinJour,
Local (local)
retorna a vista [agenda] e o seu modelo

protected String getPartialViewLogin(WebContext thymeleafContext)
retorna a vista [login] sem um modelo

protected Response getViewErrors(WebContext thymeleafContext, List<String> errors)
retorna a resposta ao cliente quando a ação solicitada pelo terminou com um erro

protected ActionContext getActionContext
(String lang, String origin,
HttpServletRequest request,
HttpServletResponse response,
BindingResult result,
RdvMedecinsCorsController rdvMedecinsCorsController)
o método de inicialização para todas as ações do controlador [RdvMedecinsController]

Vamos examinar dois desses métodos.

O método [getPartialViewAgenda] renderiza a vista mais complexa de gerar, a do calendário. O seu código é o seguinte:


    // feed [agenda]
    protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
        // contexts
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        // build the [agenda] page template
        ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
        // the agenda with its model
        thymeleafContext.setVariable("agenda", modelAgenda);
        thymeleafContext.setVariable("clientItems", application.getClientItems());
        return engine.process("agenda", thymeleafContext);
}
  • Linhas 9–10: os dois elementos do modelo do calendário:
    • linha 9: o calendário apresentado.
    • linha 10: a lista de clientes apresentada quando o utilizador marca uma consulta;

O método [setModelforAgenda] na linha 7 é o seguinte:


// agenda] page template
    private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
        // page title
        String dateFormat = springContext.getMessage("date.format", null, locale);
        Medecin médecin = agenda.getMedecin();
        String titre = springContext.getMessage("agenda.titre", new String[] { médecin.getTitre(), médecin.getPrenom(),
                médecin.getNom(), new SimpleDateFormat(dateFormat).format(agenda.getJour()) }, locale);
        // reservation slots
        ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
        int i = 0;
        for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
            // doctor's slot
            Creneau créneau = creneauMedecinJour.getCreneau();
            ViewModelCreneau modelCréneau = new ViewModelCreneau();
            modelCréneaux[i] = modelCréneau;
            // id
            modelCréneau.setId(créneau.getId());
            // time slot
            modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
                    créneau.getHfin(), créneau.getMfin()));
            Rv rv = creneauMedecinJour.getRv();
            // customer and order
            String commande;
            if (rv == null) {
                modelCréneau.setClient("");
                commande = springContext.getMessage("agenda.reserver", null, locale);
                modelCréneau.setCommande(commande);
                modelCréneau.setAction(ViewModelCreneau.ACTION_RESERVER);
 
            } else {
                Client client = rv.getClient();
                modelCréneau.setClient(String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom()));
                commande = springContext.getMessage("agenda.supprimer", null, locale);
                modelCréneau.setCommande(commande);
                modelCréneau.setIdRv(rv.getId());
                modelCréneau.setAction(ViewModelCreneau.ACTION_SUPPRIMER);
            }
            // next slot
            i++;
        }
        // we render the agenda model
        ViewModelAgenda modelAgenda = new ViewModelAgenda();
        modelAgenda.setTitre(titre);
        modelAgenda.setCreneaux(modelCréneaux);
        return modelAgenda;
    }
  • Linha 6: A agenda tem um título:

Image

ou:

Image

Podemos ver que o formato da data depende do idioma. Recuperamos este formato dos ficheiros de mensagens (linha 4).

  • linhas 11–40: para cada intervalo de tempo, temos de apresentar a vista:

Image

ou a visualização:

Image

  • Linhas 19–20: exibir o intervalo de tempo;
  • linhas 25–28: o caso em que o intervalo de tempo está disponível. Neste caso, o botão [Reservar] deve ser exibido;
  • linhas 31–36: o caso em que o intervalo de tempo está ocupado. Neste caso, tanto o cliente como o botão [Eliminar] devem ser apresentados;

O outro método que iremos discutir mais detalhadamente é o método [getActionContext]. É chamado no início de cada ação no [RdvMedecinsController]. A sua assinatura é a seguinte:


protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController)

Retorna o seguinte tipo [ActionContext]:


public class ActionContext {
 
    // data
    private WebContext thymeleafContext;
    private WebApplicationContext springContext;
    private Locale locale;
    private List<String> erreurs;
...
}
  • linha 4: o contexto Thymeleaf da ação;
  • linha 5: o contexto Spring da ação;
  • linha 6: a localização da ação;
  • linha 7: uma lista de possíveis mensagens de erro;

Os seus parâmetros são os seguintes:

  • [lang]: o idioma solicitado para a ação, «en» ou «fr»;
  • [origin]: o cabeçalho HTTP [origin] no caso de um pedido entre domínios;
  • [request]: o pedido HTTP atualmente a ser processado, o que há algum tempo é referido como uma ação;
  • [response]: a resposta que será enviada em resposta a esta solicitação;
  • [result]: cada ação do [RdvMedecinsController] recebe um valor enviado cuja validade é testada. [result] é o resultado desse teste;
  • [rdvMedecinsController]: o controlador que contém as ações;

O método [getActionContext] é implementado da seguinte forma:


    // context of an action
    protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
        // language?
        if (lang == null) {
            lang = "fr";
        }
        // local
        Locale locale = null;
        if (lang.trim().toLowerCase().equals("fr")) {
            // french
            locale = new Locale("fr", "FR");
        } else {
            // everything else in English
            locale = new Locale("en", "US");
        }
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, response);
        // ActionContext
        ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
        // initialization errors
        RdvMedecinsException e = application.getRdvMedecinsException();
        if (e != null) {
            actionContext.setErreurs(e.getMessages());
            return actionContext;
        }
        // POST errors?
        if (result != null && result.hasErrors()) {
            actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
            return actionContext;
        }
        // no errors
        return actionContext;
}
  • linhas 3–15: com base no parâmetro [lang], definimos a localização da ação;
  • linha 17: enviamos os cabeçalhos HTTP necessários para pedidos entre domínios. Não entraremos em detalhes aqui. A técnica utilizada é a descrita na secção 8.4.14;
  • linha 19: construção de um objeto [ActionContext] sem erros;
  • linha 21: vimos na secção 8.6.6.2 que o singleton [ApplicationModel] acedeu à base de dados para recuperar tanto os clientes como os médicos. Este acesso pode falhar. Registamos então a exceção que ocorre. Na linha 21, recuperamos esta exceção;
  • linhas 22–25: se ocorreu uma exceção durante o arranque da aplicação, não é possível realizar nenhuma ação. Por isso, devolvemos um objeto [ActionContext] para qualquer ação, contendo as mensagens de erro da exceção;
  • linhas 27–20: analisamos o parâmetro [result] para determinar se o valor enviado era válido ou não. Se fosse inválido, devolvemos um objeto [ActionContext] com as mensagens de erro apropriadas;
  • linha 32: caso sem erros;

Vamos agora examinar as ações do [RdvMedecinsController]

8.6.6.4. A ação [/getNavBarStart]

A ação [/getNavBarStart] renderiza a vista [navbar-start]. A sua assinatura é a seguinte:


@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin)

Retorna o seguinte tipo [Response]:


public class Reponse {
 
    // ----------------- properties
    // operation status
    private int status;
    // the navigation bar
    private String navbar;
    // the jumbotron
    private String jumbotron;
    // the body of the page
    private String content;
    // the diary
    private String agenda;
...
}

e tem os seguintes parâmetros:

  • [PostLang postlang]: o próximo valor publicado:

public class PostLang {
 
    // data
    @NotNull
    private String lang;
...
}

A classe [PostLang] é a classe pai de todos os valores enviados. Isto porque o cliente deve sempre especificar o idioma em que a ação deve ser executada.

O método [getNavbarStart] é implementado da seguinte forma:


    // navbar-start
    @RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // returns the [navbar-start] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
        return reponse;
}
  • linha 7: inicialização da ação;
  • linhas 10–13: se o método de inicialização da ação reportou erros, estes são enviados na resposta ao cliente (linha 12) com o estado 2:
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • linhas 15-18: enviar a vista [navbar-start] com o status 1:
 {"status":1,"navbar": navbar-start, "jumbotron": null, "agenda":null, "content":null}

A seguir, iremos detalhar apenas as novas funcionalidades.

8.6.6.5. A ação [/getNavbarRun]

A ação [/getNavBarRun] apresenta a vista [navbar-run]:


    // navbar-run
    @RequestMapping(value = "/getNavbarRun", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getNavbarRun(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // returns the [navbar-run] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        return reponse;
}

A ação pode devolver dois tipos de respostas:

  • a resposta com um erro (linhas 10–13):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • a resposta com a vista [navbar-run]:
 {"status":1,"navbar": navbar-run, "jumbotron": null, "agenda":null, "content":null}

8.6.6.6. A ação [/getJumbotron]

A ação [/getJumbotron] devolve a vista [jumbotron]:


    // jumbotron
    @RequestMapping(value = "/getJumbotron", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getJumbotron(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // return view [jumbotron]
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        return reponse;
}

A ação pode devolver dois tipos de respostas:

  • a resposta com um erro (linhas 10–13):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • resposta com a visualização [jumbotron]:
 {"status":1,"navbar": null, "jumbotron": jumbotron, "agenda":null, "content":null}

8.6.6.7. A ação [/getLogin]

A ação [/getLogin] apresenta a vista [login]:


@RequestMapping(value = "/getLogin", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getLogin(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // returns the [login] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
        reponse.setContent(getPartialViewLogin(thymeleafContext));
        return reponse;
    }

A ação pode devolver dois tipos de respostas:

  • A resposta com um erro (linhas 9–11):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • A resposta com a visualização [login]:
 {"status":1,"navbar": navbar-start, "jumbotron": jumbotron, "agenda":null, "content":login}

8.6.6.8. A ação [/getHome]

A ação [/getHome] devolve a vista [home]. A sua assinatura é a seguinte:


    @RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) 
  • Linha 3: O valor enviado é do tipo [PostUser], conforme segue:

public class PostUser extends PostLang {
    // data
    @NotNull
    private User user;
...
}
  • linha 1: a classe [PostUser] estende a classe [PostLang] e, portanto, inclui um idioma;
  • linha 4: o utilizador que tenta recuperar a vista;

O código de implementação é o seguinte:


    @RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // the [home] view is protected
        try{
            // user
            User user = postUser.getUser();
            // we check identifiers [userName, password]
            application.authenticate(user);
        }catch(RdvMedecinsException e){
            // an error is returned
            return getViewErreurs(thymeleafContext, e.getMessages());
        }
        // returns the [home] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        return reponse;
}
  • Linhas 15–22: Note que a página [inicial] está protegida, pelo que o utilizador deve ser autenticado;

A ação pode devolver dois tipos de respostas:

  • a resposta de erro (linhas 11 e 21):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • resposta com a vista [home] (linhas 24–27):
 {"status":1,"navbar": null, "jumbotron": null, "agenda":null, "content":accueil}

8.6.6.9. A ação [/getNavbarRunJumbotronHome]

A ação [/getNavbarRunJumbotronHome] renderiza as vistas [navbar-run, jumbotron, home]. Tem a seguinte assinatura:


@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser post, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) 
  • linha 3: o valor enviado é do tipo [PostUser];

A implementação da ação é a seguinte:


// navbar+ jumbotron + home
    @RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
                rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // the [home] view is protected
        try {
            // user
            User user = postUser.getUser();
            // we check identifiers [userName, password]
            application.authenticate(user);
        } catch (RdvMedecinsException e) {
            // an error is returned
            return getViewErreurs(thymeleafContext, e.getMessages());
        }
        // we send the answer
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        return reponse;
    }

A ação pode devolver dois tipos de respostas:

  • a resposta com um erro (linhas 13, 23):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • a resposta com as visualizações [navbar-run, jumbotron, home] (linhas 26–31):
 {"status":1,"navbar": navbar-run, "jumbotron": jumbotron, "agenda":null, "content":accueil}

8.6.6.10. A ação [/getAgenda]

A ação [/getAgenda] apresenta a vista [agenda]. A sua assinatura é a seguinte:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin)
  • Linha 3: O valor enviado é do tipo [PostGetAgenda], conforme se segue:

public class PostGetAgenda extends PostUser {
 
    // data
    @NotNull
    private Long idMedecin;
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date jour;
...
}
  • linha 1: a classe [PostGetAgenda] estende a classe [PostUser] e, por isso, inclui um idioma e um utilizador;
  • linha 5: o ID do médico cujo calendário é pretendido;
  • linha 8: o dia pretendido do calendário;

A implementação é a seguinte:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result,    rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        Locale locale = actionContext.getLocale();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // check the validity of the post
        if (result != null) {
            new PostGetAgendaValidator().validate(postGetAgenda, result);
            if (result.hasErrors()) {
                // returns the [errors] view
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        ...
}
  • Até à linha 14, o código é agora padrão;
  • linhas 16–21: realizamos uma verificação adicional do valor enviado. A data deve ser igual ou posterior à data de hoje. Para verificar isso, usamos um validador:

package rdvmedecins.web.validators;
 
import java.text.SimpleDateFormat;
import java.util.Date;
 
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
 
import rdvmedecins.springthymeleaf.server.requests.PostGetAgenda;
import rdvmedecins.springthymeleaf.server.requests.PostValiderRv;
 
public class PostGetAgendaValidator implements Validator {
 
    public PostGetAgendaValidator() {
    }
 
    @Override
    public boolean supports(Class<?> classe) {
        return PostGetAgenda.class.equals(classe) || PostValiderRv.class.equals(classe);
    }
 
    @Override
    public void validate(Object post, Errors errors) {
        // the day chosen for the appointment
        Date jour = null;
        if (post instanceof PostGetAgenda) {
            jour = ((PostGetAgenda) post).getJour();
        } else {
            if (post instanceof PostValiderRv) {
                jour = ((PostValiderRv) post).getJour();
            }
        }
        // transform dates into yyyy-MM-dd format
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String strJour = sdf.format(jour);
        String strToday = sdf.format(new Date());
        // the chosen day must not precede today's date
        if (strJour.compareTo(strToday) < 0) {
            errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
        }
    }
 
}
  • Linha 19: O validador funciona para duas classes: [PostGetAgenda] e [PostValiderRv];

Voltemos ao código da ação [/getAgenda]:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        ...
                // action
        try {
            // doctor's diary
            AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
                    new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
            // answer
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException e1) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, e1.getMessages());
        } catch (Exception e2) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
}
  • linhas 9-10: utilizando os parâmetros enviados, solicitamos a agenda do médico;
  • linhas 12-13: devolvemos a agenda:
 {"status":1,"navbar": null, "jumbotron": null, "agenda":agenda, "content":null}
  • linhas 17, 21: devolvemos uma resposta com erros:
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}

8.6.6.11. A ação [/getNavbarRunJumbotronHomeCalendar]

A ação [/getNavbarRunJumbotronHomeCalendar] renderiza as vistas [navbar-run, jumbotron, home, calendar]. A sua implementação é a seguinte:


    @RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
            HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // agenda
        Reponse agenda = getAgenda(post, result, request, response, null);
        if (agenda.getStatus() != 1) {
            return agenda;
        }
        // we send the answer
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        reponse.setAgenda(agenda.getAgenda());
        return reponse;
}
  • linhas 15–18: Aproveitamos a existência da ação [/getAgenda] para a chamar. Em seguida, verificamos o estado da resposta (linha 16). Se for detetado um erro, paramos aí e devolvemos a resposta;
  • linha 20: enviamos as visualizações solicitadas:
 {"status":1,"navbar": navbar-run, "jumbotron": jumbotron, "agenda":agenda, "content":accueil}

8.6.6.12. A ação [/supprimerRv]

A ação [/deleteRv] permite eliminar um compromisso. A sua assinatura é a seguinte:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin)
  • Linha 3: O valor enviado é do tipo [PostSupprimerRv], conforme se segue:

public class PostSupprimerRv extends PostUser {
 
    // data
    @NotNull
    private Long idRv;
..
}
  • linha 1: a classe [PostSupprimerRv] estende a classe [PostUser] e, por isso, inclui um idioma e um utilizador;
  • linha 5: o número do compromisso a ser eliminado;

A implementação da ação é a seguinte:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
                rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // posted values
        User user = postSupprimerRv.getUser();
        long idRv = postSupprimerRv.getIdRv();
        // we delete the appointment
        AgendaMedecinJour agenda = null;
        try {
            // we get it back
            Rv rv = application.getRvById(user, idRv);
            Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
            long idMedecin = creneau.getIdMedecin();
            Date jour = rv.getJour();
            // delete the associated rv
            application.supprimerRv(user, idRv);
            // we regenerate the doctor's diary
            agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // we return the new diary
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
}
  • linha 22: recupera a consulta a ser eliminada. Se não existir, é lançada uma exceção;
  • linhas 23–25: com base neste compromisso, localizamos o médico e o dia em questão. Esta informação é necessária para regenerar a agenda do médico;
  • linha 27: o compromisso é eliminado;
  • linha 29: solicitamos a nova agenda do médico. Isto é importante. Além do horário que acabou de ser liberado, outros utilizadores da aplicação podem ter feito alterações na agenda. É importante devolver a versão mais recente da agenda ao utilizador;
  • linhas 31–34: o calendário é devolvido:
 {"status":1,"navbar": null, "jumbotron": null, "agenda":agenda, "content":null}

8.6.6.13. A ação [/validerRv]

A ação [/validerRv] adiciona um compromisso ao calendário de um médico. A sua assinatura é a seguinte:


@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request,    HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
  • Linha 3: O valor enviado é do tipo [PostValiderRv], conforme se segue:

public class PostValiderRv extends PostUser {
 
    // data
    @NotNull
    private Long idCreneau;
    @NotNull
    private Long idClient;
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date jour;
...
}
  • linha 1: a classe [PostValiderRv] estende a classe [PostUser] e, por isso, inclui um idioma e um utilizador;
  • linha 5: o número do intervalo de tempo;
  • linha 7: o ID do cliente para quem a reserva é feita;
  • linha 10: o dia da marcação;

A implementação da ação é a seguinte:


// appointment validation
    @RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebApplicationContext springContext = actionContext.getSpringContext();
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // check the validity of the appointment date
        if (result != null) {
            new PostGetAgendaValidator().validate(postValiderRv, result);
            if (result.hasErrors()) {
                // returns the [errors] view
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        // posted values
        User user = postValiderRv.getUser();
        long idClient = postValiderRv.getIdClient();
        long idCreneau = postValiderRv.getIdCreneau();
        Date jour = postValiderRv.getJour();
        // action
        try {
            // get information on the niche
            Creneau créneau = application.getCreneauById(user, idCreneau);
            long idMedecin = créneau.getIdMedecin();
            // we add the Rv
            application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
            // we regenerate the agenda
            AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
                    new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // we return the new diary
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
    }
}

O código é semelhante ao da ação [/deleteRv].

8.6.7. Passo 4: Testar o servidor Spring/Thymeleaf

Vamos agora testar as várias ações descritas acima utilizando o plugin do Chrome [Advanced Rest Client] (ver secção 9.6).

8.6.7.1. Configuração do teste

Todas as ações esperam um valor enviado. Iremos enviar variações da seguinte cadeia JSON:

{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Este valor enviado inclui informações que são supérfluas para a maioria das ações. No entanto, estas são ignoradas pelas ações que as recebem e não causam um erro. Este valor enviado tem a vantagem de abranger os vários valores a serem enviados.

8.6.7.2. A ação [/getNavbarStart]

  • em [1], a ação que está a ser testada;
  • em [2], o valor enviado;
  • em [3], o valor enviado é uma cadeia JSON;
  • em [4], a vista [navbar-start] é solicitada em inglês;

O resultado obtido é o seguinte:

 

Recebemos a vista [navbar-start] em inglês (áreas destacadas).

Agora, vamos introduzir um erro. Definimos o atributo [lang] do valor enviado como nulo. Recebemos o seguinte resultado:

 

Recebemos uma resposta de erro (status 2) indicando que o campo [lang] era obrigatório.

8.6.7.3. A ação [/getNavbarRun]

Solicitamos a ação [getNavbarRun] com o seguinte valor enviado:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado obtido é o seguinte:

 

8.6.7.4. A ação [/getJumbotron]

Solicitamos a ação [getJumbotron] com os seguintes dados POST:


{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado obtido é o seguinte:

 

8.6.7.5. A ação [/getLogin]

Solicitamos a ação [getLogin] com os seguintes dados POST:


{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado é o seguinte:

 

8.6.7.6. A ação [/getAccueil]

Solicitamos a ação [getAccueil] com o seguinte valor enviado:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado obtido é o seguinte:

 

Tentamos novamente com um utilizador desconhecido:


{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado é o seguinte:

 

Começamos novamente com um utilizador existente que não está autorizado a utilizar a aplicação:


{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado é o seguinte:

 

8.6.7.7. A ação [/getAgenda]

Solicitamos a ação [getAgenda] com o seguinte valor enviado:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado obtido é o seguinte:

 

Tentamos novamente com uma data anterior à de hoje:

 

Começamos novamente com um médico que não existe:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":11, "idCreneau":2, "idClient":4, "idRv":93}

O resultado é o seguinte:

 

8.6.7.8. A ação [/getNavbarRunJumbotronAccueil]

Solicitamos a ação [getNavbarRunJumbotronAccueil] com o seguinte valor enviado:


{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado é o seguinte:

 

O mesmo se aplica a um utilizador desconhecido:

 

8.6.7.9. A ação [/getNavbarRunJumbotronHomeCalendar]

Solicitamos a ação [getNavbarRunJumbotronHomeCalendar] com o seguinte valor enviado:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado é o seguinte:

 

Introduzimos um médico que não existe:

 

8.6.7.10. A ação [/deleteAppointment]

Solicitamos a ação [deleteAppointment] com o seguinte valor enviado:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

A consulta n.º 93 não existe. O resultado obtido é o seguinte:

 

Com um compromisso existente:

 

Podemos verificar na base de dados se o compromisso foi efetivamente eliminado. O novo calendário é devolvido.

8.6.7.11. A ação [/validateAppointment]

Solicitamos a ação [validateAppointment] com o seguinte valor enviado:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado é o seguinte:

 

Podemos verificar na base de dados que o compromisso foi criado com sucesso. O novo calendário foi devolvido.

Fazemos o mesmo com um número de intervalo inexistente:

 

Fazemos o mesmo com um ID de cliente inexistente:

 

8.6.8. Passo 5: Escrever o cliente JavaScript

Voltemos à arquitetura do servidor [Web1]:

O cliente [2] do servidor [Web1] é um cliente JavaScript do tipo SPV (Single-Page Application):

  • O cliente solicita a página inicial a um servidor web (não necessariamente [Web1]);
  • solicita as páginas seguintes ao servidor [Web1] através de chamadas Ajax;

Para construir este cliente, utilizaremos a ferramenta [Webstorm] (ver secção 9.8). Achei esta ferramenta mais prática do que o STS. A sua principal vantagem é que oferece autocompletar de código, bem como algumas opções de refatoração. Isto ajuda a evitar muitos erros.

8.6.8.1. O projeto JS

O projeto JS tem a seguinte estrutura de diretórios:

  • em [1], o cliente JS como um todo. [boot.html] é a página inicial. Esta será a única página carregada pelo navegador;
  • em [2], as folhas de estilo para os componentes Bootstrap;
  • em [3], as poucas imagens utilizadas pela aplicação;
  • em [4], os scripts JS. É aqui que o nosso trabalho se realiza;
  • em [5], as bibliotecas JS utilizadas: principalmente jQuery e as destinadas aos componentes Bootstrap;

8.6.8.2. A arquitetura do código

O código foi dividido em três camadas:

  • a camada [presentation] contém as funções de inicialização da página [boot.xml], bem como as dos vários componentes do Bootstrap. É implementada pelo ficheiro [ui.js];
  • a camada [eventos] contém todos os manipuladores de eventos para a camada [apresentação]. É implementada pelo ficheiro [evts.js];
  • a camada [DAO] efetua pedidos HTTP ao servidor [Web1]. É implementada pelo ficheiro [dao.js];

8.6.8.3. A camada [presentation]

  

A camada [presentation] é implementada pelo seguinte ficheiro [ui.js]:


//la couche [présentation]
var ui = {
// variables globales;
  "agenda": "",
  "resa": "",
  "langue": "",
  "urlService": "http://localhost:8081",
  "page": "login",
  "jourAgenda": "",
  "idMedecin": "",
  "user": {},
  "login": {},
  "exceptionTitle": {},
  "calendar_infos": {},
  "erreur": "",
  "idCreneau": "",
  "done": "",
// composants de la vue
  "body": "",
  "navbar": "",
  "jumbotron": "",
  "content": "",
  "exception": "",
  "exception_text": "",
  "exception_title": "",
  "loading": ""
};
// la couche des evts
var evts = {};
// la couche [dao]
var dao = {};
 
// ------------ document ready
$(document).ready(function () {
  // initialisation document
  console.log("document.ready");
  // composants de la page
  ui.navbar = $("#navbar");
  ui.jumbotron = $("#jumbotron");
  ui.content = $("#content");
  ui.erreur = $("#erreur");
  ui.exception = $("#exception");
  ui.exception_text = $("#exception-text");
  ui.exception_title = $("#exception-title");
  // on mémorise la page de login pour pouvoir la restituer
  ui.login.lang = ui.langue;
  ui.login.navbar = ui.navbar.html();
  ui.login.jumbotron = ui.jumbotron.html();
  ui.login.content = ui.content.html();
  // URL du service
  $("#urlService").val(ui.urlService);
});
 
// ------------------------ Bootstrap component initialization functions
ui.initNavBarStart = function () {
...
};
 
ui.initNavBarRun = function () {
...
};
 
ui.initChoixMedecinJour = function () {
...
};
 
ui.updateCalendar = function (renew) {
...
};
 
// affiche le jour sélectionné
ui.displayJour = function () {
...
};
 
ui.initAgenda = function () {
...
};
 
ui.initResa = function () {
 ...
};
 
  • Para isolar as camadas umas das outras, decidiu-se colocá-las em três objetos:
    • [ui] para a camada [presentation] (linhas 2–27),
    • [evts] para a camada de gestão de eventos (linha 29),
    • [dao] para a camada [DAO] (linha 31);

Esta separação das camadas em três objetos ajuda a evitar uma série de conflitos de nomes de variáveis e funções. Cada camada utiliza variáveis e funções prefixadas com o objeto que encapsula a camada.

  • linhas 38–44: armazenamos os campos que estarão sempre presentes, independentemente das visualizações apresentadas. Isto evita pesquisas jQuery repetitivas e desnecessárias;
  • linhas 46–49: a página inicial é armazenada localmente para que possa ser restaurada quando o utilizador sair da sessão e não tiver alterado o idioma;
  • linhas 54–83: funções de inicialização dos componentes Bootstrap. Todas estas foram abordadas na discussão sobre os componentes Bootstrap na secção 8.6.4;

8.6.8.4. Funções utilitárias da camada [events]

  

Os manipuladores de eventos foram colocados no ficheiro [evts.js]. Várias funções são utilizadas regularmente pelos manipuladores de eventos. Apresentamo-las agora:


// début d'attente
evts.beginWaiting = function () {
  // début attente
  ui.loading = $("#loading");
  ui.loading.show();
  ui.exception.hide();
  ui.erreur.hide();
  evts.travailEnCours = true;
};
 
// fin d'attente
evts.stopWaiting = function () {
  // fin attente
  evts.travailEnCours = false;
  ui.loading = $("#loading");
  ui.loading.hide();
};
 
// affichage résultat
evts.showResult = function (result) {
  // on affiche les données reçues
  var data = result.data;
  // on analyse le status
  switch (result.status) {
    case 1:
      // erreur ?
      if (data.status == 2) {
        ui.erreur.html(data.content);
        ui.erreur.show();
      } else {
        if (data.navbar) {
          ui.navbar.html(data.navbar);
        }
        if (data.jumbotron) {
          ui.jumbotron.html(data.jumbotron);
        }
        if (data.content) {
          ui.content.html(data.content)
        }
        if (data.agenda) {
          ui.agenda = $("#agenda");
          ui.resa = $("#resa");
        }
      }
      break;
    case 2:
      // affichage erreur
      evts.showException(data);
      break;
  }
};
 
// ------------ fonctions diverses
evts.showException = function (data) {
  // affichage erreur
  ui.exception.show();
  ui.exception_text.html(data);
  ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
  • linha 2: a função [evts.beginwaiting] é chamada antes de qualquer ação [DAO] assíncrona;
  • linhas 4-5: a imagem animada da espera é exibida;
  • linhas 6-7: a área de exibição de erros e exceções é ocultada (não são a mesma coisa);
  • linha 8: observamos que uma tarefa assíncrona está em curso;
  • linha 12: a função [evts.stopwaiting] é chamada após uma ação [DAO] assíncrona ter devolvido o seu resultado;
  • linha 14: verificamos que a operação assíncrona está concluída;
  • linha 15: a imagem animada de espera é ocultada;
  • linha 20: a função [evts.showResult] exibe o resultado [result] de uma ação assíncrona [DAO]. O resultado é um objeto JS com o seguinte formato: {'status':status,'data':data,'sendMeBack':sendMeBack}.
  • linhas 47–50: utilizadas se [result.status == 2]. Isto ocorre quando o servidor [Web1] envia uma resposta com um cabeçalho de erro HTTP (por exemplo, 403 Forbidden). Neste caso, [data] é a cadeia JSON enviada pelo servidor para indicar o erro;
  • linha 25: caso em que foi recebida uma resposta válida do servidor [Web1]. O campo [data] contém então a resposta do servidor: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
  • linha 27: caso em que o servidor [Web1] enviou uma resposta de erro {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':errors};
  • linhas 28–29: é apresentada a vista [errors];
  • linhas 31-33: exibição opcional da barra de navegação;
  • linhas 34-36: exibição opcional do jumbotron;
  • linhas 37-39: o campo [data.content] pode ser exibido. Dependendo do caso, isto representa uma das vistas [home, calendar];
  • linhas 40-43: se o calendário tiver sido regenerado, determinadas referências aos seus componentes são recuperadas para que não seja necessário procurá-las sempre que forem necessárias;
  • linha 54: a função [evts.showException] exibe o texto da exceção contido no seu parâmetro [data];
  • linhas 57-58: o texto da exceção é exibido;
  • linha 58: o título da exceção depende do idioma atual;

O ficheiro [evts.js] contém mais de 300 linhas de código, que não irei comentar na íntegra. Vou simplesmente destacar alguns exemplos para ilustrar o objetivo desta camada.

8.6.8.5. Início de sessão do utilizador

Image

O login do utilizador é tratado pela seguinte função:


// ------------------------ connexion
evts.connecter = function () {
  // retrieve the values to be posted
  var login = $("#login").val().trim();
  var passwd = $("#passwd").val().trim();
  // set the server's URL
  ui.urlService = $("#urlService").val().trim();
  dao.setUrlService(ui.urlService);
  // query parameters
  var post = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "lang": ui.langue
  };
  var sendMeBack = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "caller": evts.connecterDone
  };
  // query
  evts.execute([{
    "name": "accueil-sans-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • linhas 4-5: recuperam o nome de utilizador e a palavra-passe do utilizador;
  • linhas 7-8: recuperam o URL do serviço [Web1]. Este é armazenado tanto na camada [ui] como na camada [dao];
  • linhas 10-16: o valor a ser enviado: o idioma atual e o utilizador que está a tentar iniciar sessão;
  • linhas 17–23: o objeto [sendMeBack] é passado para a função [DAO] que será chamada, e esta função deve devolvê-lo à função na linha 22. Aqui, o objeto [sendMeBack] encapsula o utilizador que está a tentar iniciar sessão;
  • linhas 25–29: a função [evts.execute] é capaz de executar uma sequência de ações assíncronas. Aqui, passamos uma lista composta por uma única ação. Os seus campos são os seguintes:
    • [name]: o nome da ação assíncrona a ser executada,
    • [post]: o valor a ser enviado para o servidor [Web1],
    • [sendMeBack]: o valor que a ação assíncrona deve devolver juntamente com o seu resultado;

Antes de entrarmos em detalhes sobre a função [evts.execute], vamos analisar a função [evts.connecterDone] na linha 22. Esta é a função à qual a função [DAO] assíncrona chamada deve devolver o seu resultado:


evts.connecterDone = function (result) {
  // affichage résultat
  evts.showResult(result);
  // connexion réussie ?
  if (result.status == 1 && result.data.status == 1) {
    // page
    ui.page = "accueil-sans-agenda";
    // on note l'utilisateur
    ui.user = result.sendMeBack.user;
  }
};
  • linha 3: o resultado devolvido pelo servidor [Web1] é apresentado;
  • linha 5: se este resultado não contiver erros, então armazenamos o tipo da nova página (linha 7), bem como o utilizador autenticado (linha 9);

A função [evts.execute] executa uma sequência de ações assíncronas:


// exécution d'une suite d'actions
evts.execute = function (actions) {
  // travail en cours ?
  if (evts.travailEnCours) {
    // on ne fait rien
    return;
  }
  // attente
  evts.beginWaiting();
  // exécution des actions
  dao.doActions(actions, evts.stopWaiting);
};
  • linha 2: o parâmetro [actions] é uma lista de ações assíncronas a serem executadas;
  • linhas 4–7: a execução só é aceite se nenhuma outra ação estiver já em curso;
  • linha 9: a espera é iniciada;
  • linha 11: a camada [DAO] é solicitada a executar a sequência de ações. O segundo parâmetro é o nome da função a ser executada assim que todas as ações da sequência tiverem retornado os seus resultados;

Não entraremos em detalhes sobre a função [dao.doActions] neste momento. Vamos examinar outro evento.

8.6.8.6. Mudança de idioma

Image

A mudança de idioma é tratada pela seguinte função:


// ------------------------ changement de langue
evts.setLang = function (lang) {
  // chgt de langue ?
  if (lang == ui.langue) {
    // on ne fait rien
    return;
  }
  // nouvelle langue
  ui.langue = lang;
  // quelle page faut-il traduire ?
  switch (ui.page) {
    case "login":
      evts.getLogin();
      break;
    case "accueil-sans-agenda":
      evts.getAccueilSansAgenda();
      break;
    case "accueil-avec-agenda":
      evts.getAccueilAvecAgenda(ui);
      break;
  }
};
  • linha 2: o parâmetro [lang] é o novo idioma: 'fr' ou 'en';
  • linhas 4–7: se o novo idioma for o atual, não fazer nada;
  • linha 9: o novo idioma é guardado;
  • linhas 12–20: se o idioma tiver mudado, a página atualmente exibida pelo navegador deve ser recarregada. Existem três páginas possíveis:
    • aquela chamada [login], em que a página apresentada é a página de início de sessão,
    • aquela chamada [home-without-calendar], que é a página apresentada imediatamente após a autenticação bem-sucedida,
    • aquela chamada [home-with-calendar], que é a página exibida assim que o primeiro calendário é apresentado. Permanece então no ecrã até o utilizador sair da sessão;

Vamos abordar o caso da página [home-with-calendar]. Existem três versões desta função:

  
  • a versão [getAccueilAvecAgenda-one] executa uma única ação assíncrona;
  • a versão [getAccueilAvecAgenda-parallel] executa quatro ações assíncronas em paralelo;
  • a versão [getAccueilAvecAgenda-sequence] executa quatro ações assíncronas uma após a outra;

8.6.8.7. A função [getAccueilAvecAgenda-one]

Esta é a seguinte função:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // query parameters
  var post = {
    "user": ui.user,
    "lang": ui.langue,
    "idMedecin": ui.idMedecin,
    "jour": ui.jourAgenda
  };
  var sendMeBack = {
    "caller": evts.getAccueilAvecAgendaDone
  };
  // request
  evts.execute([{
    "name": "accueil-avec-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • linhas 4-9: o valor a ser enviado encapsula o utilizador que iniciou sessão, o idioma pretendido, o ID do médico cuja agenda é pretendida e o dia da consulta pretendida;
  • linhas 10–12: o objeto [sendMeBack] é o objeto que será devolvido à função na linha 11. Aqui, não contém nenhuma informação;
  • linhas 14–18: execução de uma sequência de ações assíncronas, especificamente a denominada [welcome-with-calendar] (linha 15);
  • linha 11: a função executada quando a ação assíncrona [welcome-with-calendar] retorna o seu resultado;

A função [evts.getAccueilAvecAgendaDone] na linha 11 exibe o resultado da função assíncrona denominada [accueil-avec-agenda]:


evts.getAccueilAvecAgendaDone = function (result) {
  // affichage résultat
  evts.showResult(result);
  // nouvelle page ?
  if (result.status == 1 && result.data.status == 1) {
    ui.page = "accueil-avec-agenda";
  }
};
  • linha 1: [result] é o resultado da função assíncrona denominada [home-with-calendar];
  • linha 3: este resultado é apresentado;
  • linha 5: se for um resultado sem erros, a nova página é carregada (linha 6);

8.6.8.8. A função [getHomeWithCalendar-parallel]

Esta é a seguinte função:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // actions [navbar-run, jumbotron, home, calendar] in //
  // navbar-run
  var navbarRun = {
    "name": "navbar-run"
  };
  navbarRun.post = {
    "lang": ui.langue
  };
  navbarRun.sendMeBack = {
    "caller": evts.showResult
  };
  // jumbotron
  var jumbotron = {
    "name": "jumbotron"
  };
  jumbotron.post = {
    "lang": ui.langue
  };
  jumbotron.sendMeBack = {
    "caller": evts.showResult
  };
  // home
  var accueil = {
    "name": "accueil"
  };
  accueil.post = {
    "lang": ui.langue,
    "user": ui.user
  };
  accueil.sendMeBack = {
    "caller": evts.showResult
  };
  // agenda
  var agenda = {
    "name": "agenda"
  };
  agenda.post = {
    "user": ui.user,
    "lang": ui.langue,
    "idMedecin": ui.idMedecin,
    "jour": ui.jourAgenda
  };
  agenda.sendMeBack = {
    'idMedecin': ui.idMedecin,
    'jour': ui.jourAgenda,
    "caller": evts.getAgendaDone
  };
  // execution actions in //
  evts.execute([navbarRun, jumbotron, accueil, agenda])
};
  • linha 51: desta vez, são executadas quatro ações assíncronas. Estas serão executadas em paralelo;
  • linhas 5–13: definição da ação [navbarRun], que recupera a barra de navegação [navbar-run];
  • linha 12: a função a executar assim que a ação assíncrona [navbarRun] tiver devolvido o seu resultado;
  • linhas 15–23: definição da ação [jumbotron], que recupera a vista [jumbotron];
  • linha 22: a função a executar quando a ação assíncrona [jumbotron] devolver o seu resultado;
  • linhas 25–34: definição da ação [home], que recupera a vista [home];
  • linha 33: a função a ser executada quando a ação assíncrona [home] devolver o seu resultado;
  • linhas 36–49: definição da ação [agenda] que recupera a vista [jumbotron];
  • linha 48: a função a executar quando a ação assíncrona [agenda] devolve o seu resultado;

8.6.8.9. A função [getHomeWithAgenda-sequence]

Esta é a seguinte função:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // actions [navbar-run, jumbotron, home, agenda] in order
  // agenda
  var agenda = {
    "name" : "agenda"
  };
  agenda.post = {
    "user" : ui.user,
    "lang" : ui.langue,
    "idMedecin" : ui.idMedecin,
    "jour" : ui.jourAgenda
  };
  agenda.sendMeBack = {
    'idMedecin' : ui.idMedecin,
    'jour' : ui.jourAgenda,
    "caller" : evts.getAgendaDone
  };
  // home
  var accueil = {
    "name" : "accueil"
  };
  accueil.post = {
    "lang" : ui.langue,
    "user" : ui.user
  };
  accueil.sendMeBack = {
    "caller" : evts.showResult,
    "next" : agenda
  };
  // jumbotron
  var jumbotron = {
    "name" : "jumbotron"
  };
  jumbotron.post = {
    "lang" : ui.langue
  };
  jumbotron.sendMeBack = {
    "caller" : evts.showResult,
    "next" : accueil
  };
  // navbar-run
  var navbarRun = {
    "name" : "navbar-run"
  };
  navbarRun.post = {
    "lang" : ui.langue
  };
  navbarRun.sendMeBack = {
    "caller" : evts.showResult,
    "next" : jumbotron
  };
  // execution actions in sequence
  evts.execute([ navbarRun ])
};
  • linha 54: a ação [navbarRun] é executada. Quando termina, passamos para a seguinte: [jumbotron], linha 51. Esta ação é então executada por sua vez. Quando termina, passamos para a seguinte: [home], linha 40. Esta é executada por sua vez. Quando termina, passamos para a seguinte: [agenda], linha 29. Esta é executada por sua vez. Quando termina, paramos porque a ação [agenda] não tem nenhuma ação subsequente.

8.6.8.10. A camada [DAO]

  

O ficheiro [dao.js] contém todas as funções da camada [DAO]. Iremos apresentá-las gradualmente:


// URL exposed by the server
dao.urls = {
  "login": "/getLogin",
  "accueil": "/getAccueil",
  "jumbotron": "/getJumbotron",
  "agenda": "/getAgenda",
  "supprimerRv": "/supprimerRv",
  "validerRv": "/validerRv",
  "navbar-start": "/getNavbarStart",
  "navbar-run": "/getNavbarRun",
  "accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
  "accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- interface
// server url
dao.setUrlService = function (urlService) {
  dao.urlService = urlService;
};
  • linhas 16–18: a função que define o URL do serviço [Web1];
  • linhas 2-13: o dicionário que associa o nome de uma ação assíncrona à URL do servidor [Web1] a ser consultada;

// ------------------ gestion générique des actions
// exécution d'une suite d'actions asynchrones
dao.doActions = function (actions, done) {
  // traitement des actions
  dao.actionsCount = actions.length;
  dao.actionIndex = 0;
  for (var i = 0; i < dao.actionsCount; i++) {
    // requête DAO asynchrone
    var deferred = $.Deferred();
    deferred.done(dao.actionDone);
    dao.doAction(deferred, actions[i], done);
  }
};
  • linha 3: a função [dao.doActions] executa uma sequência de ações assíncronas [actions]. O parâmetro [done] é a função a ser executada assim que todas as ações tiverem devolvido os seus resultados;
  • linhas 7–12: as ações assíncronas são executadas em paralelo. No entanto, se uma delas tiver um sucessor, esse sucessor é executado no final da ação precedente;
  • linha 9: um objeto [Deferred] no estado [pending];
  • Linha 10: Quando este objeto entrar no estado [resolved], a função [dao.actionDone] será executada;
  • linha 11: a ação #i da lista é executada de forma assíncrona. O parâmetro [done] da linha 3 é passado como argumento;

A função [dao.actionDone], que é executada no final de cada ação assíncrona, é a seguinte:


// on a reçu un résultat
dao.actionDone = function (result) {
  // caller ?
  var sendMeBack = result.sendMeBack;
  if (sendMeBack && sendMeBack.caller) {
    sendMeBack.caller(result);
  }
  // next ?
  if (sendMeBack && sendMeBack.next) {
    // requête DAO asynchrone
    var deferred = $.Deferred();
    deferred.done(dao.actionDone);
    dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
  }
  // fini ?
  dao.actionIndex++;
  if (dao.actionIndex == dao.actionsCount) {
    // done ?
    if (sendMeBack && sendMeBack.done) {
      sendMeBack.done(result);
    }
  }
};
  • linha 2: a função [dao.actionDone] recebe o resultado [result] de uma das ações assíncronas na lista de ações a serem executadas;
  • linhas 4–7: se a ação assíncrona concluída especificou uma função à qual o resultado deve ser devolvido, essa função é chamada;
  • linhas 9–14: se a ação assíncrona concluída tiver um sucessor, essa ação é executada por sua vez;
  • linha 16: uma ação é concluída. O contador de ações concluídas é incrementado. Uma ação que tenha um número indeterminado de ações subsequentes conta como uma ação;
  • linhas 19–21: se tiver sido inicialmente especificado que uma função [done] fosse executada quando todas as ações da sequência tivessem devolvido os seus resultados, essa função é agora executada;

O método [dao.doAction] executa uma ação assíncrona:


// exécution d'une action
dao.doAction = function (deferred, action, done) {
  // fonction done à embarquer dans l'action
  if (action.sendMeBack) {
    action.sendMeBack.done = done;
  } else {
    action.sendMeBack = {
      "done": done
    };
  }
  // exécution action
  dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
  • Linhas 4–10: Como acabámos de ver, a função que irá tratar o resultado da ação assíncrona a ser executada deve ter acesso à função [done]. Para tal, colocamos a função [done] no objeto [sendMeBack], que fará parte do resultado da operação assíncrona;
  • linha 12: Executamos a função [dao.executePost], que faz uma solicitação HTTP ao servidor [Web1]. A URL de destino é a URL associada ao nome da ação a ser executada;

A função [dao.executePost] executa uma solicitação HTTP:


// requête HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
  // on fait un appel Ajax à la main
  $.ajax({
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    url: dao.urlService + url,
    type: 'POST',
    data: JSON3.stringify(post),
    dataType: 'json',
    success: function (data) {
      // on rend le résultat
      deferred.resolve({
        "status": 1,
        "data": data,
        "sendMeBack": sendMeBack
      });
    },
    error: function (jqXHR, textStatus, errorThrown) {
      var data;
      if (jqXHR.responseText) {
        data = jqXHR.responseText;
      } else {
        data = textStatus;
      }
      // on rend l'erreur
      deferred.resolve({
        "status": 2,
        "data": data,
        "sendMeBack": sendMeBack
      });
    }
  });
};

Já nos deparámos com esta função e já a discutimos. Repare simplesmente na linha 9 que o URL de destino é a concatenação do URL do servidor [Web1] com o URL associado ao nome da ação.

8.6.8.11. A página inicial

  

Image

A página inicial [boot.html] apresenta a visualização mostrada acima. É a única página carregada diretamente pelo navegador. As outras são recuperadas através de chamadas Ajax. O seu código é o seguinte:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
  <meta name="viewport" content="width=device-width"/>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title>RdvMedecins</title>
  <!-- Bootstrap core CSS -->
  <link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
  <link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
  <link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
  <link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
  <!-- Custom styles for this template -->
  <link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
  <!-- Bootstrap core JavaScript ================================================== -->
  <script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
  <script type="text/javascript" src="vendor/bootstrap.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-select.js"></script>
  <script type="text/javascript" src="vendor/moment-with-locales.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
  <script type="text/javascript" src="vendor/footable.js"></script>
  <!-- user scripts -->
  <script type="text/javascript" src="js/json3.js"></script>
  <script type="text/javascript" src="js/ui.js"></script>
  <script type="text/javascript" src="js/evts.js"></script>
  <script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
  <script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
      <div class="navbar-collapse collapse">
        <img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
        <!-- identification form -->
        <div class="navbar-form navbar-right" role="form" id="formulaire">
          <div class="form-group">
            <input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
          </div>
          <div class="form-group">
            <input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
          </div>
          <div class="form-group">
            <input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
          </div>
          <button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
          <!-- languages -->
          <div class="btn-group">
            <button type="button" class="btn btn-danger">Langue</button>
            <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
              <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
            </button>
            <ul class="dropdown-menu" role="menu">
              <li><a href="javascript:evts.setLang('fr')">Français</a></li>
              <li><a href="javascript:evts.setLang('en')">English</a></li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
<div class="container">
  <!-- Bootstrap Jumbotron -->
  <div id="jumbotron">
    <div class="jumbotron">
      <div class="row">
        <div class="col-md-2">
          <img src="images/caduceus.jpg" alt="RvMedecins"/>
        </div>
        <div class="col-md-10">
          <h1>
            Cabinet médical<br/>Les Médecins associés
          </h1>
        </div>
      </div>
    </div>
  </div>
  <!-- error panels -->
  <div id="erreur"></div>
  <div id="exception" class="alert alert-danger" style="display: none">
    <h3 id="exception-title"></h3>
    <span id="exception-text"></span>
  </div>
  <!-- content -->
  <div id="content">
    <div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
  </div>
</div>
<!-- init page -->
<script>
  // on initialise la page
  ui.langue = 'fr';
  ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
  ui.exceptionTitle['en'] = "The following server error was met:";
  ui.initNavBarStart();
</script>
</body>
</html>
  • Já nos deparámos com este tipo de página no capítulo sobre o Bootstrap (secção 8.6.4);
  • linhas 99–105: inicialização de determinados elementos da camada [presentation];
  • linha 27: é utilizado o script [getAccueilAvecAgenda-sequence.js]. Ao alterar o script nesta linha, obtemos três comportamentos diferentes para recuperar a página [accueil-avec-agenda]:
    • [getAccueilAvecAgenda-one.js] recupera a página com uma única solicitação HTTP,
    • [getAccueilAvecAgenda-parallel.js] recupera a página com quatro pedidos HTTP simultâneos,
    • [getAccueilAvecAgenda-sequence.js] recupera a página com quatro pedidos HTTP sucessivos;

8.6.8.12. Testes

Existem diferentes formas de executar testes. Aqui, iremos utilizar a ferramenta [Webstorm]:

  • em [1] abrimos um projeto. Basta selecionar a pasta [2] que contém a estrutura de diretórios estáticos (HTML, CSS, JS) do site a ser testado;
  • em [3], o site estático;
  • Em [4-5], carregamos a página [boot.html];
  • em [5], vemos que um servidor incorporado pelo [Webstorm] serviu a página [boot.html] a partir da porta [63342]. Este é um ponto importante a compreender, pois significa que os scripts na página [boot.html] irão efetuar pedidos entre domínios para o servidor [Web1], que está a ser executado em [localhost:8081]. O navegador que carregou [boot.html] sabe que a carregou a partir de [localhost:63342]. Por conseguinte, não permitirá que esta página efetue chamadas para o site [localhost:8081], uma vez que não se trata da mesma porta. Assim, aplicará as regras de domínio cruzado descritas na secção 8.4.14. Por este motivo, a aplicação [Web1] deve ser configurada para aceitar estas solicitações entre domínios. Isto é configurado no ficheiro [AppConfig] do servidor Spring/Thymeleaf:
 

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
 
    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // root web service / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout in milliseconds
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;
...

Deixamos ao leitor a tarefa de testar o cliente JS. Este deverá ser capaz de reproduzir a funcionalidade descrita na secção 8.6.3.

Depois de o cliente JavaScript ter sido validado, pode ser implementado na pasta [Web1] do servidor para evitar ter de permitir pedidos entre domínios:

  

Acima, copiámos o site testado para a pasta [src/main/resources/static]. Depois, podemos aceder ao URL [http://localhost:8081/boot.html]:

Image

Agora já não precisamos de pedidos entre domínios e podemos escrever o seguinte no ficheiro de configuração [AppConfig] do servidor [Web1]:


    // CORS
    private final boolean CORS_ALLOWED=false;

A aplicação acima continuará a funcionar. Se voltarmos à aplicação [WebStorm], esta já não funciona:

Image

Image

Se acedermos à consola do programador (Ctrl-Shift-I), vemos a causa do erro:

Image

Trata-se de um erro de pedido entre domínios não autorizado.

8.6.8.13. Conclusão

Implementámos a seguinte arquitetura JS:

  • as camadas estão bastante bem separadas;
  • temos uma Aplicação de Página Única (SPA). É esta característica que nos permitirá agora gerar uma aplicação nativa para várias plataformas móveis (Android, iOS, Windows Phone);
  • criámos um modelo capaz de executar ações assíncronas em paralelo, sequencialmente ou numa combinação de ambas;

8.6.9. Passo 6: Gerar uma aplicação nativa para Android

A ferramenta [Phonegap] [http://phonegap.com/] permite produzir um executável para dispositivos móveis (Android, iOS, Windows 8, etc.) a partir de uma aplicação HTML/JS/CSS. Existem diferentes formas de o fazer. Vamos utilizar o método mais simples: uma ferramenta online disponível no site do Phonegap [http://build.phonegap.com/apps]. Esta ferramenta irá carregar o ficheiro ZIP do site estático a ser convertido. A página inicial deve ter o nome [index.html]. Por isso, renomeamos a página [boot.html] para [index.html]:

 

depois compactamos a pasta, neste caso [rdvmedecins-client-js-03]. A seguir, acedemos ao site da Phonegap [http://build.phonegap.com/apps]:

  • Antes de [1], poderá ser necessário criar uma conta;
  • em [1], começamos;
  • em [2], escolhemos um plano gratuito que permite apenas uma aplicação Phonegap;
  • em [3], carregamos a aplicação compactada [4];
  • em [5], nomeie a aplicação;
  • em [6], compile-o. Isto pode demorar 1 minuto. Aguarde até que os ícones das várias plataformas móveis indiquem que a compilação está concluída;
  • apenas os binários para Android [7] e Windows [8] foram gerados;
  • Clique em [7] para descarregar o ficheiro binário para Android;
  • em [9], o binário [apk] descarregado;

Inicie um emulador [GenyMotion] para um tablet Android (ver secção 9.9):

 

Acima, iniciamos um emulador de tablet com a API 19 do Android. Assim que o emulador for iniciado,

  • desbloqueie-o arrastando o cadeado (se houver) para o lado e, em seguida, soltando-o;
  • Usando o rato, arraste o ficheiro [PGBuildApp-debug.apk] que descarregou e solte-o no emulador. Este será então instalado e executado;

Tem de alterar o URL para [1]. Para tal, numa janela do prompt de comandos, digite o comando [ipconfig] (linha 1 abaixo), que irá apresentar os vários endereços IP do seu computador:


C:\Users\Serge Tahé>ipconfig
 
Configuration IP de Windows
 
 
Carte réseau sans fil Connexion au réseau local* 15 :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :
 
Carte Ethernet Connexion au réseau local :
 
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
   Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
   Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
   Masque de sous-réseau. . . . . . . . . : 255.255.0.0
   Passerelle par défaut. . . . . . . . . : 172.19.0.254
 
Carte réseau sans fil Wi-Fi :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :
 
...

Anote o endereço IP do Wi-Fi (linhas 6–9) ou o endereço IP da rede local (linhas 11–17). Em seguida, utilize este endereço IP no URL do servidor web:

Depois de fazer isso, conecte-se ao serviço web:

Teste a aplicação no emulador. Deverá funcionar. No lado do servidor, pode ou não permitir cabeçalhos CORS na classe [ApplicationModel]:


    // CORS
    private final boolean CORS_ALLOWED=false;

Isto não é relevante para a aplicação Android. Ela não é executada num navegador. A exigência de cabeçalhos CORS provém do navegador, não do servidor.

8.6.10. Conclusão do estudo de caso

Desenvolvemos a seguinte arquitetura:

Trata-se de uma arquitetura complexa de três camadas. Foi concebida para reutilizar a camada [Web2], que era a camada de servidor da aplicação [AngularJS-Spring MVC] do documento [Tutorial AngularJS / Spring 4] disponível no URL [http://tahe.developpez.com/angularjs-spring4/]. Esta é a única razão pela qual temos uma arquitetura de três camadas. Enquanto na aplicação [AngularJS-Spring MVC] o cliente de [Web2] era um cliente [AngularJS], aqui o cliente de [Web2] é uma arquitetura de duas camadas [jQuery] / [Spring MVC / Thymeleaf]. Aumentámos o número de camadas, pelo que perderemos algum desempenho.

A aplicação aqui discutida foi desenvolvida ao longo do tempo em três documentos diferentes:

  1. [Introdução ao JSF2, PrimeFaces e PrimeFaces Mobile] na URL [http://tahe.developpez.com/java/primefaces/]. O estudo de caso foi então desenvolvido utilizando as estruturas JSF2 / PrimeFaces. O PrimeFaces é uma biblioteca de componentes compatíveis com AJAX que elimina a necessidade de escrever JavaScript. A aplicação desenvolvida na altura era menos complexa do que a aqui estudada. Tinha uma versão web clássica para computadores e uma versão móvel para telemóveis;
  2. [Tutorial AngularJS / Spring 4] na URL [http://tahe.developpez.com/angularjs-spring4/]. A aplicação desenvolvida na altura tinha as mesmas funcionalidades que a discutida neste documento. A aplicação também tinha sido portada para Android;
  3. este documento;

Deste trabalho, destacam-se para mim os seguintes pontos:

  • a aplicação [Primefaces] foi, de longe, a mais simples de escrever, e a sua versão web móvel revelou-se de alto desempenho. Não requer qualquer conhecimento de JavaScript. Não é possível portá-la nativamente para os sistemas operativos de vários dispositivos móveis, mas será isso necessário? Parece difícil alterar o estilo da aplicação. Estamos, de facto, a trabalhar com folhas de estilo Primefaces. Isto pode ser uma desvantagem;
  • A aplicação [AngularJS-Spring MVC] foi complexa de escrever. A estrutura [AngularJS] pareceu bastante difícil de compreender quando se pretende dominá-la. A arquitetura [cliente Angular] / [serviço web / JSON implementado pelo Spring MVC] é particularmente limpa e de alto desempenho. Esta arquitetura é replicável para qualquer aplicação web. É a arquitetura que me parece mais promissora, porque envolve conjuntos de competências diferentes nos lados do cliente e do servidor (JS+HTML+CSS no lado do cliente, Java ou outra linguagem no lado do servidor), o que permite que o cliente e o servidor sejam desenvolvidos em paralelo;
  • Para a aplicação desenvolvida neste documento utilizando uma arquitetura de 3 camadas [cliente jQuery] / [servidor Web1 / Spring MVC / Thymeleaf] / [servidor Web2 / Spring MVC], alguns poderão achar a tecnologia [jQuery+Spring MVC+Thymeleaf] mais fácil de compreender do que a do [AngularJS]. A camada [DAO] do cliente JavaScript que escrevemos é reutilizável noutras aplicações;