Skip to content

8. Estudo de caso

8.1. Introduction

Propomos desenvolver um aplicativo web para agendamento de consultas em um consultório médico. Esse problema foi abordado no documento “Tutorial AngularJS / Spring 4”, nas seções URL e [http://tahe.developpez.com/angularjs-spring4/]. A arquitetura desse aplicativo era a seguinte:

  • no [1], um servidor web fornece páginas estáticas a um navegador. Essas páginas contêm uma aplicação AngularJS construída com base no modelo MVC (Modelo – Visão – Controlador). O modelo, neste caso, é tanto o das visualizações quanto o do domínio, representado aqui pela camada [Services];
  • o usuário irá interagir com as visualizações que lhe são apresentadas no navegador. Suas ações, por vezes, exigirão uma consulta ao servidor Spring 4 [2]. Este processará a solicitação e retornará uma resposta jSON (JavaScript Object Notation) [3]. Essa resposta será utilizada para atualizar a vista apresentada ao usuário.

Propomos retomar essa aplicação e implementá-la de ponta a ponta com o Spring MVC. A arquitetura passa a ser a seguinte:

O navegador se conectará a uma aplicação [Web 1] implementada pelo Spring MVC, que buscará seus dados em um serviço web [Web 2], também implementado com o Spring MVC.

8.2. Funcionalidades da aplicação

Convidamos o leitor a descobrir as funcionalidades da aplicação testando-a. Carregamos no STS os projetos Maven da pasta [etude-de-cas]:

Primeiramente, vamos criar o banco de dados MySQL 5 [dbrdvmedecins] com a ferramenta [Wamp Server] (ver parágrafo 9.5):

  • no [1], seleciona-se a ferramenta [phpMyAdmin] a partir do WampServer;
  • em [2], escolhe-se a opção [Importer];
  • no [3], seleciona-se o arquivo [database/dbrdvmedecins.sql];
  • no [4], execute-o;
  • em [5], o banco de dados é criado.

Em seguida, precisamos iniciar o servidor conectado ao banco de dados. Esse é o projeto [rdvmedecins-webjson-server]

O servidor ficará disponível nos arquivos URL e [http://localhost:8080]. Isso pode ser alterado no arquivo [application.properties] do projeto:

  

server.port=8080

As características de acesso ao banco de dados estão registradas na classe [DomainAndPersistenceConfig] do projeto [rdvmedecins-metier-dao]:

  

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

Se você acessar o SGBD MySQL com outros identificadores, é aqui que tudo acontece.

Em seguida, da mesma forma que no servidor anterior, iniciamos o servidor [rdvmedecins-springthymeleaf-server]:

 

Esse servidor está disponível por padrão nos endereços URL e [http://localhost:8081]. Mais uma vez, isso pode ser configurado no arquivo [application.properties] do projeto:


server.port=8081

Além disso, esse servidor precisa conhecer o URL do servidor conectado ao banco de dados. Essa configuração está na classe [AppConfig] acima:


    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // raiz do serviço web / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // tempo limite em milissegundos
    private final int TIMEOUT = 5000;
    // CORS
private final boolean CORS_ALLOWED=true;

Se o primeiro servidor tiver sido iniciado em uma porta diferente da 8080, é necessário alterar a linha 5.

Em seguida, usando um navegador, acesse o URL e o [http://localhost:8081/boot.html]:

  • em [1], a página de login do aplicativo;
  • em [2] e [3], o nome de usuário e a senha de quem deseja utilizar o aplicativo. Existem dois usuários: admin/admin (login/senha) com uma função (ADMIN) e user/user com uma função (USER). Apenas a função ADMIN tem permissão para usar o aplicativo. A função USER existe apenas para mostrar a resposta do servidor nesse caso de uso;
  • em [4], o botão que permite conectar-se ao servidor;
  • em [5], o idioma do aplicativo. Há dois: o francês, por padrão, e o inglês;
  • em [6], o URL do servidor [rdvmedecins-springthymeleaf-server];
  • no [1], faz-se o login;
  • uma vez conectado, é possível escolher o médico com quem deseja marcar uma consulta [2] e o dia da consulta [3]. Assim que o médico e o dia forem informados, a agenda é exibida automaticamente:
  • Depois de acessar a agenda do médico, é possível reservar um horário [5];
  • em [6], seleciona-se o paciente para a consulta e confirma-se essa escolha em [7];

Assim que a consulta for confirmada, o usuário é redirecionado automaticamente para a agenda, onde a nova consulta já está registrada. Essa consulta poderá ser excluída posteriormente em [8].

As principais funcionalidades já foram descritas. Elas são simples. Vamos encerrar com o gerenciamento do idioma:

1

Image

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

8.3. O banco de dados

O banco de dados, doravante denominado [dbrdvmedecins], é um banco de dados MySQL5 com as seguintes tabelas:

  

As consultas são gerenciadas pelas seguintes tabelas:

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

As tabelas [roles], [users] e [users_roles] são tabelas relacionadas à autenticação. Por enquanto, não vamos nos preocupar com elas. As relações entre as tabelas que gerenciam 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 por meio de um horário disponível deste último;
  • um cliente tem 0 ou mais consultas;
  • a um horário estão associadas 0 ou mais consultas (em dias diferentes).

8.3.1. A tabela [MEDECINS]

Ela contém informações sobre os médicos gerenciados pelo aplicativo [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. Esse número é incrementado em 1 sempre que uma alteração é feita na linha.
  • NOM: o sobrenome do médico
  • PRENOM: seu nome
  • TITRE: seu título (Srta., Sra., Sr.)

8.3.2. A tabela [CLIENTS]

Os clientes dos diferentes médicos estão registrados 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. Esse número é incrementado em 1 sempre que uma modificação é feita na linha.
  • NOM: o nome do cliente
  • PRENOM: seu nome
  • TITRE: seu título (Srta., Sra., Sr.)

8.3.3. A tabela [CRENEAUX]

Ela lista os horários em que os RV são possíveis:

  • ID: número que identifica o intervalo horário — chave primária da tabela (linha 8)
  • VERSION: número que identifica a versão da linha na tabela. Esse número é incrementado em 1 sempre que uma alteração é feita na linha.
  • ID_MEDECIN: número que identifica o médico ao qual esse horário pertence – chave estrangeira na coluna MEDECINS (ID).
  • HDEBUT: hora de início do horário
  • MDEBUT: minutos de início do horário
  • HFIN: hora de término do horário
  • MFIN: minutos de término do intervalo

A segunda linha da tabela [CRENEAUX] (ver [1] acima) indica, por exemplo, que o horário 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]

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

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

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

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

Se uma linha da tabela [RV] tiver o valor (JOUR1, ID_CRENEAU1) para as colunas (JOUR, ID_CRENEAU), esse valor não pode aparecer em nenhum outro lugar. Caso contrário, isso significaria que dois RV foram registrados ao mesmo tempo para o mesmo médico. Do ponto de vista da programação em Java, o driver JDBC do banco de dados aciona um SQLException quando esse caso ocorre.

A linha de id igual a 3 (ver [1] acima) significa que um RV foi agendado para o horário nº 20 e o cliente nº 4 em 23/08/2006. A tabela [CRENEAUX] nos informa que o horário n.º 20 corresponde ao intervalo das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] nos informa que o cliente nº 4 é a Srta. Brigitte BISTROU.

8.3.5. Criação do banco de dados

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

Utilizamos a ferramenta [PhpMyAdmin] do WampServer:

  • no [1], seleciona-se a ferramenta [phpMyAdmin] do WampServer;
  • em [2], escolhe-se a opção [Importer];
  • no [3], seleciona-se o arquivo [database/dbrdvmedecins.sql];
  • no [4], execute-o;
  • em [5], o banco de dados é criado.

8.4. O serviço web / jSON

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

  • primeiro, as camadas [métier] e [DAO] (Data Access Object). Aqui, utilizaremos o Spring Data;
  • depois, o serviço web jSON sem autenticação. Aqui, utilizaremos o Spring MVC;
  • em seguida, adicionaremos a parte de autenticação com 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

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

No site do Spring, há diversos tutoriais para começar a usar o Spring [http://spring.io/guides]. Vamos utilizar um deles para apresentar o Spring Data. Para isso, usaremos o Spring Tool Suite (STS).

  • em [1], importamos um dos tutoriais de [spring.io/guides];
  • no [2], selecionamos o tutorial [Accessing Data Jpa], que mostra como acessar um banco de dados com o Spring Data;
  • em [3], escolhe-se um projeto configurado pelo Maven;
  • em [4], o tutorial pode ser fornecido de duas formas: [initial], que é uma versão vazia a ser preenchida seguindo o tutorial, ou [complete], que é a versão final do tutorial. Escolhemos esta última;
  • em [5], é possível optar por visualizar o tutorial em um navegador;
  • em [6], o projeto final.

8.4.1.1. A configuração do Maven para o projeto

As dependências do Maven do projeto são configuradas no arquivo [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 para tudo -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • linhas 5-9: definem um projeto pai do Maven. É ele que define a maior parte das dependências do projeto. Elas podem ser suficientes, caso em que não se adicionam mais, ou não, caso em que se adicionam as dependências que faltam;
  • linhas 12-15: definem uma dependência do [spring-boot-starter-data-jpa]. Esse artefato contém as classes do Spring Data;
  • linhas 16-19: definem uma dependência do SGBD e do H2, que permitem criar e gerenciar bancos de dados em memória.

Vejamos as classes fornecidas por essas dependências:

São muitas:

  • algumas pertencem ao ecossistema Spring (aquelas que começam com “spring”);
  • outras pertencem ao ecossistema Hibernate (hibernate, jboss), cuja implementação JPA é utilizada aqui;
  • outras são bibliotecas de testes (junit, hamcrest);
  • outras são bibliotecas de logs (log4j, logback, slf4j);

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

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


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

Essa linha está relacionada às seguintes:


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

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

8.4.1.2. A camada [JPA]

O acesso ao banco de dados é feito por meio de uma camada [JPA], Java Persistence API:

  

A aplicação é básica e gerencia clientes [Customer]. A classe [Customer] faz parte da camada [JPA] e é a seguinte:


package hello;

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

@Entity
public class Customer {

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

    protected Customer() {
    }

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

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

}

Um cliente possui um identificador [id], um nome próprio [firstName] e um sobrenome [lastName]. Cada instância [Customer] representa uma linha de uma tabela do banco de dados.

  • linha 8: anotação JPA que faz com que a persistência das instâncias [Customer] (Create, Read, Update, Delete) seja gerenciada por uma implementação JPA. De acordo com as dependências do Maven, percebe-se que é utilizada a implementação JPA / Hibernate;
  • linhas 11-12: anotações JPA que associam o campo [id] à chave primária da tabela [Customer]. A linha 12 indica que a implementação JPA utilizará o método de geração de chave primária específico do SGBD utilizado, neste caso, o H2;

Não há outras anotações para JPA. Nesse caso, serão utilizados valores padrão:

  • a tabela [Customer] terá o nome da classe, ou seja, [Customer];
  • as colunas dessa tabela terão os nomes dos campos da classe: [id, firstName, lastName], considerando que a distinção entre maiúsculas e minúsculas não é levada em conta no nome de uma coluna de tabela;

Observe-se que, em nenhum momento, a implementação JPA utilizada é nomeada.

8.4.1.3. A camada [DAO]

  

A classe [CustomerRepository] implementa a camada [DAO]. 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). Essa interface é parametrizada por dois tipos: o primeiro é o tipo dos elementos gerenciados, neste caso o tipo [Customer]; o segundo é o tipo da chave primária dos elementos gerenciados, 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();
}

Essa interface define as operações CRUD (Criar – Ler – Atualizar – Excluir) que podem ser realizadas em um tipo JPA T:

  • linha 8: o método `save` permite persistir uma entidade `T` no banco de dados. Ele torna a entidade persistida com a chave primária que lhe foi atribuída pelo SGBD. Ele também permite atualizar uma entidade `T` identificada por sua chave primária id. A escolha entre uma ou outra ação depende do valor da chave primária id: se este for nulo, ocorre a operação de persistência; caso contrário, ocorre a operação de atualização;
  • linha 10: o mesmo, mas para uma lista de entidades;
  • linha 12: o método findOne permite recuperar uma entidade T identificada por sua chave primária id;
  • linha 22: o método delete permite excluir uma entidade T identificada por sua chave primária id;
  • linhas 24-28: variantes do método [delete];
  • linha 16: o método [findAll] permite recuperar todas as entidades T persistentes;
  • linha 18: o mesmo, mas limitado às entidades cuja lista de identificadores foi passada;

Voltemos à interface [CustomerRepository]:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • a linha 9 permite recuperar um [Customer] pelo seu nome [lastName];

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


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

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

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

Portanto, é necessário que o tipo T possua um campo chamado [something]. Assim, o método

List<Customer> findByLastName(String lastName);

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

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

onde [em] designa o contexto de persistência JPA. Isso só é possível se a classe [Customer] tiver um campo chamado [lastName], o que de fato ocorre.

Concluindo, em casos simples, o Spring Data nos permite 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);

        // salvar alguns clientes
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

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

        // buscar um cliente específico por ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // buscar clientes pelo sobrenome
        List<Customer> bauers = repository.findByLastName("Bauer");
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : bauers) {
            System.out.println(bauer);
        }

        context.close();
    }

}
  • na linha 10: indica que a classe serve para configurar o Spring. As versões recentes do Spring podem, de fato, ser configuradas em Java em vez de em XML. Os dois métodos podem ser usados simultaneamente. No código de uma classe com a anotação [Configuration], normalmente encontram-se beans do Spring, ou seja, definições de classes a serem instanciadas. Aqui, nenhum bean está definido. É importante lembrar que, ao trabalhar com um SGBD, vários beans do Spring devem ser definidos:
    • um [EntityManagerFactory] que define a implementação JPA a ser utilizada,
    • um [DataSource] que define a fonte de dados a ser utilizada,
    • um [TransactionManager] que define o gerenciador de transações a ser utilizado;

Aqui, nenhum desses beans está definido.

  • na linha 11: a anotação [EnableAutoConfiguration] é uma anotação proveniente do projeto [Spring Boot] (linhas 5-6). Essa anotação solicita ao Spring Boot, por meio da classe [SpringApplication] (linha 16), que configure a aplicação de acordo com as bibliotecas encontradas em seu Classpath. Como as bibliotecas do Hibernate estão no Classpath, o bean [entityManagerFactory] será implementado com o Hibernate. Como a biblioteca SGBD H2 está no Classpath, o bean [dataSource] será implementado com H2. No bean [dataSource], também é necessário definir o usuário e sua senha. Aqui, o Spring Boot utilizará o administrador padrão de H2, que não possui senha. Como a biblioteca [spring-tx] está no Classpath, será utilizado o gerenciador de transações do Spring.

Além disso, a pasta na qual se encontra a classe [Application] será verificada 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], ela será catalogada como uma entidade a ser gerenciada pelo Hibernate. Como a segunda estende a interface [CrudRepository], ela será registrada como um bean do Spring.

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


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

As operações a seguir utilizam apenas os métodos do bean que implementa a interface [CustomerRepository]. Observe, na linha 50, que o contexto é fechado. Os resultados exibidos no console são os seguintes:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 logotipo do projeto Spring Boot;
  • linha 9: a classe [hello.Application] é executada;
  • linha 10: [AnnotationConfigApplicationContext] é uma classe que implementa a interface [ApplicationContext] do Spring. Trata-se de um contêiner de beans;
  • linha 11: o bean [entityManagerFactory] é implementado pela classe [LocalContainerEntityManagerFactory], uma classe do Spring;
  • linha 15: surge o [Hibernate]. Foi essa implementação, JPA, que foi escolhida;
  • linha 19: um dialeto do Hibernate é a variante SQL a ser usada com o SGBD. Aqui, o dialeto [H2Dialect] indica que o Hibernate trabalhará com o SGBD e o H2;
  • linhas 21-22: o banco de dados é criado. A tabela [CUSTOMER] é criada. Isso significa que o Hibernate foi configurado para gerar as tabelas a partir das definições JPA; neste caso, a definição JPA da classe [Customer];
  • linhas 27-31: os cinco clientes inseridos;
  • linhas 33635: resultado do método [findOne] da interface;
  • linhas 37-40: resultados do método [findByLastName];
  • linhas 41 e seguintes: logs do fechamento do contexto Spring.

8.4.1.5. Configuração manual do projeto Spring Data

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

  

Neste novo projeto, não vamos utilizar a configuração automática feita pelo Spring Boot. Vamos fazê-la manualmente. Isso pode ser útil caso as configurações padrão não sejam adequadas para nós.

Primeiramente, vamos especificar as dependências necessárias no arquivo [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>
        <!-- Transações do Spring -->
        <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>
        <!-- Banco de dados H2 -->
        <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: as bibliotecas básicas do Spring;
  • linhas 19-29: as bibliotecas do Spring para gerenciar transações com um banco de dados;
  • linhas 30-35: a biblioteca do Spring para trabalhar com um ORM (Mapeador Objeto-Relacional);
  • linhas 36-41: Spring Data, usado para acessar o banco de dados;
  • linhas 42-47: o Spring Boot para iniciar a aplicação;
  • linhas 54-59: o SGBD H2;
  • linhas 60-70: os bancos de dados são frequentemente utilizados com pools de conexões abertas, o que evita a abertura e o fechamento repetidos de conexões. Aqui, a implementação utilizada é a do [commons-dbcp];

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


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

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

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

A classe executável [Main] é a mesma 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] não possui mais anotações de configuração;
  • linha 16: o aplicativo é iniciado com o Spring Boot. O parâmetro [Config.class] é a nova classe de configuração do projeto;

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


package demo.config;

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

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

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

    // o provedor 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();
    }

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

}
  • linha 22: a anotação [@Configuration] transforma a classe [Config] em uma classe de configuração do Spring;
  • linha 21: a anotação [@EnableJpaRepositories] permite indicar as pastas onde se encontram as interfaces Spring Data [CrudRepository]. Essas interfaces se tornarão componentes Spring e estarão disponíveis em 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] permite especificar as pastas onde as entidades JPA devem ser procuradas. Aqui, ela foi colocada entre comentários, pois essa informação já foi fornecida explicitamente na linha 50. Essa anotação deve estar presente se for utilizado o modo [@EnableAutoConfiguration] e as entidades JPA não estiverem na mesma pasta que a classe de configuração;
  • linha 18: a anotação [@ComponentScan] permite listar as pastas onde os componentes Spring devem ser procurados. Os componentes Spring são classes marcadas com anotações Spring, como @Service, @Component, @Controller, etc. Aqui, não há outros além daqueles definidos na classe [Config]; por isso, a anotação foi colocada entre comentários;
  • linhas 25-33: definem a fonte de dados, o banco de dados H2. É a anotação @Bean da linha 25 que torna o objeto criado por esse método um componente gerenciado pelo Spring. O nome do método pode ser qualquer um aqui. No entanto, ele deve ser chamado de [dataSource] se o EntityManagerFactory da linha 47 estiver ausente e for definido por autoconfiguração;
  • linha 29: o banco de dados se chamará [demo] e será gerado na pasta do projeto;
  • linhas 36-43: definem a implementação JPA utilizada, neste caso, uma implementação do Hibernate. O nome do método pode ser qualquer um;
  • linha 39: sem logs SQL;
  • linha 30: o banco de dados será criado caso não exista;
  • linhas 46-54: definem o EntityManagerFactory que irá gerenciar a persistência do JPA. O método deve se chamar obrigatoriamente [entityManagerFactory];
  • linha 47: o método recebe dois parâmetros com o tipo dos dois beans definidos anteriormente. Estes serão então instanciados e injetados pelo Spring como parâmetros do método;
  • linha 49: define a implementação JPA a ser utilizada;
  • linha 50: define as pastas onde se encontram as entidades JPA;
  • linha 51: define a fonte de dados a ser gerenciada;
  • linhas 57-62: o gerenciador de transações. O método deve se chamar obrigatoriamente [transactionManager]. Ele recebe como parâmetro o bean das linhas 46-54;
  • linha 60: o gerenciador 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 arquivo aparece na pasta do projeto, o arquivo do banco de dados H2:

  

Por fim, é possível dispensar o Spring Boot. Criamos uma segunda classe executável, [Main2]:

  

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


package demo.console;

import java.util.List;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class Main2 {

    public static void main(String[] args) {

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

        context.close();
    }

}
  • linha 15: a classe de configuração [Config] agora é utilizada pela classe Spring [AnnotationConfigApplicationContext]. Pode-se observar na linha 5 que agora não há mais dependências em relação ao Spring Boot.

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

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

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

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

Feito isso, abre-se um terminal na pasta que contém o arquivo executável:

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

O arquivo é executado da seguinte maneira:


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

Os resultados obtidos no terminal são os seguintes:

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

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

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

8.4.1.7. Criar um novo projeto Spring Data

Para criar um esqueleto de projeto Spring Data, pode-se proceder da seguinte maneira:

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

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


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath/> <!-- pesquisar pai no repositório -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
  • linhas 9-12: as dependências necessárias para o JPA – incluirão o [Spring Data];
  • linhas 13-17: as dependências necessárias para os testes JUnit integrados com o Spring;

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


package istia.st;

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

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {

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

A classe de testes [ApplicationTests] não executa nenhuma ação, mas está pré-configurada:


package istia.st;

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

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

    @Test
    public void contextLoads() {
    }

}
  • linha 9: a anotação [@SpringApplicationConfiguration] permite utilizar o arquivo de configuração [Application]. Assim, a classe de teste terá acesso a todos os beans definidos nesse arquivo;
  • linha 8: a anotação [@RunWith] permite a integração do Spring com JUnit: a classe poderá ser executada como um teste JUnit. [@RunWith] é uma anotação JUnit (linha 4), enquanto a classe [SpringJUnit4ClassRunner] é uma classe Spring (linha 6);

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

8.4.2. O projeto Eclipse do servidor

  

Os principais elementos do projeto são os seguintes:

  • [pom.xml]: arquivo de configuração Maven do projeto;
  • [rdvmedecins.entities]: as entidades JPA;
  • [rdvmedecins.repositories]: as interfaces Spring Data para acesso às entidades JPA;
  • [rdvmedecins.metier]: a camada [métier];
  • [rdvmedecins.domain]: as entidades manipuladas pela camada [métier];
  • [rdvmdecins.config]: as classes de configuração da camada de persistência;
  • [rdvmedecins.boot]: um aplicativo de console básico;

8.4.3. A configuração do Maven

O arquivo [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>
                <!-- teste do Spring -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <!-- Segurança Spring -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-security</artifactId>
                </dependency>
                <!-- piloto 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>
                <!-- Mapeador jSON -->
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-databind</artifactId>
                </dependency>
                <!-- Google Guava -->
                <dependency>
                        <groupId>com.google.guava</groupId>
                        <artifactId>guava</artifactId>
                        <version>16.0.1</version>
                </dependency>
        </dependencies>
        <properties>
                <!-- use UTF-8 para tudo -->
                <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 se baseia no projeto pai [spring-boot-starter-parent]. Para as dependências já presentes no projeto pai, não se especifica a versão. Será utilizada a versão definida no projeto pai. As demais dependências são declaradas normalmente;
  • linhas 15-18: para o Spring Data;
  • linhas 20-24: para os testes JUnit;
  • linhas 26-29: para a biblioteca Spring Security, cuja camada [DAO] utiliza uma das classes de criptografia de senhas;
  • linhas 31-34: driver JDBC do SGBD MySQL5;
  • linhas 36-39: pool de conexões do Tomcat JDBC. Um pool de conexões reúne conexões abertas com um banco de dados. Quando o código deseja abrir uma conexão, ela é solicitada ao pool. Quando o código fecha a conexão, ela não é encerrada, mas devolvida ao pool. Tudo isso ocorre de forma transparente para o código. Há um ganho de desempenho, pois a abertura e o fechamento repetidos de uma conexão acarretam um custo em termos de tempo. Aqui, o pool de conexões estabelece um determinado número de conexões com o banco de dados assim que é instanciado. Depois disso, não há abertura nem fechamento de conexões, a menos que o número de conexões armazenadas no pool se mostre insuficiente. Nesse caso, o pool cria automaticamente novas conexões;
  • linhas 41-44: biblioteca Jackson para gerenciamento do jSON;
  • linhas 46-50: biblioteca do Google para gerenciamento de coleções;

8.4.4. As entidades JPA

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

  

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

    // inicialização
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }

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


    // getters e setters
    ..
}
  • linha 11: a anotação [@MappedSuperclass] indica que a classe anotada é pai das entidades JPA e [@Entity];
  • linhas 15-17: definem a chave primária [id] de cada entidade. É a anotação [@Id] que torna o campo [id] uma chave primária. A anotação [@GeneratedValue(strategy = GenerationType.IDENTITY)] indica que o valor dessa chave primária é gerado pelo SGBD e que o modo de geração [IDENTITY] é imposto. Para o SGBD MySQL, isso 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á esse número de versão sempre que a entidade for modificada. Esse número serve para impedir a atualização simultânea da entidade por dois usuários diferentes: dois usuários, U1 e U2, leem a entidade E com um número de versão igual a V1. U1 modifica E e grava essa modificação no banco de dados: o número de versão passa então para V1+1. U2, por sua vez, modifica E e grava essa modificação no banco de dados: ele receberá uma exceção, pois possui uma versão (V1) diferente daquela no banco de dados (V1+1);
  • linhas 29-33: o método [build] permite inicializar os dois campos de [AbstractEntity]. Esse método torna a referência da instância [AbstractEntity] assim inicializada;
  • linhas 36-44: o método [equals] da classe é redefinido: duas entidades serão consideradas iguais se tiverem o mesmo nome de classe e o mesmo identificador id;
  • linhas 21-26: ao redefinir o método [equals] de uma classe, é necessário redefinir também seu método [hashCode] (linhas 21-26). A regra é que duas entidades consideradas iguais pelo método [equals] devem, portanto, ter o mesmo [hashCode]. Aqui, o [hashCode] de uma entidade é igual à sua chave primária [id]. O [hashCode] de uma classe é utilizado, notadamente, no gerenciamento de dicionários cujos valores são instâncias da classe;

A entidade [Personne] é a classe pai das entidades [Medecin] e [Client]:


package rdvmedecins.entities;

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

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

    // construtor padrão
    public Personne() {
    }

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

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

    // getters e setters
    ...
}
  • linha 6: a anotação [@MappedSuperclass] indica que a classe anotada é pai das entidades JPA e [@Entity];
  • linhas 10-15: uma pessoa possui um título (Srta.), um nome próprio (Jacqueline) e um sobrenome (Tatou). Não há informações sobre as colunas da tabela. Portanto, por padrão, elas terão os mesmos nomes dos campos;

A entidade [Medecin] é a seguinte:


package rdvmedecins.entities;

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

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

    private static final long serialVersionUID = 1L;

    // construtor padrão
    public Medecin() {
    }

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

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

}
  • linha 6: a classe é uma entidade JPA;
  • linha 7: associada à tabela [MEDECINS] do banco de dados;
  • linha 8: a entidade [Medecin] deriva da entidade [Personne];

Um médico poderá ser inicializado da seguinte forma:

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

Além disso, se quisermos atribuir a ele um identificador e uma versão, poderemos 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;

    // construtor padrão
    public Client() {
    }

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

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

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

A entidade [Creneau] é a seguinte:


package rdvmedecins.entities;

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

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

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

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

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

    // fabricante padrão
    public Creneau() {
    }

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

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

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

    // setter e getter
    ...
}
  • linha 10: a classe é uma entidade JPA;
  • linha 11: associada à tabela [CRENEAUX] do banco de dados;
  • linha 12: a entidade [Creneau] deriva da entidade [AbstractEntity] e, portanto, herda o identificador [id] e a versão [version];
  • linha 16: hora de início do intervalo (14);
  • linha 17: minutos de início do intervalo (20);
  • linha 18: hora de término do intervalo (14);
  • linha 19: minutos de término do horário (40);
  • linhas 22-24: o médico responsável pelo horário. A tabela [CRENEAUX] possui uma chave estrangeira na tabela [MEDECINS]. Essa relação é representada pelas linhas 22-24;
  • linha 22: a anotação [@ManyToOne] indica uma relação de muitos (horários) para um (médico). O atributo [fetch=FetchType.LAZY] indica que, quando se solicita uma entidade [Creneau] ao contexto de persistência e esta precisa ser buscada no banco de dados, a entidade [Medecin] não é retornada junto com ela. A vantagem desse modo é que a entidade [Medecin] só é pesquisada se o desenvolvedor solicitar. Assim, economiza-se memória e ganha-se em desempenho;
  • linha 23: indica o nome da coluna-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. Isso significa que ela pode ser modificada por dois caminhos diferentes, o que não é permitido pela norma JPA. Portanto, adicionamos os atributos [insertable = false, updatable = false], o que faz com que a coluna só possa ser lida;

A entidade [Rv] é a seguinte:


package rdvmedecins.entities;

import java.util.Date;

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

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

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

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

    // um RV está vinculado a um horário
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

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

    // fabricante padrão
    public Rv() {
    }

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

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

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

    public long getIdClient() {
        return idClient;
    }

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

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

  

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

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

A interface [MedecinRepository] é a seguinte:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Medecin;

public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
  • linha 7: a interface [MedecinRepository] limita-se a herdar os métodos da interface [CrudRepository], sem adicionar outros;

A interface [ClientRepository] é a seguinte:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Client;

public interface ClientRepository extends CrudRepository<Client, Long> {
}
  • linha 7: a interface [ClientRepository] limita-se a herdar os métodos da interface [CrudRepository], sem adicionar outros;

A interface [CreneauRepository] é a seguinte:


package rdvmedecins.repositories;

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

import rdvmedecins.entities.Creneau;

public interface CreneauRepository extends CrudRepository<Creneau, Long> {
    // lista de horários de atendimento de um médico
    @Query("select c from Creneau c where c.medecin.id=?1")
    Iterable<Creneau> getAllCreneaux(long idMedecin);
}
  • linha 8: a interface [CreneauRepository] herda os métodos da interface [CrudRepository];
  • linhas 10-11: o método [getAllCreneaux] permite obter os horários disponíveis de um médico;
  • linha 11: o parâmetro é o identificador do médico. O resultado é uma lista de horários disponíveis na forma de um objeto [Iterable<Creneau>];
  • linha 10: a anotação [@Query] permite especificar a consulta JPQL (Java Persistence Query Language) que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método;

A interface [RvRepository] é a seguinte:


package rdvmedecins.repositories;

import java.util.Date;

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

import rdvmedecins.entities.Rv;

public interface RvRepository extends CrudRepository<Rv, Long> {

    @Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
    Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
  • linha 10: a interface [RvRepository] herda os métodos da interface [CrudRepository];
  • linhas 12-13: o método [getRvMedecinJour] permite obter as consultas de um médico para um determinado dia;
  • linha 13: os parâmetros são o identificador do médico e o dia. O resultado é uma lista de consultas na forma de um objeto [Iterable<Rv>];
  • linha 12: a anotação [@Query] permite especificar a consulta JPQL que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método, e o parâmetro [?2] será substituído pelo parâmetro [jour] do método. Não basta utilizar a seguinte consulta JPQL:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

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

8.4.6. A camada [métier]

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

8.4.6.1. As entidades

A entidade [CreneauMedecinJour] associa um intervalo de tempo e o possível compromisso agendado nesse intervalo:


package rdvmedecins.domain;

import java.io.Serializable;

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

public class CreneauMedecinJour implements Serializable {

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

    // construtores
    public CreneauMedecinJour() {

    }

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

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

    // getters e setters
...
}
  • linha 12: o intervalo horário;
  • linha 13: a eventual consulta – null caso contrário;

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


package rdvmedecins.domain;

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

import rdvmedecins.entities.Medecin;

public class AgendaMedecinJour implements Serializable {

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

    // construtores
    public AgendaMedecinJour() {

    }

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

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

    // getters e setters
...
}
  • linha 13: o médico;
  • linha 14: o dia na agenda;
  • linha 15: seus horários disponíveis, com ou sem agendamento;

8.4.6.2. O serviço

A interface da camada [métier] é a seguinte:


package rdvmedecins.metier;

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

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

public interface IMetier {

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

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

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

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

    // localizar um cliente identificado por seu ID
    public Client getClientById(long id);

    // localizar um cliente identificado por seu ID
    public Medecin getMedecinById(long id);

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

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

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

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

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

}

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

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


package rdvmedecins.metier;

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

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

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

import com.google.common.collect.Lists;

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

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

    // implementação da interface
    @Override
    public List<Client> getAllClients() {
        return Lists.newArrayList(clientRepository.findAll());
    }

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

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

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

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

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

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

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

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

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

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

}
  • linha 24: a anotação [@Service] é uma anotação do Spring que transforma a classe anotada em um componente gerenciado pelo Spring. É possível ou não atribuir um nome a um componente. Este foi denominado [métier];
  • linha 25: a classe [Metier] implementa a interface [IMetier];
  • linha 28: a anotação [@Autowired] é uma anotação do Spring. O valor do campo assim anotado será inicializado (injetado) pelo Spring com a referência de um componente do Spring do tipo ou nome especificados. Aqui, a anotação [@Autowired] não especifica um nome. Portanto, será realizada uma injeção por tipo;
  • linha 29: o campo [medecinRepository] será inicializado com a referência a um componente Spring do tipo [MedecinRepository]. Essa será a referência à classe gerada pelo Spring Data para implementar a interface [MedecinRepository] que já apresentamos;
  • linhas 30-35: esse processo é repetido para as outras três interfaces analisadas;
  • linhas 39-41: implementação do método [getAllClients];
  • linha 40: utilizamos o método [findAll] da interface [ClientRepository]. Esse método retorna um tipo [Iterable<Client>], que transformamos em [List<Client>] com o método estático [Lists.newArrayList]. A classe [Lists] está definida na biblioteca Google Guava. Em [pom.xml], essa dependência foi importada:

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

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


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

Recomenda-se ao leitor que leia os comentários. O algoritmo é o seguinte:

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

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 {

    // pacotes de entidades JPA
    public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };

    // a fonte de dados MySQL
    @Bean
    public DataSource dataSource() {
        // fonte de dados TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuração JDBC
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        // conexões abertas inicialmente
        dataSource.setInitialSize(5);
        // resultado
        return dataSource;
    }

    // o provedor JPA é o 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();
    }

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

}
  • linha 17: a classe é uma classe de configuração do Spring;
  • linha 18: os pacotes onde se encontram as interfaces [CrudRepository] do Spring Data. Estas serão adicionadas ao contexto do Spring;
  • linha 19: adiciona ao contexto do Spring todas as classes do pacote [rdvmedecins] e suas descendentes que possuam uma anotação do Spring. No pacote [rdvmdecins.metier], a classe [Metier], com sua anotação [@Service], será localizada e adicionada ao contexto do Spring;
  • linhas 26-39: configuram o pool de conexões do Tomcat JDBC (linha 5);
  • linha 36: o pool de conexões terá, por padrão, 5 conexões abertas. Essa linha é mostrada apenas a título de exemplo. No nosso caso, uma conexão seria suficiente. Caso a camada [DAO] fosse utilizada por várias threads, essa linha seria necessária. Esse será o caso posteriormente, quando a camada [DAO] servir de suporte a uma aplicação web que, por natureza, suporta vários usuários atendidos simultaneamente;
  • linhas 42-49: a implementação JPA utilizada é uma implementação do Hibernate;
  • linha 45: não há logs SQL;
  • linha 46: não há regeneração das tabelas;
  • linha 47: o SGBD utilizado é o MySQL;
  • linhas 53-61: definem o EntityManagerFactory da camada JPA. A partir desse objeto, obtém-se o objeto [EntityManager], que permite realizar as operações JPA;
  • linha 57: indica o(s) pacote(s) onde se encontram as entidades JPA;
  • linha 58: indica a fonte de dados a ser conectada à camada JPA;
  • linhas 64-69: o gerenciador de transações associado ao EntityManagerFactory anterior. Por padrão, os métodos das interfaces [CrudRepository] do Spring Data são executados dentro de uma transação. A transação é iniciada antes da entrada no método e é encerrada (por meio de um commit ou rollback) após a saída do mesmo;

8.4.8. Os testes da camada [métier]

  

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


package rdvmedecins.tests;

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

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

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

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

    @Autowired
    private IMetier métier;

    @Test
    public void test1(){
        // exibição de clientes
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // exibição de médicos
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // visualização dos horários de atendimento de um médico
        Medecin médecin = medecins.get(0);
        List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
        display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
        // Lista de consultas de um médico em um determinado dia
        Date jour = new Date();
        display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // adicionar um RV
        Rv rv = null;
        Creneau créneau = creneaux.get(2);
        Client client = clients.get(0);
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        rv = métier.ajouterRv(jour, créneau, client);
        // verificação
        Rv rv2 = métier.getRvById(rv.getId());
        Assert.assertEquals(rv, rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // adicionar um RV no mesmo horário do mesmo dia
        // deve gerar uma exceção
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        Boolean erreur = false;
        try {
            rv = métier.ajouterRv(jour, créneau, client);
            System.out.println("Rv ajouté");
        } catch (Exception ex) {
            Throwable th = ex;
            while (th != null) {
                System.out.println(ex.getMessage());
                th = th.getCause();
            }
            // registra-se o erro
            erreur = true;
        }
        // verifica-se se houve um erro
        Assert.assertTrue(erreur);
        // lista de RV
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // exibição da agenda
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // excluir um RV
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // verificação
        rv2 = métier.getRvById(rv.getId());
        Assert.assertNull(rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
    }

    // método utilitário — exibe os elementos de uma coleção
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }

}
  • linha 22: a anotação [@SpringApplicationConfiguration] permite utilizar o arquivo de configuração [DomainAndPersistenceConfig] analisado anteriormente. A classe de teste, assim, se beneficia de todos os beans definidos por esse arquivo;
  • linha 23: a anotação [@RunWith] permite a integração do Spring com o JUnit: a classe poderá ser executada como um teste JUnit. [@RunWith] é uma anotação JUnit (linha 9), enquanto a classe [SpringJUnit4ClassRunner] é uma classe Spring (linha 12);
  • linhas 26-27: injeção na classe de teste de uma referência à camada [métier];
  • muitos testes são apenas testes visuais simples:
    • linhas 32-33: lista de clientes;
    • linhas 35-36: lista de médicos;
    • linhas 39-40: lista de 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 [ajouterRv] retorna a consulta com uma informação adicional: sua chave primária id;
  • linha 53: usa-se essa chave primária para pesquisar o compromisso no banco de dados;
  • linha 54: verifica-se se o compromisso procurado e o compromisso encontrado são os mesmos. Vale lembrar que o método [equals] da entidade [Rv] foi redefinido: dois compromissos são iguais se tiverem o mesmo ID. Aqui, isso nos mostra que o compromisso adicionado foi efetivamente inserido no banco de dados;
  • linhas 61-73: tentamos adicionar o mesmo compromisso pela segunda vez. Isso deve ser rejeitado pelo SGBD, pois existe uma restrição de exclusividade:

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

A linha 8 acima indica que a combinação [JOUR, ID_CRENEAU] deve ser única, o que impede a inserção de dois compromissos no mesmo dia e no mesmo intervalo de tempo.

  • linha 73: verifica-se se realmente ocorreu uma exceção;
  • linha 77: solicita-se a agenda do médico para o qual acabou de ser adicionado um compromisso;
  • linha 79: verifica-se se a consulta adicionada está de fato presente na agenda dele;
  • linha 82: exclui-se a consulta adicionada;
  • linha 84: busca-se na base de dados a consulta excluída;
  • linha 85: verifica-se se foi recuperado um ponteiro null, indicando assim que a consulta procurada não existe;

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

 

8.4.9. O programa de console

  

O programa de console é básico. Ele ilustra como recuperar uma chave externa:


package rdvmedecins.boot;

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

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

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

public class Boot {
    // inicialização
    public static void main(String[] args) {
        // preparando a configuração
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // iniciando
        ConfigurableApplicationContext context = app.run(args);
        // função
        IMetier métier = context.getBean(IMetier.class);
        try {
            // adicionar um RV
            Date jour = new Date();
            System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
            Client client = (Client) new Client().build(1L, 1L);
            Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
            Rv rv = métier.ajouterRv(jour, créneau, client);
            System.out.println(String.format("Rv ajouté = %s", rv));
            // verificação
            créneau = métier.getCreneauById(1L);
            long idMedecin = créneau.getIdMedecin();
            display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
        } catch (Exception ex) {
            System.out.println("Exception : " + ex.getCause());
        }
        // fechamento do contexto Spring
        context.close();
    }

    // método utilitário — exibe os elementos de uma coleção
    private static <T> void display(String message, Iterable<T> elements) {
        System.out.println(message);
        for (T element : elements) {
            System.out.println(element);
        }
    }

}

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

  • linha 19: a classe [SpringApplication] utilizará a classe de configuração [DomainAndPersistenceConfig];
  • linha 20: supressão dos logs de inicialização do aplicativo;
  • linha 22: a classe [SpringApplication] é executada. Ela retorna um contexto Spring, ou seja, a lista de beans registrados;
  • linha 24: obtém-se uma referência ao bean que implementa a interface [IMetier]. Trata-se, portanto, de uma referência à camada [métier];
  • linhas 27-31: adição de um novo agendamento para hoje, para o cliente nº 1 no horário nº 1. O cliente e o horário foram criados do zero para mostrar que apenas os identificadores são utilizados. Inicializamos aqui a versão, mas poderíamos ter colocado qualquer valor. Ela não é utilizada neste contexto;
  • linha 34: queremos saber qual é o médico responsável pelo horário nº 1. Para isso, precisamos consultar o banco de dados para buscar o horário nº 1. Como estamos no modo [FetchType.LAZY], o médico não é retornado junto com o horário. No entanto, tomamos o cuidado 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: exibe-se a lista de consultas do médico;

Os resultados na console são os seguintes:

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

8.4.10. Gerenciamento de logs

Os logs da console são configurados por dois arquivos: [application.properties] e [logback.xml] [1]:

O arquivo [application.properties] é utilizado pelo framework Spring Boot. Nele, é possível definir diversos parâmetros para alterar os valores padrão utilizados pelo Spring Boot (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Aqui, seu conteúdo é o seguinte:


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

O arquivo [logback.xml] é o arquivo de configuração do framework de logs [logback] [2]:


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- os codificadores recebem, por padrão, o tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- controle do nível dos logs -->
        <root level="info"> <!-- desativado, informação, depuração, aviso -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>
  • o nível geral de logs é controlado pela linha 9 — aqui, logs de nível [info];

Isso resulta no seguinte:

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 alterarmos o nível de logs do Hibernate para [info] (sem alterar nada mais):


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

obtém-se 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 alterarmos o nível de log para [debug] (sem alterar nada mais):


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

isso resulta no seguinte:


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;
  • etapa 3: implementação das camadas CORS e [Cross-Origin Resource Sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]. O cliente do nosso serviço web será um cliente web Angular que não pertencerá necessariamente ao mesmo domínio que o nosso serviço web. Por padrão, ele não poderá acessá-lo, a menos que o serviço web o autorize. Veremos como;

8.4.11.1. Configuração do Maven

O arquivo [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>
                <!-- camada web Spring MVC -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <!-- camada de teste -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <!-- camada 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 pai do Maven;
  • linhas 19-22: as dependências para um projeto Spring MVC;
  • linhas 24-28: as dependências para os testes JUnit / Spring;
  • linhas 30-34: dependências do projeto em relação às camadas [métier, DAO, JPA];

8.4.11.2. A interface do serviço web

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

Todas as respostas do nosso serviço web terão o mesmo formato, correspondente à transformação jSON de um objeto do tipo [Response], conforme segue:


package rdvmedecins.web.models;

import java.util.List;

public class Response<T> {

    // ----------------- propriedades
    // status da operação
    private int status;
    // eventuais mensagens de erro
    private List<String> messages;
    // o corpo da resposta
    private T body;

    // construtores
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters e setters
    ...
}
  • linha 7: código de erro da resposta 0: OK; em caso contrário: KO;
  • linha 11: uma lista de mensagens de erro, caso haja erro;
  • linha 13: o corpo da resposta;

Apresentamos agora as capturas de tela que ilustram a interface do serviço web / jSON:

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

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

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

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

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

Para adicionar/excluir um compromisso, usamos a extensão do Chrome [Advanced Rest Client], pois essas operações são realizadas com um POST.

Adicionar uma consulta [/ajouterRv]

  • no [0], o URL do serviço web;
  • em [1], utiliza-se o método POST;
  • em [2], o texto jSON das informações transmitidas ao serviço web na forma {dia, idClient, idCreneau};
  • em [3], o cliente informa ao serviço web que está enviando 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á enviando estão no formato jSON;
  • em [5]: o serviço web responde que também está enviando jSON;
  • em [6]: a resposta jSON do serviço web. O campo [body] contém a forma jSON do compromisso adicionado;

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

Observe-se o ID [50] do compromisso. Vamos excluí-lo.

Excluir um compromisso [/supprimerRv]

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

A resposta é, então, a seguinte:

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

É possível verificar se o compromisso foi excluído:

Acima, a consulta do paciente [Mme GERMAIN] não está mais presente.

O serviço web também permite recuperar entidades por meio de seus identificadores:

Todas essas entidades URL são processadas 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 do Spring;
  • linha 10: solicita-se que os componentes do Spring sejam procurados no pacote [rdvmedecins.web] e seus descendentes. É assim que serão encontrados os componentes:
    • [@RestController RdvMedecinsController] no pacote [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] no pacote [rdvmedecins.web.models];
  • linha 11: importamos a classe [DomainAndPersistenceConfig], que configura o projeto [rdvmedecins-metier-dao] para ter 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 {

    // configuração do DispatcherServlet para os cabeçalhos CORS
    @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);
    }

    // mapeadores 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 do framework Spring MVC. Ela desempenha a função de [FrontController]: intercepta as solicitações enviadas ao site Spring MVC e as encaminha para serem processadas por um dos controladores (Controller) do site;
  • linha 22: instanciação da classe;
  • linha 23: essa linha pode ser ignorada por enquanto;
  • linhas 27-30: o servlet [dispatcherServlet] processa todas as solicitações URL;
  • linhas 27-30: ativam o servidor Tomcat integrado nas dependências do projeto. Ele funcionará na porta 8080;
  • linhas 38-67: quatro mapeadores jSON configurados com filtros jSON diferentes;
  • linhas 38-41: um mapeador jSON sem filtros;
  • linhas 43-49: o mapeador jSON [jsonMapperShortCreneau] serializa/desserializa 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 / desserializa um objeto [Rv], ignorando os campos [Rv.creneau] e [Rv.client];

8.4.11.4. A classe [ApplicationModel]

  

A classe [ApplicationModel] servirá para duas coisas:

  • como cache para armazenar as listas de médicos e pacientes (clientes);
  • como 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 {

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

    // dados provenientes da camada [métier]
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    // dados de configuração
    private boolean CORSneeded = false;
    private boolean secured = false;
    
    @PostConstruct
    public void init() {
        // recuperam-se os médicos e os clientes
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }

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

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

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

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

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

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

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

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

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

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

    @Override
    public void supprimerRv(long idRv) {
        métier.supprimerRv(idRv);
    }

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

     // getters e setters
public boolean isCORSneeded() {
        return CORSneeded;
    }

    public boolean isSecured() {
        return secured;
    }

}
  • linha 19: a anotação [@Component] transforma a classe [ApplicationModel] em um componente Spring. Assim como todos os componentes Spring vistos até agora (com exceção de @Controller), apenas um objeto desse tipo será instanciado (singleton);
  • linha 20: a classe [ApplicationModel] implementa a interface [IMetier];
  • linhas 23-24: uma referência na camada [métier] é injetada pelo Spring;
  • linha 34: a anotação [@PostConstruct] faz com que o método [init] seja executado logo após a instanciação da classe [ApplicationModel];
  • linhas 38-39: recuperam-se as listas de médicos e clientes da camada [métier];
  • 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) se comunicam com o singleton [ApplicationModel];

Essa estratégia proporciona flexibilidade no gerenciamento do cache. Atualmente, os horários dos médicos não são armazenados em cache. Para fazê-lo, basta modificar a classe [ApplicationModel]. Isso não tem nenhum impacto sobre o controlador, que continuará a utilizar o método [List<Creneau> getAllCreneaux(long idMedecin)] como fazia anteriormente. É a implementação desse método na classe [ApplicationModel] que será alterada.

8.4.11.5. A classe Static

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

  

Seu código é o seguinte:


package rdvmedecins.web.helpers;

import java.util.ArrayList;
import java.util.List;

public class Static {

    public Static() {
    }

    // lista de mensagens de erro de uma exceção
    public static List<String> getErreursForException(Exception exception) {
        // recupera-se a lista de mensagens de erro da exceção
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }
}
  • linha 12: o método [Static.getErreursForException], que foi utilizado (linha 8 abaixo) no método [init] da classe [ApplicationModel]:

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

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

8.4.11.6. O esqueleto do controlador [RdvMedecinsController]

  

Vamos agora detalhar o processamento do URL do serviço web. Três classes principais estão envolvidas nesse processamento:

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

    // lista de mensagens
    private List<String> messages;

    // mapeadores jSON
    @Autowired
    private ObjectMapper jsonMapper;

    @Autowired
    private ObjectMapper jsonMapperShortCreneau;

    @Autowired
    private ObjectMapper jsonMapperLongRv;

    @Autowired
    private ObjectMapper jsonMapperShortRv;

    @PostConstruct
    public void init() {
        // mensagens de erro do aplicativo
        messages = application.getMessages();
    }

    // lista de médicos
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {...}

    // lista de clientes
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllClients() throws JsonProcessingException {...}

    // lista de horários de atendimento de um médico
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}

    // lista de consultas de um médico
    @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] transforma a classe [RdvMedecinsController] em um controlador Spring, o C do MVC;
  • linhas 38-39: um objeto do tipo [ApplicationModel] será injetado aqui pelo Spring. Já o apresentamos;
  • linhas 41-42: um objeto do tipo [RdvMedecinsCorsController] será injetado aqui pelo Spring. Apresentaremos esse objeto apenas posteriormente;
  • 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 logo após a instanciação da classe. Quando este é executado, os objetos injetados pelo Spring já estão disponíveis;
  • linha 63: recuperam-se as eventuais mensagens de erro do objeto [ApplicationModel]. Esse objeto foi instanciado no início da aplicação e tentou armazenar em cache os médicos e os clientes. Se isso falhou, então temos [messages!=null]. Isso permitirá que os métodos do controlador saibam se a aplicação foi inicializada corretamente;
  • linhas 67-118: os URL expostos pelo serviço [web / jSON]. Todos os métodos retornam a string jSON de um objeto do tipo [Response<T>], conforme segue:
 

package rdvmedecins.web.models;

import java.util.List;

public class Response<T> {

    // ----------------- propriedades
    // status da operação
    private int status;
    // eventuais mensagens de erro
    private List<String> messages;
    // o corpo da resposta
    private T body;

    // construtores
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters e 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 assume o valor null em caso de erro;

Esse objeto é serializado como jSON antes de ser enviado ao navegador do cliente;

  • linha 67: o URL exposto é [/getAllMedecins]. O cliente deve utilizar um método [GET] para fazer sua solicitação (method = RequestMethod.GET). Se esse URL fosse solicitado por um POST, ele seria recusado e o Spring MVC enviaria um código de erro HTTP ao cliente web. O próprio método retorna a resposta ao cliente (linha 68). Será uma sequência de caracteres (linha 67). O cabeçalho HTTP [Content-type : application/json; charset=UTF-8] será enviado ao cliente para informá-lo de que receberá uma sequência jSON (linha 67);
  • linha 77: o URL é configurado por {idMedecin}. Esse parâmetro é recuperado com a anotação [@PathVariable] na linha 79;
  • linha 79: o parâmetro [long idMedecin] recebe seu valor do parâmetro {idMedecin} do URL [@PathVariable("idMedecin")]. O parâmetro no URL e o do método podem ter nomes diferentes. É importante observar aqui que [@PathVariable("idMedecin")] é do tipo String (todo o URL é um String), enquanto o parâmetro [long idMedecin] é do tipo [long]. A conversão de tipo é feita automaticamente. Um código de erro HTTP é retornado se essa conversão de tipo falhar;
  • linha 105: a anotação [@RequestBody] designa o corpo da consulta. Em uma solicitação GET, quase nunca há corpo (mas é possível incluir um). Em uma solicitação POST, na maioria das vezes há corpo (mas é possível não incluir um). Para o URL [ajouterRv], o cliente web envia em seu POST a seguinte sequência:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

A sintaxe [@RequestBody PostAjouterRv post] (linha 105) , somada ao fato de que o método espera o jSON [consumes = "application/json; charset=UTF-8"] da linha 103, fará com que a string jSON enviada pelo cliente web seja deserializada em um objeto do tipo [PostAjouterRv]. Este é o seguinte:


package rdvmedecins.web.models;

public class PostAjouterRv {

    // dados da postagem
    private String jour;
    private long idClient;
    private long idCreneau;

    // getters e setters
    ...
}

Nesse caso também, as alterações de tipo necessárias ocorrerão automaticamente;

  • nas linhas 107 a 109, encontramos um mecanismo semelhante para URL e [/supprimerRv]. A string jSON enviada é a seguinte:
{"idRv":116}

e o tipo [PostSupprimerRv] é o seguinte:


package rdvmedecins.web.models;

public class PostSupprimerRv {

    // dados da postagem
    private long idRv;

    // getters e setters
    ...
}

8.4.11.7. O URL [/getAllMedecins]

O URL e o [/getAllMedecins] são processados pelo seguinte método do controlador [RdvMedecinsController]:


// lista de médicos
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {
        // a resposta
        Response<List<Medecin>> response;
        // estado do aplicativo
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // lista de médicos
            try {
                response = new Response<>(0, null, application.getAllMedecins());
            } catch (RuntimeException e) {
                response = new Response<>(1, Static.getErreursForException(e), null);
            }
        }
        // resposta
        return jsonMapper.writeValueAsString(response);
    }
  • linhas 9-10: verifica-se se o aplicativo foi inicializado corretamente (messages==null). Caso contrário, é enviada uma resposta com status=-1 e body=messages;
  • linha 13: caso contrário, solicita-se a lista de médicos à classe [ApplicationModel];
  • linha 19: envia-se a cadeia jSON da resposta com o mapeador jSON [jsonMapper], pois a classe [Medecin]possui o filtro jSON. A resposta pode estar sem erros (linha 14) ou com erros (linha 16). O método [application.getAllMedecins()] não lança uma exceção, pois se limita a retornar uma lista que está no cache. No entanto, manteremos esse tratamento de exceções para o caso de os médicos não estarem mais no cache;

Ainda não ilustramos o caso em que a aplicação foi inicializada incorretamente. Vamos interromper o SGBD e o MySQL5, iniciar o serviço web e, em seguida, solicitar o URL e o [/getAllMedecins]:

Image

De fato, ocorre um erro. Em condições normais, obtemos a seguinte visualização:

8.4.11.8. O URL [/getAllClients]

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


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

Ela é análoga ao método [getAllMedecins] já estudado. Os resultados obtidos são os seguintes:

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

O URL e o [/getAllCreneaux/{idMedecin}] são processados pelo seguinte método do controlador [RdvMedecinsController]:


// lista de horários de um médico
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
        // a resposta
        Response<List<Creneau>> response;
        // status do aplicativo
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        }
        // recuperação do médico
        Response<Medecin> responseMedecin = getMedecin(idMedecin);
        if (responseMedecin.getStatus() != 0) {
            response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
        } else {
            Medecin médecin = responseMedecin.getBody();
            // horários do médico
            try {
                response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // resposta
        return jsonMapperShortCreneau.writeValueAsString(response);
    }
  • linha 12: o médico identificado pelo parâmetro [id] é consultado por um método local:

private Response<Medecin> getMedecin(long id) {
        // buscando o médico
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (RuntimeException e1) {
            return new Response<Medecin>(1, Static.getErreursForException(e1), null);
        }
        // o médico já existe?
        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);
    }

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

  • linhas 13-14: se for status!=0, constrói-se uma resposta com erro;
  • linha 16: recupera-se o médico;
  • linha 19: recupera-se os horários desse médico;
  • linha 25: envia-se como resposta um objeto [List<Creneau>]. Vale lembrar a definição da classe [Creneau]:

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

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

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

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

Recordemos a consulta JPQL que implementa o método [getAllCreneaux] na camada [DAO]:


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

A notação [c.medecin.id] força a junção entre as tabelas [CRENEAUX] e [MEDECINS]. Assim, a consulta retorna todos os horários do médico, com o nome do médico em cada um deles. Ao serializar esses horários na tabela jSON, a cadeia jSON do médico aparece em cada um deles. Isso é desnecessário. Para controlar a serialização, precisamos de duas coisas:

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

O ponto 1 é verificado com a injeção do conversor jSON apropriado para o objeto no controlador:


@Autowired
private ObjectMapper jsonMapperShortCreneau;

O ponto 2 é obtido 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 jSON Jackson. Ela cria um filtro chamado [creneauFilter]. Com esse filtro, poderemos definir programaticamente quais campos devem ou não ser serializados;

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


        // resposta
        return jsonMapperShortCreneau.writeValueAsString(response);

O mapeador jSON [jsonMapperShortCreneau] foi definido na classe [WebConfig] da seguinte maneira:


    @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. Esse filtro serializa o objeto [Creneau] sem seu campo [medecin];

O resultado retornado pelo método [getAllCreneaux] é a cadeia jSON, do tipo [Response<List<Creneau>].

Os resultados obtidos são os seguintes:

ou estes, caso o intervalo não exista:

A partir desse exemplo, retenhamos a seguinte regra:

  • os métodos do servidor web / jSON retornam um objeto do tipo [Response<T>], que é serializado como jSON;
  • se o tipo T tiver um ou mais filtros jSON, para serializá-lo utilizar-se-á um mapeador com esses mesmos filtros;

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

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


// lista de consultas de um médico
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
                    throws JsonProcessingException {
        // a resposta
        Response<List<Rv>> response=null;
        boolean erreur = false;
        // status da aplicação
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // verifica-se a data
        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) {
            // recuperando o médico
            responseMedecin = getMedecin(idMedecin);
            if (responseMedecin.getStatus() != 0) {
                response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            Medecin médecin = responseMedecin.getBody();
            // lista de suas consultas
            try {
                response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
            } catch (RuntimeException e1) {
                response = new Response<>(4, Static.getErreursForException(e1), null);
            }
        }
        // resposta
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • é necessário converter a cadeia jSON para o tipo [Response<List<Rv>>]. A classe [Rv] possui um campo [Rv.creneau]. Se esse campo for serializado, encontraremos o filtro jSON [creneauFilter];
  • linha 47: o objeto do tipo [Response<List<Rv>>] da linha 7 é serializado como jSON;

Vamos analisar o caso em que a lista de compromissos 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;

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

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

    // uma consulta está vinculada a um horário
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

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

...

}
  • linha 11: o cliente é pesquisado com o modo [FetchType.LAZY];
  • linha 18: o horário é pesquisado com o modo [FetchType.LAZY];

Lembramos a consulta JPQL, que busca os compromissos:


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

São realizadas junções explicitamente para recuperar os campos [client] e [creneau]. Além disso, devido à junção [cr.medecin.id=?1], também teremos o médico. O médico aparecerá, portanto, na cadeia jSON de cada consulta. No entanto, essa informação duplicada é, além disso, desnecessária. Vimos como resolver esse problema usando um filtro jSON no objeto [Creneau]. Devido aos modos [FetchType.LAZY] dos campos [client] e [creneau] da classe [Rv], em breve perceberemos a necessidade de aplicar um filtro jSON à classe [RV] do projeto [rdvmedecins-metier-dao]:


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

Verificaremos a serialização do objeto [Rv] com o filtro [rvFilter]. Aparentemente, neste caso, não precisamos aplicar o filtro, pois precisamos de todos os campos do objeto do tipo [Rv]. No entanto, como indicamos que a classe possui um filtro jSON, precisamos defini-lo para toda serialização de um objeto do tipo [Rv]; caso contrário, ocorrerá uma exceção. Para isso, utilizamos o mapeador jSON a seguir, definido na classe [rdvMedecinsController]:


    @Autowired
    private ObjectMapper jsonMapperLongRv;

Esse 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: indicamos que todos os campos do objeto [Rv] devem ser serializados;
  • linha 5: indicamos 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 ainda estes, com um dia sem agendamento:

ou ainda estes com um dia incorreto:

ou ainda estes com um médico incorreto:

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

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


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
                    throws JsonProcessingException {
        // a resposta
        Response<AgendaMedecinJour> response = null;
        boolean erreur = false;
        // status da solicitação
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // verifica-se a data
        Date jourAgenda = null;
        if (!erreur) {
            // verificando a data
            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);
            }
        }
        // buscando o médico
        Medecin médecin = null;
        if (!erreur) {
            // buscando o médico
            Response<Medecin> responseMedecin = getMedecin(idMedecin);
            if (responseMedecin.getStatus() != 0) {
                response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
            } else {
                médecin = responseMedecin.getBody();
            }
        }
        // recuperando a agenda
        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);
            }
        }
        // resposta
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • linhas 6, 49: a cadeia jSON é convertida em um tipo [AgendaMedecinJour] encapsulado em um objeto [Response];

O tipo [AgendaMedecinJour] é o seguinte:


public class AgendaMedecinJour implements Serializable {
    // campos
    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;
    // campos
    private Creneau creneau;
   private Rv rv;

Os campos [creneau] e [rv] possuem filtros jSON que devem ser configurados. É isso que faz a linha 49 do método [getAgendaMedecinJour], que utiliza o mapeador jSON [jsonMapperLongRv], já mencionado 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 do médico for inválido:

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

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


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
        // a resposta
        Response<Medecin> response;
        // status do aplicativo
        if (messages != null) {
            response = new Response<Medecin>(-1, messages, null);
        } else {
            response = getMedecin(id);
        }
        // resposta
        return jsonMapper.writeValueAsString(response);
}
  • linhas 5, 13: o método converte a cadeia jSON em um tipo [Medecin]. Esse tipo não possui nenhuma anotação de filtro jSON. Portanto, na linha 14, utiliza-se o mapeador jSON sem filtros;

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


    private Response<Medecin> getMedecin(long id) {
        // buscando o médico
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (RuntimeException e1) {
            return new Response<Medecin>(1, Static.getErreursForException(e1), null);
        }
        // médico já existe?
        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 obtidos são os seguintes:

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

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

O URL [/getClientById/{id}] é processado pelo método a seguir do 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 {
        // a resposta
        Response<Client> response;
        // estado do aplicativo
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            response = getClient(id);
        }
        // resposta
        return jsonMapper.writeValueAsString(response);
}
  • linhas 5, 13: o método converte a cadeia jSON em um tipo [Client]. Esse tipo não possui anotação de filtros jSON. Portanto, na linha 13, utiliza-se o mapeador jSON sem filtros;

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


    private Response<Client> getClient(long id) {
        // recuperando o cliente
        Client client = null;
        try {
            client = application.getClientById(id);
        } catch (RuntimeException e1) {
            return new Response<Client>(1, Static.getErreursForException(e1), null);
        }
        // cliente já existe?
        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 obtidos são os seguintes:

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

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

O URL [/getCreneauById/{id}] é processado pelo seguinte método do 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 {
        // a resposta
        Response<Creneau> response;
        // status do aplicativo
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // retornando o horário
            response = getCreneau(id);
        }
        // resposta
        return jsonMapperShortCreneau.writeValueAsString(response);
}
  • linhas 5, 14: o método retorna a cadeia jSON de um tipo [Response<Creneau>];

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


    private Response<Creneau> getCreneau(long id) {
        // recuperando o horário
        Creneau créneau = null;
        try {
            créneau = application.getCreneauById(id);
        } catch (RuntimeException e1) {
            return new Response<Creneau>(1, Static.getErreursForException(e1), null);
        }
        // o horário já existe?
        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);
    }

Lembramos o código da entidade [Creneau]:


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

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

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

    // chave estrangeira
    @Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
  • linhas 14-16: como o campo [medecin] está no modo [fetch = FetchType.LAZY], ele não é recuperado quando se busca um horário por meio de seu [id]. Portanto, é necessário excluí-lo da serialização. Sem essa exclusão, ocorre uma exceção. Isso se deve ao fato de que o objeto de serialização [mapper] chamará o método [getMedecin] para obter o campo [medecin]. No entanto, com uma implementação JPA / Hibernate, o modo [fetch = FetchType.LAZY] do campo [medecin] retornou um objeto [Creneau], cujo método [getMedecin] está programado para buscar o médico no contexto JPA. Isso é chamado de objeto [proxy]. Mas lembremos a arquitetura do aplicativo web:

O controlador está localizado no bloco [Contrôleurs / Actions]. Quando se está nesse bloco, não há mais a noção de contexto JPA. Este último é criado durante as operações da camada [DAO]. Ele não persiste além disso. Portanto, quando o controlador tenta acessar o contexto JPA, ocorre uma exceção indicando que ele está fechado. Para evitar essa exceção, é necessário impedir a serialização do campo [medecin] da classe [Rv]. É isso que faz o mapeador jSON [jsonMapperShortCreneau]:


    @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, caso o número do intervalo esteja incorreto:

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

O URL [/getRvById/{id}] é processado pelo seguinte método do 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 {
        // a resposta
        Response<Rv> response;
        // status da aplicação
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // recuperação do agendamento
            response = getRv(id);
        }
        // resposta
        return jsonMapperShortRv.writeValueAsString(response);
}
  • linhas 5, 14: o método retorna a cadeia jSON de um tipo [Response<Rv>];

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


    private Response<Rv> getRv(long id) {
        // recuperando o Rv
        Rv rv = null;
        try {
            rv = application.getRvById(id);
        } catch (RuntimeException e1) {
            return new Response<Rv>(1, Static.getErreursForException(e1), null);
        }
        // Rv já existe?
        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] possui dois campos com a anotação [fetch = FetchType.LAZY]: os campos [creneau] e [client]. Portanto, esses campos não são recuperados quando se busca um [Rv] por meio de sua chave primária. Portanto, pelas mesmas razões mencionadas anteriormente, é necessário excluí-los da serialização. É isso que faz o mapeador [jsonMapperShortRv] a seguir, definido na classe [WebConfig]:


    @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, caso o número do compromisso esteja incorreto:

8.4.11.16. O URL [/ajouterRv]

O URL [/ajouterRv] é processado pelo seguinte método do 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 {
        // a resposta
        Response<Rv> response = null;
        boolean erreur = false;
        // estado da aplicação
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // recuperando os valores lançados
        String jour;
        long idCreneau = -1;
        long idClient = -1;
        Date jourAgenda = null;
        if (!erreur) {
            // recuperando os valores lançados
            jour = post.getJour();
            idCreneau = post.getIdCreneau();
            idClient = post.getIdClient();
            // verifica-se a data
            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;
            }
        }
        // recupera-se o horário
        Response<Creneau> responseCréneau = null;
        if (!erreur) {
            // recupera-se o horário
            responseCréneau = getCreneau(idCreneau);
            if (responseCréneau.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
            }
        }
        // recupera-se o cliente
        Response<Client> responseClient = null;
        Creneau créneau = null;
        if (!erreur) {
            créneau = (Creneau) responseCréneau.getBody();
            // recupera-se o cliente
            responseClient = getClient(idClient);
            if (responseClient.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
            }
        }
        if (!erreur) {
            Client client = responseClient.getBody();
            // adiciona-se o compromisso
            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);
            }
        }
        // resposta
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • linhas 5, 67: o método deve retornar a string jSON como um tipo [Response<Rv>];
  • linha 3: a anotação [@RequestBody PostAjouterRv post] recupera o corpo do POST e o coloca no parâmetro [PostAjouterRv post]. Esse corpo pertence ao jSON [consumes = "application/json; charset=UTF-8"], que será deserializado automaticamente no tipo [PostAjouterRv] a seguir:

public class PostAjouterRv {

    // dados da postagem
    private String jour;
    private long idClient;
    private long idCreneau;
...
  • em seguida, há código que já foi encontrado de uma forma ou de outra;
  • linha 67: a configuração dos filtros jSON, [creneauFilter] e [rvFilter]. O método converte a cadeia jSON em um tipo [Response<Rv>], em que Rv foi obtido na linha 61. O objeto [Rv] encapsula um objeto [Creneau], bem como um objeto [Client]. O objeto [Creneau] possui uma dependência [FetchType.LAZY] de um objeto [Medecin] e foi obtido nas linhas 36 a 44. Ele foi buscado no contexto JPA por meio de sua chave primária e foi obtido sem sua dependência [FetchType.LAZY]. Por fim,
    • o objeto [Rv] possui todas as suas dependências. Elas podem ser serializadas;
    • o objeto [Creneau] não possui sua dependência [medecin]. Portanto, esta não deve ser serializada;

O mapeador jSON [jsonMapperLongRv] definido na classe [WebConfig] atende a essas 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 são semelhantes a estes com o cliente [Advanced Rest Client]:

  • em [1], o URL do POST;
  • em [2], o POST;
  • em [3], o valor lançado;
  • em [4a], esse valor lançado é do jSON;
  • em [4b], o cliente indica que está enviando jSON;
  • em [5], o servidor indica que está devolvendo jSON;
  • em [6], a resposta jSON do servidor, que representa o compromisso adicionado. Nela, vemos o identificador [id] do compromisso adicionado;

Obtém-se o seguinte com um número de horário inexistente:

8.4.11.17. O URL [/supprimerRv]

O URL [/supprimerRv] é processado pelo seguinte método do 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 {
        // a resposta
        Response<Void> response = null;
        boolean erreur = false;
        // cabeçalhos CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // estado da aplicação
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // recuperam-se os valores lançados
        long idRv = post.getIdRv();
        // recuperação do rv
        if (!erreur) {
            Response<Rv> responseRv = getRv(idRv);
            if (responseRv.getStatus() != 0) {
                response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            // exclusão do rv
            try {
                application.supprimerRv(idRv);
                response = new Response<Void>(0, null, null);
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // resposta
        return jsonMapper.writeValueAsString(response);
    }
  • linha 5: o tipo [Void] é a classe correspondente ao tipo primitivo [void];
  • linhas 5 e 34: o método retorna a cadeia jSON de um tipo [Response<Void>] que não possui filtros jSON. Por isso, na linha 34, utiliza-se o mapeador jSON sem filtros;
  • linha 3: o método tem como parâmetro o corpo do POST, ou seja, o valor enviado. Este é recebido na forma jSON [consumes = "application/json; charset=UTF-8"] e deserializado automaticamente no tipo [PostSupprimerRv] a seguir:

public class PostSupprimerRv {

    // dados da postagem
    private long idRv;

  • linha 28: quando a exclusã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 exclusão foi bem-sucedida;

Com um número de agendamento inexistente, obtém-se o seguinte:

Concluímos a parte do 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);
    }
}

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

Os logs são controlados pelos seguintes arquivos [2]:

[logback.xml]


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- os encoders são atribuídos por padrão ao tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- controle do nível dos logs -->
        <root level="info"> <!-- desativado, informação, depuração, aviso -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>
  • linha 9: o nível geral de logs é definido como [info];

[application.properties]


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

As linhas 1 e 2 permitem definir um nível de log específico para determinados elementos do aplicativo:

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

Os logs gerados 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á sendo inicializado;
  • linhas 27-38: os URL expostos pelo serviço web são detectados;
  • linha 44: o servidor Tomcat está pronto e aguardando solicitações na porta 8080;

Se modificarmos o arquivo [application.properties] da seguinte maneira:


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

obtêm-se os seguintes logs:

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 arquivo [logback.xml] da seguinte maneira:


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- os encoders recebem, por padrão, o tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- controle do nível dos logs -->
        <root level="off"> <!-- desativado, informação, depuração, aviso -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>

obtêm-se os seguintes logs:

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

Vemos, portanto, que temos certo controle sobre os logs que aparecem no console. O nível [info] costuma ser o nível adequado de logs.

Agora temos um serviço web operacional que pode ser consultado por meio de um cliente web. Passaremos agora à segurança desse serviço: queremos que apenas determinadas pessoas possam gerenciar as consultas médicas. Para isso, 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 as etapas 1 a 3 abaixo:

  

O projeto é composto pelos seguintes elementos:

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

8.4.12.1. Configuração do Maven

O projeto [3] é um projeto Maven. Vamos examinar seu arquivo [pom.xml] para conhecer 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 do framework [Thymeleaf];
  • linhas 22-25: dependência do framework Spring Security;

8.4.12.2. As visualizações Thymeleaf

  

A visualização [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], onde [context] é o contexto do aplicativo 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>. Esse texto contém uma expressão $ que deve ser avaliada. O elemento [[${#httpServletRequest.remoteUser}]] é o valor do atributo [RemoteUser] da consulta HTTP atual. Trata-se do nome do usuário conectado;
  • linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da tag [form]. O valor [@{/logout}] irá gerar o caminho [<context>/logout], onde [context] é o contexto do aplicativo 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 Hello [[${#httpServletRequest.remoteUser}]]!;
  • linha 9: a tradução de @{/logout};
  • linha 11: um campo oculto chamado (atributo name) _csrf;

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

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • linha 9: o atributo [th:if="${param.error}"] faz com que a tag <div> só seja 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}"] faz com que a tag <div> só seja gerada se o 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], onde <context> é o contexto do aplicativo web;
  • linha 13: um campo de entrada chamado [username];
  • linha 17: um campo de entrada chamado [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>

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

8.4.12.3. Configuração Spring MVC

  

A classe [MvcConfig] configura o framework Spring MVC:


package hello;

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

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

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

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

O sufixo [html] e a pasta [templates] são os valores padrão utilizados pelo Thymeleaf. Eles podem ser alterados por meio da configuração. A pasta [templates] deve estar na raiz do Classpath do projeto:

Acima de [1], as pastas [java] e [resources] são ambas pastas de origem (source folders). Isso significa que seu conteúdo estará na raiz do Classpath do projeto. Portanto, no [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 o framework Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • linha 9: a anotação [@Configuration] transforma a classe [WebSecurityConfig] em uma classe de configuração;
  • linha 10: a anotação [@EnableWebSecurity] transforma a classe [WebSecurityConfig] em uma classe de configuração do Spring Security;
  • linha 11: a classe [WebSecurity] estende a classe [WebSecurityConfigurerAdapter] para redefinir alguns de seus métodos;
  • linha 12: redefinição de um método da classe pai;
  • linhas 13 a 16: o método [configure(HttpSecurity http)] é redefinido para definir os direitos de acesso às diferentes URL do aplicativo;
  • linha 14: o método [http.authorizeRequests()] permite associar URL a direitos de acesso. São feitas as seguintes associações:
URL
regra
código
/, /home
acesso sem autenticação

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
acesso somente com autenticação
http.anyRequest().authenticated();
  • linha 15: define o método de autenticação. A autenticação é feita por meio 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 gerencia os usuários;
  • linha 20: a autenticação é feita com usuários definidos de forma “estática” [auth.inMemoryAuthentication()]. Um usuário é definido aqui com o login [user], a senha [password] e a função [USER]. É possível conceder os mesmos direitos a usuários com a mesma função;

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] solicita ao Spring Boot (linha 3) que realize a configuração que o desenvolvedor não terá feito explicitamente;
  • linha 9: transforma a classe [Application] em uma classe de configuração do Spring;
  • linha 10: solicita a varredura da pasta da classe [Application] para localizar componentes do Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão, assim, detectadas, pois 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 deparamos com esse processo e sabemos que o servidor Tomcat incorporado nas dependências Maven do projeto será iniciado e o projeto será implantado nele. Vimos que quatro instâncias de URL eram gerenciadas por [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.

8.4.12.6. Testes da aplicação

Vamos começar solicitando o URL [/], que é um dos quatro URL aceitos. Ele está associado à visualização [/templates/home.html]:

 

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

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

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

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

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Acesso restrito a usuários autenticados
http.anyRequest().authenticated();

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


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

Assim, obtemos [1]:

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

<!DOCTYPE html>

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

Lembramos que apenas o usuário user/password é reconhecido pelo Spring Security. Se inserirmos qualquer outra coisa em [2], obtemos a mesma página com uma mensagem de erro em [3]. O Spring Security redirecionou o navegador para a página URL [http://localhost:8080/login?error]. A presença do parâmetro [error] acionou a exibição da tag:


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

Agora, vamos inserir os valores esperados para user/password [4]:

  • em [4], fazemos o login;
  • em [5], o Spring Security nos redireciona para URL [/hello], pois era URL que estávamos solicitando quando fomos redirecionados para a página de login. A identidade do usuário foi exibida na seguinte linha de [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

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


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

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


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

Em nossa associação URL / visualizações, não definimos nada para o URL e o [/logout]. O que vai acontecer? Vamos testar:

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

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

8.4.12.7. Conclusion

No exemplo anterior, poderíamos ter escrito a aplicação web primeiro e, em seguida, implementado a segurança. O Spring Security não é intrusivo. É possível implementar a segurança em uma aplicação web já desenvolvida. Além disso, descobrimos os seguintes pontos:

  • é possível definir uma página de autenticação;
  • a autenticação deve ser acompanhada do token CSRF emitido pelo Spring Security;
  • se a autenticação falhar, o usuário é redirecionado para a página de autenticação, além disso, com um parâmetro “error” no token URL;
  • se a autenticação for bem-sucedida, o usuário é redirecionado para a página solicitada no momento em que a autenticação ocorreu. Se a página de autenticação for solicitada diretamente, sem passar por uma página intermediária, o Spring Security nos redireciona para o URL [/] (esse caso não foi apresentado);
  • o usuário se desconecta ao acessar a página URL [/logout] com um POST. O Spring Security nos redireciona então para a página de autenticação com o parâmetro logout no URL;

Todas essas conclusões se baseiam nos comportamentos padrão do Spring Security. Esses comportamentos podem ser alterados por meio de configuração, redefinindo certos métodos da classe [WebSecurityConfigurerAdapter].

O tutorial anterior não nos ajudará muito daqui para frente. De fato, vamos utilizar:

  • um banco de dados para armazenar os usuários, suas senhas e suas funções;
  • autenticação por cabeçalho HTTP;

Existem poucos tutoriais sobre o que queremos fazer aqui. A solução que será proposta é uma combinação de códigos encontrados aqui e ali.

8.4.13. Implementação da segurança no serviço web de agendamento

8.4.13.1. O banco de dados

O banco de dados [rdvmedecins] está sendo adaptado para incluir os usuários, suas senhas e suas funções. Três novas tabelas são criadas:

Image

Tabela [USERS]: os usuários

  • ID: chave primária;
  • VERSION: coluna de controle de versão da linha;
  • IDENTITY: uma identidade descritiva do usuário;
  • LOGIN: o login do usuário;
  • PASSWORD: sua senha;

Na tabela USERS, as senhas não são armazenadas em texto simples:

 

O algoritmo que criptografa as senhas é o algoritmo BCRYPT.

Tabela [ROLES]: as funções

  • ID: chave primária;
  • VERSION: coluna de controle de versão da linha;
  • NAME: nome da função. Por padrã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 usuário pode ter várias funções, e uma função pode abranger vários usuários. Temos uma relação muitos-para-muitos representada pela tabela [USERS_ROLES].

  • ID: chave primária;
  • VERSION: coluna de controle de versão da linha;
  • USER_ID: identificador de um usuário;
  • ROLE_ID: identificador de uma função;
 

Como estamos modificando o banco de dados, todas as camadas do projeto [métier, DAO, JPA] devem ser modificadas:

8.4.13.2. O novo projeto STS a partir do [métier, DAO, JPA]

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

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

8.4.13.3. As novas entidades [JPA]

A camada JPA define três novas entidades:

  

A classe [User] é a imagem da tabela [USERS]:


package rdvmedecins.entities;

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

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

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

    // fabricante
    public User() {
    }

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

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

    // getters e setters
....
}
  • linha 9: a classe estende a classe [AbstractEntity] já utilizada para as outras entidades;
  • linhas 13-15: não se especifica nome para as colunas, pois elas têm o mesmo nome dos campos a elas associados;

A classe [Role] é o espelho da tabela [ROLES]:


package rdvmedecins.entities;

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

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

    private static final long serialVersionUID = 1L;

    // propriedades
    private String name;

    // construtores
    public Role() {
    }

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

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

    // getters e setters
...
}

A classe [UserRole] é a imagem da tabela [USERS_ROLES]:


package rdvmedecins.entities;

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

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

    private static final long serialVersionUID = 1L;

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

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

8.4.13.4. Alterações na camada [DAO]

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

  

A interface [UserRepository] gerencia o acesso às entidades [User]:


package rdvmedecins.repositories;

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

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

public interface UserRepository extends CrudRepository<User, Long> {

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

    // lista de funções de um usuário identificado por seu login e senha
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // busca de um usuário pelo 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)] permite obter todas as funções de um usuário identificado por seu [id]
  • linhas 16-17: o mesmo, mas para um usuário identificado por seu login/senha;
  • linha 20: para localizar um usuário por meio de seu login;

A interface [RoleRepository] gerencia os acessos às entidades [Role]:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // busca de uma função pelo nome
    Role findRoleByName(String name);

}
  • linha 5: a interface [RoleRepository] estende a interface [CrudRepository];
  • linha 8: é possível pesquisar uma função pelo nome;

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


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • linha 5: a interface [UserRoleRepository] limita-se a estender a interface [CrudRepository] sem adicionar novos métodos a ela;

8.4.13.5. As classes de gerenciamento de usuários e funções

  

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

 

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

    // propriedades
    private User user;
    private UserRepository userRepository;

    // construtores
    public AppUserDetails() {
    }

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

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

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

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

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

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

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

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

    // getters e setters
    ...
}
  • linha 10: a classe [AppUserDetails] implementa a interface [UserDetails];
  • linhas 15-16: a classe encapsula um usuário (linha 15) e o repositório que permite obter os detalhes desse usuário (linha 16);
  • linhas 22-25: o construtor que instancia a classe com um usuário e seu repositório;
  • linhas 28-35: implementação do método [getAuthorities] da interface [UserDetails]. Ele deve construir uma coleção de elementos do tipo [GrantedAuthority] ou derivado. Aqui, utilizamos o tipo derivado [SimpleGrantedAuthority] (linha 32), que encapsula o nome de uma das funções do usuário da linha 15;
  • linhas 31-33: percorremos a lista de funções do usuário da linha 15 para construir uma lista de elementos do tipo [SimpleGrantedAuthority];
  • linhas 38-40: implementam o método [getPassword] da interface [UserDetails]. Retorna-se a senha do usuário da linha 15;
  • linhas 38-40: implementam o método [getUserName] da interface [UserDetails]. Retorna-se o login do usuário da linha 15;
  • linhas 47-50: a conta do usuário nunca expira;
  • linhas 52-55: a conta do usuário nunca é bloqueada;
  • linhas 57-60: as credenciais do usuário nunca expiram;
  • linhas 62-65: a conta do usuário está sempre ativa;

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

 

Essa 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 {
        // buscando o usuário pelo login
        User user = userRepository.findUserByLogin(login);
        // encontrado?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // retornamos os detalhes do usuário
        return new AppUserDetails(user, userRepository);
    }

}
  • linha 9: a classe será um componente Spring e, portanto, estará disponível em 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 login do usuário;
  • linha 18: o usuário é pesquisado por meio de seu login;
  • linhas 20-22: se ele não for encontrado, é lançada uma exceção;
  • linha 24: um objeto [AppUserDetails] é criado e renderizado. Ele é, de fato, do tipo [UserDetails] (linha 16);

8.4.13.6. Testes da camada [DAO]

  

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


package rdvmedecins.security;

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

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

public class CreateUser {

    public static void main(String[] args) {
        // sintaxe: login senha roleName

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

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

}
  • linha 17: a classe espera três argumentos que definem um usuário: seu login, sua senha e sua função;
  • linhas 25-27: os três parâmetros são recuperados;
  • linha 29: o contexto Spring é construído a partir da classe de configuração [DomainAndPersistenceConfig]. Essa classe já existia no projeto inicial. Ela deve ser alterada da seguinte forma:

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

Voltemos ao código de criação de um usuário:

  • linhas 30-32: recuperamos as referências das três entidades [Repository] que podem ser úteis para criar o usuário;
  • linha 34: verifica-se se a função já existe;
  • linhas 36-38: se não for o caso, criamos a função no banco de dados. Ela terá um nome do tipo [ROLE_XX];
  • linha 40: verifica-se se o login já existe;
  • linhas 42-49: se o login não existir, criamo-lo no banco de dados;
  • linha 44: criptografamos a senha. Aqui, utilizamos a classe [BCrypt] do Spring Security (linha 4). Portanto, precisamos dos arquivos desse framework. O arquivo [pom.xml] inclui uma nova dependência:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • linha 46: o usuário é armazenado no banco de dados;
  • linha 48: assim como a relação que o vincula à sua função;
  • linhas 51-57: caso o login já exista – verifica-se, então, se entre suas funções já se encontra a função que se deseja atribuir a ele;
  • linhas 59-61: se a função procurada não for encontrada, cria-se uma linha na tabela [USERS_ROLES] para vincular o usuário à sua função;
  • não há proteção contra possíveis exceções. Trata-se de uma classe de suporte para criar rapidamente um usuário com uma função.

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

Tabela [USERS]

Tabela [ROLES]

 

Tabela [USERS_ROLES]

 

Consideremos agora a segunda classe [UsersTest], que é um teste da classe JUnit:

  

package rdvmedecins.security;

import java.util.List;

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

import rdvmedecins.config.DomainAndPersistenceConfig;

import com.google.common.collect.Lists;

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

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

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

    @Test
    public void findUserByLogin() {
        // recupera-se o usuário [admin]
        User user = userRepository.findUserByLogin("admin");
        // verifica-se se a senha dele é [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // verifica-se a função de admin / admin
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        // recupera-se o usuário [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // verifica-se se a senha dele é [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // verifica-se a função de admin / admin
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // método utilitário — exibe os elementos de uma coleção
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • linhas 27-34: teste visual. Exibimos todos os usuários com suas funções;
  • linhas 36-46: verifica-se se o usuário [admin] possui a senha [admin] e a função [ROLE_ADMIN] utilizando o repositório [UserRepository];
  • linha 41: [admin] é a senha em texto simples. Na base, ela está criptografada de acordo com o algoritmo BCrypt. O método [BCrypt.checkpw] permite verificar se a senha em texto simples, uma vez criptografada, é de fato igual à que está na base;
  • linhas 48-59: verifica-se se o usuário [admin] possui a senha [admin] e a função [ROLE_ADMIN] utilizando o serviço [appUserDetailsService];

A execução dos testes é bem-sucedida com os seguintes logs:

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 intermediária

A adição das classes necessárias ao Spring Security foi realizada com poucas alterações no projeto original. Vamos recapitular:

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

Esse cenário muito favorável decorre do fato de que as três tabelas adicionadas ao banco de dados são independentes das tabelas existentes. Seria até possível colocá-las em um banco de dados separado. Isso foi possível porque se decidiu que um usuário tem uma existência independente dos médicos e dos clientes. Se estes últimos fossem usuários potenciais, teria sido necessário criar ligações entre a tabela [USERS] e as tabelas [MEDECINS] e [CLIENTS]. Isso teria, então, um impacto significativo no projeto existente.

8.4.13.8. O projeto STS da camada [web]

O projeto [rdvmedecins-webjson] evolui da seguinte forma a partir do [1]:

As principais alterações devem ser feitas no pacote [rdvmedecins.web.config], onde é necessário configurar o Spring Security. Há outras alterações, menores, nas classes [AppConfig] e [ApplicationModel]. Já encontramos uma classe de configuração do Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

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

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

Vamos seguir o mesmo procedimento:

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

A configuração do Spring Security é feita 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 {
        // a autenticação é feita pelo bean [appUserDetailsService]
        // a senha é criptografada pelo algoritmo de hash BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // aplicativo seguro?
        if (application.isSecured()) {
            // a senha é transmitida pelo cabeçalho Authorization: Basic xxxx
            http.httpBasic();
            // o método HTTP OPTIONS deve ser autorizado para todos
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // apenas a função ADMIN pode utilizar o aplicativo
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // todas as URL
                    .hasRole("ADMIN");
            // sem sessão
            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 concede acesso aos usuários do aplicativo, é injetada;
  • linhas 21-22: a classe [ApplicationModel], que funciona como cache para o aplicativo web, é injetada. Decide-se aqui utilizá-la também, para configurar a aplicação web em um único local. É ela que define o booleano [isSecured] da linha 36. Esse booleano protege (true) ou não (false) a aplicação web;
  • linhas 25-29: o método [configure(HttpSecurity http)] define os usuários e suas funções. Ele recebe como parâmetro um tipo [AuthenticationManagerBuilder]. Esse parâmetro é complementado com duas informações (linha 28):
    • uma referência ao serviço [appUserDetailsService] da linha 20, que dá acesso aos usuários registrados. Observe-se aqui que o fato de eles estarem registrados em um banco de dados não é mencionado. Portanto, eles poderiam estar em um cache, fornecidos por um serviço web, etc.;
    • o tipo de criptografia utilizado para a senha. Vale lembrar que utilizamos o algoritmo BCrypt;
  • linhas 38-47: o método [configure(HttpSecurity http)] define os direitos de acesso aos URL do serviço web;
  • linha 34: vimos no projeto de introdução que, por padrão, o Spring Security gerencia um token CSRF (Cross Site Request Forgery) que o usuário que deseja se autenticar deve enviar de volta ao servidor. Aqui, esse mecanismo está desativado. Isso, aliado ao valor booleano (isSecured=false), permite utilizar o aplicativo web sem segurança;
  • linha 38: ativamos o modo de autenticação por cabeçalho HTTP. O cliente deverá enviar o seguinte cabeçalho HTTP:
Authorization:Basic code

onde “code” é a codificação da sequência “login:password” pelo algoritmo Base64. Por exemplo, a codificação Base64 da sequência admin:admin é YWRtaW46YWRtaW4=. Portanto, o usuário com login [admin] e senha [admin] enviará o cabeçalho HTTP a seguir para se autenticar:

Authorization:Basic YWRtaW46YWRtaW4=
  • linhas 40-42: indicam que todos os URL do serviço web estão acessíveis aos usuários com a função [ROLE_ADMIN]. Isso significa que um usuário que não possua essa função não pode acessar o serviço web;
  • linha 47: a senha do usuário pode ou não ser armazenada em uma sessão. Se for armazenada, o usuário só precisa se autenticar na primeira vez. Nas vezes seguintes, suas credenciais não serão solicitadas. Aqui, optou-se por um modo sem sessão. Cada solicitação deverá ser acompanhada das credenciais de segurança;

A classe [AppConfig], que configura toda a aplicação, é alterada 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 modificação ocorre na linha 11: adiciona-se a classe de configuração [SecurityConfig];

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


@Component
public class ApplicationModel implements IMetier {

...
    // dados de configuração
    private boolean secured = false;
    
    public boolean isSecured() {
        return secured;
}
  • linha 6: define-se o valor booleano [secured] como [true / false], dependendo se se deseja ou não ativar a proteção.

8.4.13.9. Testes do serviço web

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

Authorization:Basic code

onde [code] é o código Base64 da sequência [login:password]. Para gerar esse código, pode-se usar o seguinte programa:

  

package rdvmedecins.helpers;

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

public class Base64Encoder {

    public static void main(String[] args) {
        // são esperados dois argumentos: login e senha
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // os dois argumentos são recuperados
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // codifica-se a sequência
        byte[] data = Base64.encode(chaîne.getBytes());
        // exibe-se sua codificação Base64
        System.out.println(new String(data));
    }

}

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

  

obtemos o seguinte resultado:

YWRtaW46YWRtaW4=

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


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

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

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

A resposta do servidor é a seguinte:

  • em [1], o cabeçalho de autenticação HTTP;
  • em [2], o servidor retorna uma resposta jSON;
  • em [3], uma lista de cabeçalhos HTTP relacionados à segurança do aplicativo web;

Conseguimos obter a lista de médicos:

 

Vamos agora tentar uma consulta 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 testar o usuário 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. Ela é diferente da anterior, que era [401 Unauthorized]. Desta vez, o usuário se autenticou corretamente, mas não possui permissões suficientes para acessar o URL;

Um serviço web seguro já está operacional. Vamos complementá-lo para que autorize solicitações entre domínios. Essa necessidade surgiu no documento [Tutoriel AngularJS / Spring 4] e, embora essa necessidade não exista aqui, vamos atendê-la mesmo assim.

8.4.14. Implementação de solicitações entre domínios

Vamos examinar o problema das solicitações entre domínios. No documento [Tutoriel AngularJS / Spring 4], é desenvolvida uma aplicação cliente/servidor em que o cliente é uma aplicação AngularJS:

  • as páginas HTML / CSS / JS do aplicativo Angular são originárias 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 se trata de uma falha de segurança. A aplicação só pode consultar o servidor de onde ela vem, ou seja, o servidor [1];

Na verdade, não é correto dizer que o navegador proíbe a aplicação Angular de consultar o servidor [2]. Na realidade, ela o consulta para perguntar se ele autoriza que um cliente que não seja do próprio servidor o consulte. Essa técnica de compartilhamento é chamada de CORS (Cross-Origin Resource Sharing). O servidor [2] dá sua autorização enviando cabeçalhos HTTP específicos.

Para demonstrar os problemas que podem surgir, vamos criar um aplicativo cliente/servidor em que:

  • o servidor será nosso servidor web / jSON;
  • o cliente será uma página simples HTML equipada com um código JavaScript que fará solicitações ao servidor web / jSON;

8.4.14.1. O projeto do cliente

  

O projeto é um projeto Maven com o seguinte arquivo [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 /> <!-- busca o pai no repositório -->
        </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: trata-se de um projeto Spring Boot;
  • linhas 29-32: utiliza-se a dependência [spring-boot-starter-web], que traz consigo um servidor Tomcat e o Spring MVC;

A página HTML é a seguinte:

 

Ela é gerada pelo código a seguir:


<!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">
        <!-- método 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 />
        <!-- valor lançado -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
        <!-- botão de confirmação -->
        <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: importa-se um código que iremos escrever;

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


// dados globais
var url;
var posted;
var response;
var method;

function requestServer() {
    // recuperamos as informações do formulário
    var urlValue = url.val();
    var postedValue = posted.val();
    method = document.forms[0].elements['method'].value;
    // faz-se uma chamada Ajax manualmente
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}

function doGet(url) {
    // é feita uma chamada Ajax manualmente
    $.ajax({
        headers : {
            ''Authorization': 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8080' + url,
        type : 'GET',
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // resultado em texto
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // erro de sistema
            response.text(jqXHR.responseText);
        }
    })
}

function doPost(url, posted) {
    // fazemos uma chamada Ajax manualmente
    $.ajax({
        headers : {
            'Autorização: 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8080' + url,
        type : 'POST',
        contentType : 'application/json',
        data : posted,
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // resultado em texto
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // erro de sistema
            response.text(jqXHR.responseText);
        }
    })
}

// ao carregar o documento
$(document).ready(function() {
    // recuperando as referências dos componentes da página
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});

Deixamos que o leitor compreenda esse código. Tudo já foi abordado em algum momento. Algumas linhas, no entanto, merecem uma explicação:

  • linha 11:
    • [document] designa o documento carregado pelo navegador, o que é chamado de DOM (Document Object Model),
    • [document.forms[0]] designa o primeiro formulário do documento, já que um documento pode conter vários. Neste caso, há apenas um,
    • [document.forms[0].elements['method']] refere-se ao elemento do formulário que possui o atributo [name='method']. Existem dois:

<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 que possui o atributo [name='method']. Sabemos que o valor enviado é o valor do atributo [value] do botão de opção marcado. Aqui, será, portanto, uma das cadeias ['get', 'post'];
  • linhas 23-25: estamos nos comunicando com um servidor que exige um cabeçalho HTTP [Authorization: Basic code]. Criamos esse cabeçalho para o usuário [admin / admin], que é o único com permissão para consultar o servidor;
  • linha 26: o usuário inserirá URL do tipo [/getAllMedecins, /supprimerRv, ...]. Portanto, é necessário preencher esses URL;
  • linha 28: o servidor retorna jSON, que é um texto. Indica-se o tipo [text/plain] como tipo de resultado para exibi-lo exatamente como foi recebido;
  • linha 33: exibição da resposta em texto do servidor;
  • linha 39: exibição de uma eventual mensagem de erro no formato de texto;
  • linha 52: para indicar que o cliente envia jSON;

Na aplicação cliente/servidor desenvolvida:

  • o cliente é um aplicativo web disponível em URL [http://localhost:8081]. É o aplicativo que estamos desenvolvendo;
  • o servidor é um aplicativo web disponível em URL [http://localhost:8080]. Esse é o nosso servidor web / jSON;

Como o cliente não é acessado pela mesma porta que o servidor, surge o problema 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 console iniciada 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);
    }

    // páginas estáticas
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
    }

    // configuração dispatcherServlet
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }

    // servidor Tomcat integrado
    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8081);
    }

}
  • linha 14: a classe [Client] é uma classe de configuração do Spring;
  • linha 15: configura-se uma aplicação Spring MVC. Essa anotação traz uma série de configurações automáticas;
  • linha 16: para redefinir alguns valores padrão do framework Spring MVC, é necessário estender a classe [WebMvcConfigurerAdapter];
  • linhas 23-26: o método [addResourceHandlers] permite especificar as pastas onde se encontram os recursos estáticos (html, css, js, etc.) da aplicação. Aqui, indica-se a pasta [static] localizada no Classpath do projeto:
  
  • linhas 29-37: configuração do bean [dispatcherServlet], que designa o servlet do Spring MVC;
  • linhas 40-43: o servidor Tomcat integrado funcionará na porta 8081;

8.4.14.2. O URL [/getAllMedecins]

Iniciamos:

  • o servidor web/json na porta 8080;
  • o cliente desse servidor na porta 8081;

em seguida, solicitamos o URL [http://localhost:8081/client.html] [1]:

  • em [2], executamos um GET no URL [http://localhost:8080/getAllMedecins];

Não obtemos resposta do servidor. Ao verificar o console de desenvolvimento (Ctrl-Shift-I), encontramos um erro:

  • no [1], estamos na aba [Network];
  • Em [2], percebe-se que a solicitação HTTP que foi feita não é [GET], mas sim [OPTIONS]. No caso de uma solicitação entre domínios, o navegador verifica com o servidor se determinadas condições estão atendidas, enviando-lhe uma solicitação HTTP [OPTIONS]. Nesse caso, as solicitações são aquelas indicadas pelos marcadores [5-6];
  • em [5], o navegador pergunta se o destino URL pode ser alcançado com um GET. O cabeçalho da 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]. Esse cabeçalho solicita uma resposta no cabeçalho HTTP [Access-Control-Allow-Origin] indicando que a origem especificada é aceita;
  • em [6], o navegador pergunta se os cabeçalhos HTTP, [accept] e [authorization] são aceitos. O cabeçalho da solicitação [Access-Control-Request-Headers] aguarda uma resposta com um cabeçalho HTTP [Access-Control-Allow-Headers] indicando que os cabeçalhos solicitados são aceitos;
  • ocorre um erro em [3]. Ao clicar no ícone, aparece o erro [4];
  • em [4], a mensagem indica que o servidor não enviou o cabeçalho HTTP [Access-Control-Allow-Origin], que indica se a origem da solicitação foi aceita;
  • em [7], é possível constatar que o servidor efetivamente não enviou esse cabeçalho. Consequentemente, o navegador recusou-se a realizar a solicitação HTTP GET solicitada inicialmente;

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

 

@Component
public class ApplicationModel implements IMetier {

    ...
    // dados de configuração
    private boolean corsAllowed = true;
    private boolean secured = true;
    
...
    public boolean isCorsAllowed() {
        return corsAllowed;
}
  • linha 6: criamos uma variável booleana que indica se aceitamos ou não clientes externos ao domínio do servidor;
  • linhas 10-12: o método de acesso a essa 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;

    // envio de opções ao cliente
    public void sendOptions(String origin, HttpServletResponse response) {
        // CORS permitido?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // definimos o cabeçalho CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // autorizando determinados cabeçalhos
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // autoriza-se o GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }

    // lista de médicos
    @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 processa o URL [/getAllMedecins] quando solicitado com o comando HTTP [OPTIONS];
  • linha 34: o método [getAllMedecins] aceita como parâmetros:
    • o objeto [@RequestHeader(value = "Origin", required = false)], que irá recuperar o cabeçalho HTTP [Origin] da solicitação. Esse cabeçalho foi enviado pelo remetente da solicitação:
Origin:http://localhost:8081

Indica-se que o cabeçalho HTTP [Origin] é opcional [required = false]. Nesse caso, se o cabeçalho estiver ausente, o parâmetro [String origin] terá o valor null. Como [required = true] é o valor padrão, uma exceção é lançada se o cabeçalho estiver ausente. Queríamos evitar esse caso;

  • linha 34:
    • o objeto [HttpServletResponse response], que será enviado ao cliente que fez a solicitação;

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

  • linha 35: delegamos o processamento da solicitação ao método das linhas 19-30;
  • linhas 15-16: o objeto [ApplicationModel] é injetado;
  • linhas 21-23: se a aplicação estiver configurada para aceitar solicitações 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 a solicitação entre domínios será aceita; caso contrário, será rejeitada;
  • linhas 25: se o cliente estiver no domínio [http://localhost:port], enviamos o cabeçalho HTTP:
Access-Control-Allow-Origin:  http://localhost:porta

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

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

Ao receber os cabeçalhos HTTP e [Access-Control-Request-X], o servidor responde com os cabeçalhos HTTP e [Access-Control-Allow-X], nos quais indica o que está autorizado. As linhas 23 a 26 limitam-se a repetir a solicitação do cliente para indicar que ela foi aceita;

Agora estamos prontos para novos testes. Lançamos a nova versão do serviço web e descobrimos que o problema persiste. Nada mudou. Se, na linha 35 acima, colocarmos uma saída de console, ela nunca será exibida, o que demonstra que o método [getAllMedecins] da linha 34 nunca é chamado.

Após algumas pesquisas, descobrimos que o Spring MVC processa por conta própria os comandos HTTP e [OPTIONS] com um tratamento padrão. Portanto, é sempre o Spring que responde, e nunca o método [getAllMedecins] da linha 34. Esse 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 {

    // configuração do DispatcherServlet para os cabeçalhos CORS
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
    
    // mapeamento jSON
...
  • linhas 10-11: o bean [dispatcherServlet] serve para definir o servlet que gerencia as solicitações dos clientes. Aqui, ele é do tipo [DispatcherServlet], o servlet do framework Spring MVC;
  • linha 12: cria-se uma instância do tipo [DispatcherServlet];
  • linha 13: solicita-se que o servlet encaminhe os comandos HTTP e [OPTIONS] para o aplicativo;
  • linha 14: configuramos o servlet dessa forma;

Repetimos os testes com essa nova configuração. Obtemos o seguinte resultado:

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

Vamos agora examinar a segunda solicitação:

  • em [1], a solicitação analisada;
  • em [2], que corresponde à solicitação GET. Graças à primeira solicitação [OPTIONS], o navegador recebeu as informações que solicitava. Agora, ele realiza a solicitação [GET], solicitada inicialmente;
  • 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 [3] do servidor é normal [HTTP/1.1 200 OK]. Portanto, deveríamos ter o documento solicitado. É possível que o servidor tenha realmente enviado o documento, mas que o navegador esteja impedindo seu uso porque exige que, para a solicitação GET também, a resposta inclua o cabeçalho HTTP [Access-Control-Allow-Origin:http://localhost:8081].

Alteramos o controlador [RdvMedecinsController] da seguinte maneira:


    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;
...
    // lista de médicos
    @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 {
        // a resposta
        Response<List<Medecin>> response;
        // cabeçalhos CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // status do aplicativo
...
  • linhas 1-2: o controlador [RdvMedecinsCorsController] é inserido;
  • linhas 7-8: injetamos nos parâmetros do método [getAllMedecins] o objeto HttpServletResponse, que encapsula a resposta a ser enviada ao cliente, e o cabeçalho HTTP [Origin];
  • linha 12: é chamado o método [sendOptions] do controlador [RdvMedecinsCorsController], o mesmo que foi chamado para processar a solicitação HTTP [OPTIONS]. Portanto, ele enviará os mesmos cabeçalhos HTTP que para essa solicitação;

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

 

Conseguimos, de fato, obter a lista de médicos.

8.4.14.3. Os demais URL e [GET]

Mostramos agora os demais URL consultados por meio de um GET. Nos controladores, o código das ações que as processam segue o modelo das ações que processaram anteriormente o URL [/getAllMedecins]. O leitor pode verificar o código nos exemplos fornecidos com este documento. Aqui está um exemplo:

no [RdvMedecinsCorsController]


    // lista de consultas de um médico
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
    public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin,    HttpServletResponse response) {
        sendOptions(origin, response);
}

no [RdvMedecinsController]


    // lista de consultas de um médico
    @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 {
        // a resposta
        Response<List<Rv>> response = null;
        boolean erreur = false;
        // cabeçalhos CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // status do aplicativo
...

A seguir, apresentamos capturas de tela da execução:

 
 
 
 
 
 

8.4.14.4. Os URL [POST]

Vamos examinar o seguinte caso:

  • fazemos um POST [1] para o URL [2];
  • no [3], o valor lançado. Trata-se de uma sequência jSON;
  • no total, queremos excluir o compromisso com o valor [id] 100;

Por enquanto, não alteramos nenhum código. O resultado obtido é o seguinte:

  • em [1], assim como nas solicitações [GET], o navegador realiza uma solicitação [OPTIONS];
  • em [2], ele solicita autorização de acesso para uma solicitação [POST]. Anteriormente, era [GET];
  • em [3], ele solicita autorização para enviar os cabeçalhos HTTP e [accept, authorization, content-type]. Anteriormente, havia apenas os dois primeiros cabeçalhos;

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


    public void sendOptions(String origin, HttpServletResponse response) {
        // CORS permitido?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // definimos o cabeçalho CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // autorizando determinados cabeçalhos
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // autoriza-se o GET
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
  • linha 9: adicionamos os cabeçalhos HTTP e [Content-Type] (não importa se são maiúsculas ou minúsculas);
  • linha 11: adicionamos o método HTTP [POST];

Com isso, os métodos [POST] são tratados da mesma forma que as solicitações [GET]. Aqui está o exemplo de URL [/supprimerRv]:

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 {
        // a resposta
        Response<Void> response = null;
        boolean erreur = false;
        // cabeçalhos CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // estado do aplicativo
        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 obtido é o seguinte:

 

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

 

8.4.14.5. Conclusion

Nosso aplicativo agora suporta solicitações entre domínios. Essas solicitações podem ser autorizadas ou não por meio da configuração na classe [ApplicationModel]:


    // dados de configuração
    private boolean corsAllowed = false;

8.5. Cliente programado do serviço web / jSON

Voltemos à arquitetura geral do aplicativo que pretendemos desenvolver:

A parte superior do esquema já foi escrita. Trata-se do servidor web / jSON. Agora vamos nos dedicar à parte inferior e, em primeiro lugar, à sua camada [DAO]. Vamos escrever essa camada e, em seguida, testá-la com um cliente de console. A arquitetura de teste será a seguinte:

8.5.1. O projeto do cliente de console

O projeto STS do cliente de console será o seguinte:

  

8.5.2. Configuração do Maven

O arquivo [pom.xml] do cliente da console é 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 /> <!-- pesquisa de pai no repositório -->
        </parent>

        <dependencies>
                <!-- Spring -->
                <dependency>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-web</artifactId>
                </dependency>
                <!-- biblioteca jSON utilizada pelo 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>
                <!-- componente utilizado pelo Spring RestTemplate -->
                <dependency>
                        <groupId>org.apache.httpcomponents</groupId>
                        <artifactId>httpclient</artifactId>
                </dependency>
        </dependencies>
</project>
  • linhas 15-20: o projeto pai do Spring Boot;
  • linhas 24-27: o cliente de console do servidor web / jSON é baseado em um componente chamado [RestTemplate] fornecido pela dependência [spring-web];
  • linhas 29-36: a serialização/desserialização dos objetos jSON requer uma biblioteca jSON. Utilizamos uma variante da biblioteca Jackson empregada pelo Spring Web;
  • linhas 38-41: no nível mais baixo, o componente [RestTemplate] se comunica com o servidor por meio dos sockets TCP/IP. Queremos definir o [timeout] desses, ou seja, o tempo máximo de espera por uma resposta do servidor. O componente [RestTemplate] não nos permite definir esse valor. Para fazer isso, vamos passar para o construtor [RestTemplate] um componente de baixo nível fornecido pela dependência [org.apache.httpcomponents.httpclient]. É essa dependência que nos permitirá definir o [timeout] da comunicação;

8.5.3. O pacote [rdvmedecins.client.entities]

  

O pacote [rdvmedecins.client.entities] reúne todas as entidades que o serviço web / jSON envia por meio de seus diversos URL. Não vamos detalhá-las novamente. Basta dizer que as entidades JPA e [Client, Creneau, Medecin, Rv, Personne] tiveram todas as suas anotações JPA e jSON removidas. Aqui está, por exemplo, a classe [Rv]:


package rdvmedecins.client.entities;

import java.util.Date;

public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;

    // dia da consulta
    private Date jour;

    // uma reunião está vinculada a um cliente
    private Client client;

    // um compromisso está vinculado a um horário
    private Creneau creneau;

    // chaves externas
    private long idClient;
    private long idCreneau;

    // fabricante padrão
    public Rv() {
    }

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

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

// getters e setters
...
}

8.5.4. O pacote [rdvmedecins.client.requests]

  

O pacote [rdvmedecins.client.requests] reúne as duas classes cujo valor jSON é enviado para URL, [/ajouterRv] e [supprimerRv]. Elas são idênticas às que existem no lado do servidor.

8.5.5. O pacote [rdvmedecins.client.responses]

  

[Response] é o tipo de todas as respostas do serviço web / jSON. Trata-se de um tipo genérico:


package rdvmedecins.client.responses;

import java.util.List;

public class Response<T> {

    // ----------------- propriedades
    // status da operação
    private int status;
    // eventuais mensagens de erro
    private List<String> messages;
    // o corpo da resposta
    private T body;

    // construtores
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters e setters
...
}
  • linha 5: o tipo [T] varia de acordo com o URL do serviço web / jSON;

8.5.6. O pacote [rdvmedecins.client.dao]

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

8.5.7. O pacote [rdvmedecins.client.config]

  

A classe [DaoConfig] configura o aplicativo. 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() {
        // criação do componente RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // resultado
        return restTemplate;
    }
    
    // mapeadores 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. Nele será encontrado o componente [Dao];
  • linhas 17-24: definem um singleton do Spring chamado [restTemplate] (o nome do método). Esse método retorna uma instância [RestTemplate], que é a ferramenta básica fornecida pelo Spring para se comunicar com um serviço web / jSON;
  • linha 21: poderíamos escrever [RestTemplate restTemplate = new RestTemplate() ;]. Isso é suficiente na maioria dos casos. Mas, neste caso, queremos definir os [timeout] do cliente. Para isso, injetamos no componente [RestTemplate] um componente de baixo nível do tipo [HttpComponentsClientHttpRequestFactory] (linha 20), que nos permitirá definir esses [timeout]. A dependência necessária do Maven já foi apresentada;
  • linhas 28-57: definem os mapeadores jSON. Esses são os mapeadores jSON utilizados no lado do servidor (ver parágrafo 8.4.11.3) para serializar o tipo T da resposta [Response<T>]. Esses mesmos conversores serão agora utilizados no lado do cliente para desserializar o tipo T;

8.5.8. A interface [IDao]

Voltemos à arquitetura do aplicativo:

A camada [DAO] é um adaptador entre a camada [console] e as camadas URL expostas pelo serviço web / jSON. 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 {
    // URL do serviço web
    public void setUrlServiceWebJson(String url);

    // tempo limite
    public void setTimeout(int timeout);

    // autenticação
    public void authenticate(User user);

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

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

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

    // localizar um cliente identificado por seu ID
    public Client getClientById(User user, long id);

    // localizar um cliente identificado por seu ID
    public Medecin getMedecinById(User user, long id);

    // localizar uma consulta identificada pelo ID
    public Rv getRvById(User user, long id);

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

    // adicionar um RV
    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);

    // excluir um RV
    public void supprimerRv(User user, long idRv);

    // lista das consultas de um médico em um determinado dia
    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 que permite definir a raiz do serviço web / jSON, por exemplo, [http://localhost:8080];
  • linha 17: o método que permite definir o [timeout] no lado do cliente. Queremos controlar esse parâmetro porque alguns clientes HTTP às vezes demoram muito tempo esperando por uma resposta que não chegará;
  • linha 20: o método que permite identificar um usuário [login, passwd]. Lança uma exceção se o usuário não for reconhecido;
  • linhas 22-53: a cada URL exposta pelo serviço web / jSON está associado um método da interface cuja assinatura deriva da assinatura do método do lado do servidor que processa a URL exposta. Tomemos, por exemplo, o 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 do URL. Esses serão os parâmetros de entrada do método associado a esse URL do lado do cliente;
  • linha 2: vemos que o método do servidor retorna um tipo [Response<String>]. Esse tipo [String] é o tipo do valor jSON, que é de tipo [AgendaMedecinJour]. O tipo do resultado do método associado a esse URL no lado do cliente será [AgendaMedecinJour];

No lado do cliente, declara-se o seguinte método:


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

Essa assinatura é adequada quando o servidor envia uma resposta [int status, List<String> messages, String body] com [status==0]. Nesse caso, temos [messages==null && body!=null]. Ela não é adequada quando [status!=0]. Nesse caso, temos [messages!=null && body==null]. Precisamos, de alguma forma, sinalizar que ocorreu um erro. Para isso, 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;
    // código de erro
    private int status;
    // lista de mensagens de erro
    private List<String> messages;

    public RdvMedecinsException() {
    }

    public RdvMedecinsException(int code, List<String> messages) {
        super();
        this.status = code;
        this.messages = messages;
    }

    // getters e setters
...
}
  • linhas 9 e 11: a exceção herdará 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 controlada, ou seja, não há obrigação de tratá-la com um try/catch nem de declará-la na assinatura dos métodos da interface;

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


package rdvmedecins.client.entities;

public class User {

    // dados
    private String login;
    private String passwd;

    // construtores
    public User() {
    }

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

    // getters e setters
    ...
}

De fato, cada troca de dados com o serviço web / jSON deve ser acompanhada de um cabeçalho de autenticação HTTP.

8.5.9. O pacote [rdvmedecins.clients.console]

Agora que conhecemos a interface da camada [DAO], podemos apresentar o aplicativo de console.

  

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 {

    // serializador jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // tempo limite das conexões em milissegundos
    static private int TIMEOUT = 1000;

    public static void main(String[] args) throws IOException {
        // obtém-se uma referência na camada [DAO]
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // define-se o URL do serviço web / JSON
        dao.setUrlServiceWebJson("http://localhost:8080");
        // define-se o tempo limite em milissegundos
        dao.setTimeout(TIMEOUT);

        // Autenticação
        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);
        }

        // lista de clientes
        message = "/getAllClients";
        try {
            showResponse(message, dao.getAllClients(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // lista de médicos
        message = "/getAllMedecins";
        try {
            showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // lista de horários do médico 2
        message = "/getAllCreneaux/2";
        try {
            showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // cliente nº 1
        message = "/getClientById/1";
        try {
            showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // médico nº 2
        message = "/getMedecinById/2";
        try {
            showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // horário n.º 3
        message = "/getCreneauById/3";
        try {
            showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // consulta n.º 4
        message = "/getRvById/4";
        try {
            showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // adição de uma consulta
        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);
        }

        // lista de consultas do médico 1 em 08/01/2015
        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);
        }

        // agenda do médico 1 em 08/01/2015
        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);
        }
        // exclusão da consulta adicionada
        message = String.format("/supprimerRv [idRv=%s]", idRv);
        try {
            dao.supprimerRv(new User("admin", "admin"), idRv);
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // lista de consultas do médico 1 em 08/01/2015
        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);
        }
        // fechamento do contexto
        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 as anotações de configuração de uma aplicação Spring. Passamos para seu construtor, a classe [AppConfig], que configura a aplicação;
  • linha 26: obtemos uma referência à camada [DAO];
  • linhas 27-30: configuramos essa camada;
  • 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 a cargo do leitor a tarefa de associar os resultados ao código. Este mostra como chamar cada método da camada [DAO]. Basta 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 o caso;
  • linhas 30-31: adiciona-se uma consulta ao médico nº 1;
  • linhas 32-33: vemos essa consulta. É a única do dia;
  • linhas 34-35: ele também aparece na agenda do médico;
  • linhas 36-37: a consulta desapareceu. O código a excluiu nesse meio tempo;

Os logs do console são controlados pelos seguintes arquivos:

 

[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">
                <!-- os codificadores são atribuídos por padrão ao tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- controle do nível dos logs -->
        <root level="info"> <!-- desativado, informação, depuração, aviso -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>

8.5.10. Implementação da camada [DAO]

Resta-nos agora apresentar o núcleo da camada [DAO]: a implementação de sua interface [IDao]. Faremos isso gradualmente.

 

A interface [IDao] é implementada pela classe abstrata [AbstractDao] e por sua classe filha [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 {

    // dados
    @Autowired
    protected RestTemplate restTemplate;
    protected String urlServiceWebJson;

    // URL serviço web / jSON
    public void setUrlServiceWebJson(String url) {
        this.urlServiceWebJson = url;
    }

    public void setTimeout(int timeout) {
        // define o tempo limite para as solicitações do cliente web
        HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
                .getRequestFactory();
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
    }

    private String getBase64(User user) {
        // codifica-se o usuário e sua senha em base 64 — requer
        // 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())));
    }

    // solicitação genérica
    protected String getResponse(User user, String url, String jsonPost) {
...
    }

}
  • linha 20: a classe é abstrata, o que nos impede de designá-la como um componente Spring. Será sua classe filha que será designada como tal;
  • linhas 23-24: injetamos o bean [restTemplate], que definimos na classe de configuração [AppConfig];
  • linha 25: o URL é a raiz do serviço web /jSON;
  • linhas 32-38: definem o tempo limite do cliente enquanto ele aguarda uma resposta do servidor;
  • linha 34: recuperamos o componente [HttpComponentsClientHttpRequestFactory] que havíamos injetado no bean [restTemplate] durante sua criação (ver [AppConfig]);
  • linha 36: definimos o tempo máximo de espera do cliente quando ele estabelece uma conexão com o servidor;
  • linha 37: definimos o tempo máximo de espera do cliente enquanto ele aguarda uma resposta a uma de suas solicitações;

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


    // solicitação genérica
    protected String getResponse(User user, String url, String jsonPost) {
...
    }
  • linha 2: os parâmetros de [getResponse] são os seguintes:
    • [User user]: o usuário que está se conectando;
    • [String url]: o URL a ser consultado. Trata-se da parte final do URL, sendo que a primeira parte é fornecida pelo campo [urlServiceWebJson] da classe;
    • [String jsonPost]: a sequência jSON a ser enviada. Se esse valor estiver presente, então o URL será solicitado com um POST; caso contrário, será com um GET;

Vamos continuar:


// solicitação genérica
    protected String getResponse(User user, String url, String jsonPost) {
        // URL: URL para contato
        // jsonPost: o valor jSON deve ser enviado
        try {
            // execução da solicitação
            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);
            }
            // a consulta está sendo executada
            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 faz a solicitação ao servidor e recebe sua resposta. O componente [RestTemplate] oferece um número significativo de métodos de comunicação com o servidor. Teríamos podido escolher outro método que não o [exchange]. O segundo parâmetro da chamada define o tipo da resposta esperada, neste caso, uma string jSON. O primeiro parâmetro é a solicitação do tipo [RequestEntity] (linha 7). O resultado do método [exchange] é do tipo [ResponseEntity<String>]. O tipo [ResponseEntity] encapsula a resposta completa do servidor, incluindo os cabeçalhos HTTP e o documento enviado por ele. Da mesma forma, o tipo [RequestEntity] encapsula toda a solicitação do cliente, incluindo os cabeçalhos HTTP e o eventual valor enviado por POST;
  • linha 23: é o corpo do objeto [ResponseEntity<String>] que é retornado ao método chamador, ou seja, a string jSON enviada pelo servidor;
  • linhas 9-21: precisamos construir a solicitação do tipo [RequestEntity]. Ela varia dependendo se utilizamos um GET ou um POST para fazer a solicitação;
  • linha 9: a solicitação para um GET. A classe [RequestEntity] oferece métodos estáticos para criar as consultas GET, POST, HEAD,... O método [RequestEntity.get] permite criar uma consulta GET encadeando os diferentes métodos que a constroem:
    • o método [RequestEntity.get] aceita como parâmetro o URL de destino na forma de uma instância URI,
    • o método [accept] permite definir os elementos do cabeçalho HTTP [Accept]. Aqui, indicamos que aceitamos o tipo [application/json] que o servidor enviará;
    • o resultado dessa sequência de métodos é um tipo [HeadersBuilder];
  • linhas 10-12: caso o parâmetro [User user] não seja null, incluímos o cabeçalho HTTP [Authorization] na solicitação;
  • linha 13: o método [HeadersBuilder.build] utiliza essas diferentes informações para construir o tipo [RequestEntity] da consulta;
  • linha 15: a consulta para um POST. O método [RequestEntity.post] permite criar uma consulta POST encadeando os diferentes métodos que a constroem:
    • o método [RequestEntity.post] aceita como parâmetro o URL de destino na forma de uma instância URI,
    • o método [header] permite definir os cabeçalhos HTTP que se deseja utilizar, neste caso, o de autorização,
    • o método [header] a seguir inclui na solicitação o cabeçalho [Content-Type: application/json] para indicar que o valor enviado chegará na forma de uma string jSON;
    • o método [accept] permite indicar que aceitamos o tipo [application/json] que o servidor enviará;
  • linhas 17-19: caso o parâmetro [User user] não seja null, incluímos o cabeçalho HTTP [Authorization] na solicitação;
  • linha 20: o método [BodyBuilder.body] define o valor lançado. Esse valor é o segundo parâmetro do método genérico [getResponse] (linha 2);
  • linhas 25-28: caso ocorra qualquer erro, é lançada uma exceção do tipo [RdvMedecinsException];

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


    // lista de mensagens de erro de uma exceção
    protected static List<String> getMessagesForException(Exception exception) {
        // recuperando a lista de mensagens de erro da exceção
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            // recupera-se a mensagem somente se ela !=null e não estiver vazia
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // próxima causa
            cause = cause.getCause();
        }
        return erreurs;
}

O método privado [getBase64] fornece o código Base64 da string 'login:passwd' para o cabeçalho de autenticação HTTP:


    private String getBase64(User user) {
        // codifica o usuário e sua senha em base 64 — requer 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 maneira:


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 {

    // mapeadores 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. Aqui, utilizou-se a anotação [@Service]. Teria sido possível continuar utilizando a anotação [@Component] usada até agora;
  • 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 esquema. Vamos detalhar uma operação GET e uma operação POST.

Primeiramente, uma consulta [GET]:


public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
        // a resposta
        Response<AgendaMedecinJour> response;
        // a agenda
        String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
        try {
            // a agenda 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));
        }
        // análise da resposta
        int status = response.getStatus();
        if (status != 0) {
            throw new RdvMedecinsException(status, response.getMessages());
        } else {
            return response.getBody();
        }
}
  • linha 5: é chamado o método genérico [getResponse]. Os parâmetros efetivamente utilizados são os seguintes:
    • 1: o usuário;
    • 2: o destino URL;
    • 3: o valor a ser enviado. Neste caso, não há nenhum;
  • linha 5: a chamada não foi envolvida por um try/catch. O método [getResponse] pode lançar uma exceção do tipo [RdvMedecinsException]. Se for lançada, essa exceção será propagada para o método que chamou o método [getAgendaMedecinJour] acima;
  • linha 8: oURL [/getAgendaMedecinJour] envia um tipo [Response<AgendaMedecinJour>] que foi serializado em jSON no lado do servidor pelo mapeador jSON [jsonMapperLongRv]. Esse mesmo mapeador é usado para desserializar a string jSON recebida;
  • linhas 10-13: se ocorrer um erro na linha 9, é lançada uma exceção do tipo [RdvMedecinsException];
  • linhas 16-21: analisa-se a resposta enviada pelo servidor;
  • linhas 17-18: se o servidor tiver sinalizado um erro, lança-se uma exceção com as informações transmitidas pelo servidor;
  • linhas 19-21: caso contrário, exibe-se a agenda do médico;

A solicitação POST analisada será a seguinte:


    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
        // a resposta
        Response<Rv> response;
        try {
            // o compromisso
            String jsonResponse = getResponse(user, "/ajouterRv",
                    jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
            // o 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));
        }
        // análise da resposta
        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 usuário;
    • 2: o URL de destino,
    • 3: o valor enviado: passa-se o valor jSON de um tipo [PostAjouter] construído com as informações recebidas como parâmetros pelo método. Utiliza-se um mapeador jSON sem filtros;
  • linha 9: no lado do servidor, foi o mapeador jSON [jsonMapperLongRv] que serializou a resposta do servidor. No lado do cliente, utiliza-se esse mesmo mapeador para deserializá-la;
  • linha 6: o URL [/ajouterRv] retorna o valor jSON de um tipo [Response<Rv>];
  • linhas 4-11: aqui, o método [getResponse] foi colocado em um try/catch porque a serialização do valor enviado pode lançar uma exceção. O método [getResponse] pode lançar uma exceção [RdvMedecinsException]. Nesse caso, basta relançá-la (linhas 11-12);

O código a seguir (linhas 13-24) é semelhante ao que acabamos de analisar. A única diferença em relação a uma operação GET é, portanto, o segundo parâmetro do método [getResponse], que deve ser o valor jSON do valor a ser lançado.

Os demais métodos seguem o mesmo modelo.

8.5.11. Anomalia

Ao realizar diversos testes, encontramos 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 {

    // serializador jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // tempo limite das conexões em milissegundos
    static private int TIMEOUT = 1000;

    public static void main(String[] args) throws IOException {
        // obtém-se uma referência na camada [DAO]
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // define-se o URL do serviço web / JSON
        dao.setUrlServiceWebJson("http://localhost:8080");
        // definimos os tempos de espera em milissegundos
        dao.setTimeout(TIMEOUT);

        // Autenticação
        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);
        }

        // Autenticação
        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);
        }

        // Autenticação
        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);
        }

        // encerramento do contexto
        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: autentica-se o usuário [admin, admin];
  • linhas 40-47: autentica-se o usuário [admin, x], que, portanto, possui uma senha incorreta;
  • linhas 49-56: autentica-se o usuário [user, user], que é um usuário existente, mas não autorizado;

Aqui estão 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: contra todas as expectativas, o usuário [admin, x] foi aceito;

Se colocarmos as linhas 33-38 do código como comentários, 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

que é o resultado esperado. É como se, quando o usuário [admin, admin] se autenticou com sucesso pela primeira vez, sua senha não fosse mais necessária nas vezes seguintes. É exatamente isso que acontece. O Spring Security utiliza, por padrão, uma sessão que faz com que, uma vez que o usuário tenha se autenticado, ele não precise mais se autenticar nas solicitações seguintes. É possível alterar a configuração de [Spring Security] no servidor web / jSON para que isso não aconteça mais:

  

O arquivo [SecurityConfig] deve ser alterado da seguinte forma:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
            // sem sessão
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
  • a linha 5 determina que não haja sessão de segurança;

Isso resolveu o problema da anomalia.

8.6. Código do servidor Spring / Thymeleaf

8.6.1. Introdução

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

  • o servidor web [Web2] / jSON 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. De fato, o [Web1] enviará fluxos HTML encapsulados em uma cadeia jSON. A arquitetura cliente/servidor é a seguinte:

  • Temos uma arquitetura cliente [2] / servidor [1], na qual o cliente e o servidor se comunicam em jSON;
  • em [1], a camada web Spring MVC / Thymeleaf fornece visualizações, fragmentos de visualização e dados em jSON. O servidor é, portanto, um servidor web / jSON, assim como o servidor [Web1]. Ele também é sem estado;
  • em [2]: o código JavaScript incorporado na visualização carregada na inicialização do aplicativo é estruturado em camadas:
    • a camada [présentation] lida com as interações com o usuário,
    • a camada [DAO] lida com o acesso aos dados por meio do servidor [Web2];
  • o cliente [2] armazenará certas visualizações em cache para aliviar a carga do servidor;

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

  • introdução ao framework CSS Bootstrap;
  • criação das visualizações;
  • criação do controlador;

Em seguida, separadamente, construiremos o cliente JS do servidor [Web1]. Para demonstrar claramente que esse cliente possui certa independência em relação ao servidor [Web1], vamos criá-lo com a ferramenta [Webstorm], em vez de usar o STS.

A seguir, alguns detalhes serão ignorados, pois poderiam nos fazer perder de vista o que é importante: a organização do código. O leitor interessado poderá encontrar o código completo no site deste documento.

8.6.2. O projeto STS

  • em [1], os códigos Java;
  • em [2], as visualizações;

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 se baseia na camada [DAO] que acabamos de construir;

A configuração Java é feita por meio de dois arquivos:

 

A camada [web] é configurada pelo seguinte arquivo [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 {

    // ----------------- configuração da camada [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;
    }

    // configuração do DispatcherServlet para os cabeçalhos CORS
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }

}

Já nos deparamos, em algum momento, com todos os elementos dessa configuração. Basta lembrar que as linhas 42 a 47 são necessárias quando se deseja consultar o servidor com solicitações entre domínios (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";
    // raiz do serviço web / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // tempo limite em milissegundos
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;

    ...
    
}
  • linha 11: [AppConfig] importa a configuração da camada [DAO] e da camada [web];
  • linhas 15-16: os identificadores que permitirão que a aplicação acesse o boot da aplicação para armazenar em cache os médicos e os clientes;
  • linha 18: o URL do serviço web / jSON [Web1];
  • linha 20: o timeout das chamadas HTTP do aplicativo;
  • linha 22: um valor booleano para autorizar ou não as chamadas entre domínios;

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

  

server.port=8081

8.6.3. As funcionalidades do aplicativo

Elas foram descritas no parágrafo 8.2. Vamos recapitulá-las agora. Com um navegador, solicita-se o URL [http://localhost:8081/boot.html]:

  • em [1], a página inicial do aplicativo;
  • em [2] e [3], o nome de usuário e a senha de quem deseja utilizar o aplicativo. Existem dois usuários: admin/admin (login/senha) com uma função (ADMIN) e user/user com uma função (USER). Apenas a função ADMIN tem permissão para usar o aplicativo. A função USER está presente apenas para mostrar a resposta do servidor nesse caso de uso;
  • em [4], o botão que permite conectar-se ao servidor;
  • em [5], o idioma do aplicativo. Há dois: o francês, por padrão, e o inglês;
  • em [6], o URL do servidor [rdvmedecins-springthymeleaf-server];
  • no [1], faz-se o login;
  • uma vez conectado, é possível escolher o médico com quem deseja marcar uma consulta [2] e o dia da consulta [3]. Assim que o médico e o dia forem informados, a agenda é exibida automaticamente:
  • Depois de obter a agenda do médico, é possível reservar um horário [5];
  • em [6], seleciona-se o paciente para a consulta e confirma-se essa escolha em [7];

Assim que a consulta for confirmada, o usuário é redirecionado automaticamente para a agenda, onde a nova consulta já está registrada. Essa consulta poderá ser excluída posteriormente em [8].

As principais funcionalidades já foram descritas. Elas são simples. Vamos encerrar com o gerenciamento do idioma:

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

8.6.4. Etapa 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 apresentamos a seguir.

8.6.4.1. O projeto dos exemplos

O formato dos exemplos será o seguinte:

  • em [1]: o projeto como um todo;
  • em [2]: os códigos Java;
  • em [3]: os scripts JavaScript;
  • em [4]: as bibliotecas JavaScript;
  • em [5]: as visualizações Thymeleaf;
  • em [6]: as folhas de estilo;

8.6.4.1.1. Configuração do Maven

O arquivo [pom.xml] é de um projeto Maven Thymeleaf:


<?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 /> <!-- pesquisa do pai no repositório -->
    </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 deparamos com esse tipo de código.

8.6.4.1.3. O controlador Spring
  

O controlador [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 servem apenas para exibir visualizações processadas pelo Thymeleaf.

8.6.4.1.4. O arquivo [application.properties]

O arquivo [application.properties] configura o servidor Tomcat integrado:


server.port=8082

8.6.4.2. Exemplo nº 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>
        <!-- Núcleo do Bootstrap 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">
            <!-- Jumbotron do Bootstrap -->
            <div th:include="jumbotron"></div>
            <!-- conteúdo -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- erro -->
            <div id="erreur" class="alert alert-danger">
                <span>Ici, un texte d'erreur</span>
            </div>
        </div>
    </body>
</html>
  • linha 7: o arquivo CSS do framework Bootstrap;
  • linha 8: um arquivo CSS local;
  • linha 13: exibe [1];
  • linhas 19-21: exibem [2];
  • linha 11: a classe CSS [container] define uma área de exibição dentro do navegador;
  • linha 19: a classe CSS [alert] exibe uma área colorida. A classe [alert-danger] utiliza uma cor predefinida. Existem várias delas, como [alert-info, alert-warning,...];

O jumbotron [1] é gerado pela seguinte visualização [jumbotron.xml]:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <!-- Jumbotron do Bootstrap -->
    <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 na linha;
  • linha 7: nessas duas colunas, insere-se uma imagem;
  • linhas 9-15: nas outras 10 colunas, insere-se o texto;

8.6.4.3. Exemplo nº 2: a barra de navegação

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

A novidade é a barra de navegação [1] com seu formulário de preenchimento e seus 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">
            <!-- barra de navegação -->
            <div th:include="navbar1"></div>
            <!-- Jumbotron do Bootstrap -->
            <div th:include="jumbotron"></div>
            <!-- conteúdo -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- informações -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 10: importa-se jQuery;
  • linha 11: um script local JS;
  • linha 16: a barra de navegação;

A barra de navegação é gerada pela seguinte visualização [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" />
                <!-- formulário de identificação -->
                <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] definirá o estilo da barra de navegação. A classe [navbar-inverse] atribui a ela um fundo preto. A classe [navbar-fixed-top] fará com que, ao rolar a página exibida pelo navegador, a barra de navegação permaneça na parte superior da tela;
  • linhas 5-13: definem a área [1]. Trata-se, tipicamente, de uma série de classes que eu não entendo. Eu uso o componente tal como está;
  • linhas 14-26: definem uma área “responsiva” da barra de comando. Em um smartphone, essa área desaparece em uma área de menu;
  • linha 15: uma imagem atualmente oculta;
  • linhas 17-25: a classe [navbar-form] estiliza um formulário da barra de comando. A classe [navbar-right] o posiciona à direita dela;
  • linhas 21-23: os dois campos de preenchimento do formulário da linha 17, [2]. Elas estão dentro de uma classe [form-group], que define os elementos de um formulário, e cada uma delas possui 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: ao clicar no botão [Connexion], a função JS a seguir é executada:

function connecter() {
    showInfo("Connexion demandée...");
}

function showInfo(message) {
    $("#info").text(message);
}

Veja um exemplo:

Image

8.6.4.4. Exemplo nº 3: o botão com lista

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

  • A novidade é o controle de lista [1], também chamado de “dropdown”;

O código da 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>
        <!-- Núcleo do Bootstrap CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Núcleo do Bootstrap JavaScript ================================================== -->
        <script src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script src="resources/vendor/bootstrap.js"></script>
        <!-- script local -->
        <script type="text/javascript" src="resources/js/bs-03.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- barra de navegação -->
            <div th:include="navbar2"></div>
            <!-- Jumbotron do Bootstrap -->
            <div th:include="jumbotron"></div>
            <!-- conteúdo -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- informações -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 11: o botão com lista requer o arquivo 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" />
                <!-- formulário de identificação -->
                <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>
                    <!-- idiomas -->
                    <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>
    <!-- página inicial -->
    <script th:inline="javascript">
        /*<![CDATA[*/
             // inicializando a página
            initNavBar2();
        /*]]>*/
    </script>
</section>
  • linhas 25-40: definem o botão com lista;
  • linha 27: a classe [btn-danger] define sua cor vermelha;
  • linhas 32-39: os elementos da lista. São links, cada um associado a uma função JS;
  • linhas 46-51: um script JS executado após o carregamento do documento;

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


function initNavBar2() {
    // menu suspenso de idiomas
    $('.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]. O [$('.dropdown-toggle')] localiza o elemento que possui a classe [dropdown-toggle]. Trata-se do botão com lista (linha 28 da visualização). A função JS [dropdown()], definida no arquivo JS [bootstrap.js], é aplicada a ele. Somente após essa operação é que o botão passa a funcionar como um botão de lista;
  • linhas 10-21: a função executada ao selecionar um idioma;

Veja um exemplo:

Image

8.6.4.5. Exemplo nº 4: um menu

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

Foi adicionado um menu [1].

A visualização [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>
        <!-- Núcleo do Bootstrap CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Núcleo do Bootstrap JavaScript ================================================== -->
        <script src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script src="resources/vendor/bootstrap.js"></script>
        <!-- script local -->
        <script type="text/javascript" src="resources/js/bs-04.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- barra de navegação -->
            <div th:include="navbar3"></div>
            <!-- Jumbotron do Bootstrap -->
            <div th:include="jumbotron"></div>
            <!-- conteúdo -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- informações -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 18: insere-se uma nova barra de navegação;

A visualização [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>
                <!-- botões à direita -->
                <div class="navbar-form navbar-right" role="form">
                    <!-- sair -->
                    <button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
                    <!-- idiomas -->
                    <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>
    <!-- página inicial -->
    <script th:inline="javascript">
        /*<![CDATA[*/
             // inicializando a página
            initNavBar3();
        /*]]>*/
    </script>
</section>
  • linhas 16-29: criam o menu com quatro opções, cada uma delas vinculada a um script JS;
  • linhas 55-60: um script executado ao carregar a página;

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


...
function initNavBar3() {
    // menu suspenso de idiomas
    $('.dropdown-toggle').dropdown();
    // a imagem animada
    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) {
    // os links do menu
    var lnkAfficherAgenda = $("#lnkAfficherAgenda");
    var lnkAccueil = $("#lnkAccueil");
    var lnkValiderRv = $("#lnkValiderRv");
    var lnkRetourAgenda = $("#lnkRetourAgenda");
    // colocamos em um dicionário
    var options = {
        "lnkAccueil" : lnkAccueil,
        "lnkAfficherAgenda" : lnkAfficherAgenda,
        "lnkValiderRv" : lnkValiderRv,
        "lnkRetourAgenda" : lnkRetourAgenda
    }
    // ocultamos todos os links
    for ( var key in options) {
        options[key].hide();
    }
    // exibimos aqueles que são solicitados
    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 com a lista de idiomas;
  • linhas 6-7: a imagem animada fica oculta;
  • linhas 26-48: uma função [setMenu] que permite indicar quais opções devem ficar visíveis;

Vamos para o console de desenvolvimento (Ctrl-Shift-I) e digitemos o seguinte código [1]:

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

8.6.4.6. Exemplo nº 5: uma lista suspensa

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

A novidade está em [1]. Aqui, utilizamos um componente fornecido fora do Bootstrap, [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].

O código da visualização [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>
        <!-- Núcleo do Bootstrap 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" />
        <!-- Núcleo do Bootstrap 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 local -->
        <script type="text/javascript" src="resources/js/bs-05.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- barra de navegação -->
            <div th:include="navbar3"></div>
            <!-- Jumbotron do Bootstrap -->
            <div th:include="jumbotron"></div>
            <!-- conteúdo -->
            <div id="content" th:include="choixmedecin">
            </div>
            <!-- informações -->
            <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 arquivo 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>
    <!-- script local -->
    <script th:inline="javascript">
        /*<![CDATA[*/
             // inicializando a página
            initChoixMedecin();
        /*]]>*/
    </script>
</section>
  • linhas 7-12: trata-se de uma tag [select] clássica, porém com uma classe específica [combobox]. O atributo [data-style="btn-primary"] atribui ao componente sua cor azul;
  • linhas 16-21: um script executado ao carregar a página;

O arquivo 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() {
    // o seletor de médicos
    $('#idMedecin').selectpicker();
    // o menu
    setMenu([ "lnkAfficherAgenda" ]);
}
  • linhas 7-12: a função executada ao carregar a página;
  • linha 9: a instrução que transforma o [select] da página em uma lista suspensa do Bootstrap. O [$('#idMedecin')] faz referência ao [select] (linha 7 da visualização [choixmedecin]) e a função JS [selectpicker] provém do arquivo JS [bootstrap-select.js];
  • linha 11: exibe-se apenas uma das opções do menu;
  • linhas 2-5: a função JS é executada quando se clica na opção de menu [Agenda];
  • linha 3: recupera-se o valor da opção selecionada na lista suspensa: [$('#idMedecin option:selected')] localiza primeiro o componente [id=idMedecin] e, em seguida, dentro desse componente, a opção selecionada. A operação [..].val() recupera, em seguida, o valor do elemento encontrado, ou seja, o atributo [value] da opção selecionada;

Veja um exemplo de escolha de um médico:

 

8.6.4.7. Exemplo nº 6: um calendário

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

Image

A escolha de um médico ou de uma data aciona uma função JS que exibe tanto o médico quanto a data selecionados. Veja um exemplo:

 

Por meio do botão “Lista de idiomas”, é possível alterar o calendário (e apenas o calendário) para o 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 visualização [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>
        <!-- Núcleo do Bootstrap 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" />
        <!-- Núcleo do Bootstrap 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 local -->
        <script type="text/javascript" src="resources/js/bs-06.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- barra de navegação -->
            <div th:include="navbar3"></div>
            <!-- Jumbotron do Bootstrap -->
            <div th:include="jumbotron"></div>
            <!-- conteúdo -->
            <div id="content" th:include="choixmedecinjour">
            </div>
            <!-- informações -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 8: o arquivo CSS do componente [bootstrap-datepicker];
  • linha 16: o arquivo JS do componente [bootstrap-datepicker];
  • linha 17: o arquivo JS para gerenciar um calendário francês. Por padrão, ele está em inglês;
  • linha 15: o arquivo JS de uma biblioteca chamada [moment], que dá acesso a inúmeras funções de cálculo de tempo [http://momentjs.com/];
  • linha 28: a visualização do calendário;

A visualização [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>
    <!-- script local -->
    <script th:inline="javascript">
        /*<![CDATA[*/
             // inicializando a página
            initChoixMedecinJour();
        /*]]>*/
    </script>
</section>
  • linhas 17-23: o calendário;
  • linha 18: a classe [btn-primary] define sua cor azul;
  • linha 18: o atributo [disabled="true"] impede que a data seja inserida manualmente. É necessário usar o calendário;
  • linha 16: o calendário foi colocado em uma seção [id="calendar_container"]. Para alterar o idioma do calendário, é necessário excluí-lo e, em seguida, regenerá-lo. Portanto, excluiremos o conteúdo do componente [id="calendar_container"] e, em seguida, inseriremos o novo calendário com o novo idioma;
  • linhas 28-33: o código de inicialização da página;

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


...
var calendar_infos = {};

function initChoixMedecinJour() {
    // calendário
    var calendar_container = $("#calendar_container");
    calendar_infos = {
        "container" : calendar_container,
        "html" : calendar_container.html(),
        "today" : moment().format('YYYY-MM-DD'),
        "langue" : "fr"
    }
    // criação do calendário
    updateCalendar();
    // a seleção de médicos
    $('#idMedecin').selectpicker();
    $('#idMedecin').change(function(e) {
        afficherAgenda();
    })
    // o menu
    setMenu([]);
}
  • linha 2: o calendário é gerenciado por várias funções JS. A variável [calendar_infos] reunirá informações sobre o calendário. Ela é global para que possa ser acessada pelas diferentes funções;
  • linha 6: identifica-se o contêiner 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 essas duas informações, é possível excluir o calendário e regenerá-lo,
    • linha 10: a data de hoje no formato [aaaa-mm-jj],
    • linha 11: o idioma do calendário;
  • linha 14: criação do calendário;
  • linha 16: a lista suspensa dos médicos;
  • linhas 17-19: sempre que o valor selecionado nesse menu suspenso mudar, o método [afficherAgenda] será executado;
  • linha 21: sem menu na barra de navegação;

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


function updateCalendar(renew) {
    if (renew) {
        // atualização do calendário atual
        calendar_infos.container.html(calendar_infos.html);
    }
    // inicialização do calendário
    var calendar = $("#calendar");
    var settings = {
        format : "yyyy-mm-dd",
        startDate : calendar_infos.today,
        language : calendar_infos.langue,
    };
    calendar.datepicker(settings);
    // seleção da data atual
    if (calendar_infos.date) {
        calendar.datepicker('setDate', calendar_infos.date)
    }
    // eventos
    calendar.datepicker().on('hide', function(e) {
        // exibição do dia selecionado
        displayJour();
    });
    calendar.datepicker().on('changeDate', function(e) {
        // anota-se a nova data
        calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
        // exibição de informações da agenda
        afficherAgenda();
        // exibição do dia selecionado
        displayJour();
    });
    // exibição do dia selecionado
    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) a partir das informações contidas em [calendar_infos];
  • linha 7: faz-se referência ao calendário;
  • linhas 8-12: seus parâmetros de inicialização;
    • linha 9: o formato das datas gerenciadas por [aaaa-mm-jj],
    • linha 10: a primeira data que pode ser selecionada no calendário. Neste caso, a data de hoje. As datas anteriores não poderão ser selecionadas,
    • linha 11: o idioma do calendário. Haverá dois: ['en'] e ['fr'];
  • linha 13: o calendário está configurado;
  • linhas 15-17: se a data de [calendar_infos] tiver sido inicializada, essa data será definida como a data atual do calendário;
  • linhas 19-22: sempre que o calendário for fechado, será exibida a data selecionada;
  • linhas 23-30: sempre que houver uma mudança de data no calendário:
    • linha 25: registra-se a data selecionada em [calendar_infos],
    • linha 27: exibimos informações sobre a agenda,
    • linha 29: exibe-se o dia selecionado;
  • linha 32: exibição do dia selecionado, se houver;

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


// exibe o dia selecionado
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 uma data já tiver sido selecionada (inicialmente, o calendário não tem nenhuma data selecionada);
  • linha 4: localiza-se o componente onde será inserida a data;
  • linha 5: essa data pode ser escrita em inglês ou francês. Define-se o idioma da biblioteca [moment];
  • linha 6: exibe-se a data selecionada no idioma escolhido e no formato longo;
  • linha 7: essa data é exibida;

Aqui estão dois exemplos:

Ao alterar o médico ou a data, o método [afficherAgenda] é executado:


function afficherAgenda() {
    // exibe o médico e a data
    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 nº 7: uma tabela HTML “responsive”

Nota: “responsive” é um termo em inglês que indica que um componente é capaz de se adaptar ao tamanho da tela na qual é exibido. Vamos mostrar um exemplo.

A ação [/bs-07] exibe a seguinte visualização [bs-07.xml] (tela cheia):

A novidade é a tabela HTML [1]. Essa tabela é gerenciada pela biblioteca JS [footable]: [https://github.com/fooplugins/FooTable].

Se reduzirmos o tamanho da janela do navegador, obtemos o seguinte:

  • a tabela HTML se adaptou ao tamanho da tela;
  • em [1], para ver o link [Réserver], é preciso clicar no símbolo [+];
  • em [2], o que aparece quando se clica no ícone [+];

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>
        <!-- Núcleo do Bootstrap 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" />
        <!-- Núcleo do Bootstrap 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>
        <!-- script local -->
        <script type="text/javascript" src="resources/js/bs-07.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- barra de navegação -->
            <div th:include="navbar3" />
            <!-- Jumbotron do Bootstrap -->
            <div th:include="jumbotron" />
            <!-- conteúdo -->
            <div id="content" th:include="choixmedecinjour" />
            <div id="agenda" th:include="agenda" />
            <!-- informações -->
            <div class="alert alert-success">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 10: o CSS da biblioteca [footable];
  • linha 19: o JS da biblioteca [footable];
  • linha 31: a tabela HTML de uma agenda;

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>
        <!-- página inicial -->
        <script th:inline="javascript">
            /*<![CDATA[*/
             // inicializando a página
            initAgenda();
        /*]]>*/
        </script>
    </body>
</html>
  • linha 4: insere a tabela em uma linha [row] e uma moldura 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] indica a coluna que contém o símbolo [+/-] que expande/retrai a linha;
  • linha 15: o atributo [data-hide='phone'] indica que a coluna deve ser ocultada se a tela tiver o tamanho de uma tela de celular. Também é possível usar o valor 'tablet';
  • linha 31: associa-se uma função JS ao link [Réserver];
  • linha 46: associa-se uma função JS ao link [Supprimer];
  • linhas 56-61: inicialização da página;

Algumas das classes CSS utilizadas acima provêm do arquivo CSS [bootstrapDemo.css]:


@CHARSET "UTF-8";

#intervalos 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 uso da tabela [footable] encontrado no site da biblioteca.

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


function initAgenda() {
    // a tabela de horários
    $("#creneaux").footable();
}

É isso. O arquivo [$("#creneaux")] faz referência à tabela HTML, que queremos tornar “responsiva”. Além disso, encontramos as funções JS relacionadas aos dois links [Réserver] e [Supprimer]:


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 visualização [bs-08.xml]:

 

Image

Enquanto anteriormente, clicar no link [Réserver] exibia uma informação na caixa de informações, aqui vamos exibir uma caixa modal para selecionar um cliente para o RV:

Image

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

A visualização [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>
        <!-- Núcleo do Bootstrap 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" />
        <!-- Núcleo do Bootstrap 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>
        <!-- script local -->
        <script type="text/javascript" src="resources/js/bs-08.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- barra de navegação -->
            <div th:include="navbar3" />
            <!-- Jumbotron do Bootstrap -->
            <div th:include="jumbotron" />
            <!-- conteúdo -->
            <div id="content" th:include="choixmedecinjour" />
            <div id="agenda" th:include="agenda-modal" />
            <div th:include="resa" />
            <!-- informações -->
            <div class="alert alert-success">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • linha 19: o arquivo JS necessário para as caixas modais;
  • linha 32: a visualização [agenda-modal] é idêntica à visualização [agenda], com uma única diferença: a função JS, que gerencia o link [Réserver]:

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

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

  • linha 33: a visualização [resa.xml] é a caixa modal de seleção de 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">Título do modal</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 -->
    <!-- página inicial -->
    <script th:inline="javascript">
        /*<![CDATA[*/
             // inicializando a página
            initResa();
        /*]]>*/
    </script>
</section>
  • linhas 3-37: a caixa modal;
  • linhas 13-30: o conteúdo dessa caixa (o que será exibido);
  • linhas 31-34: os botões da caixa de diálogo;
  • linha 32: um botão [Annuler] gerenciado pela função JS [cancelDialogResa];
  • linha 33: um botão [Valider] gerenciado pelas funções JS e [validateResa];
  • linhas 39-44: o script de inicialização da caixa modal;

Isso resulta na seguinte visualização:

 

Observe que a caixa modal não é exibida por padrão. É por isso que ela não é visível ao iniciar o aplicativo, embora seu código HTML esteja presente no documento.

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


var idCreneau;
var idClient;
var resa;

function showDialogResa(idCreneau) {
    // armazenando o ID do horário
    this.idCreneau = idCreneau;
    // exibe a janela de reserva
    var resa = $("#resa");
    resa.modal('show');
    // log
    showInfo("Réservation du créneau n° " + idCreneau);
}

function cancelDialogResa() {
    // oculta-se a caixa de diálogo
    resa.modal('hide');
}

// validação da reserva
function validateResa() {
    // recuperamos as informações
    var idClient = $('#idClient option:selected').val();
    // ocultar a caixa de diálogo
    resa.modal('hide');
    // informações
    showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}

function initResa() {
    // a lista de seleção de clientes
    $('#idClient').selectpicker();
    // caixa modal
    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 ser inicializada;
  • linhas 34-35: inicialização da própria caixa modal;
  • linhas 5-13: a função JS associada ao link [Réserver];
  • linha 7: o parâmetro da função é armazenado na variável global da linha 1;
  • linhas 9-10: a caixa modal é exibida;
  • linha 12: registra-se uma informação na caixa de informações;
  • linhas 15-18: gerenciamento do botão [Annuler]. Limita-se a ocultar a caixa modal (linha 17);
  • linhas 21-31: a função JS associada ao botão [Valider];
  • linha 23: recuperamos o atributo [value] do cliente selecionado;
  • linha 25: oculta-se a caixa de diálogo;
  • linha 27: registram-se as duas informações: o número do horário reservado e para qual cliente;

8.6.5. Etapa 2: criação das visualizações

Descreveremos agora as visualizações fornecidas pelo servidor [Web1], bem como seus modelos.

  

8.6.5.1. A visualização [navbar-start]

Ela exibe a barra de navegação da página inicial:

Image

O código de [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" />
                <!-- formulário de identificação -->
                <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>
                    <!-- idiomas -->
                    <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>
    <!-- página inicial -->
    <script th:inline="javascript">
        /*<![CDATA[*/
             // inicializando a página
            initNavBarStart();
        /*]]>*/
    </script>
</section>

Esta visualização não possui modelo. Ela possui os seguintes manipuladores de eventos:

event
manipulador
clique no botão de conexão
connecter() - ligne 27
clique no link [Français]
setLang('fr') - ligne 37
clique no link [English]
setLang('en') - ligne 40

8.6.5.2. A visualização [jumbotron]

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

Image

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


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <!-- Jumbotron do Bootstrap -->
    <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 visualização [jumbotron] não possui modelo nem eventos.

8.6.5.3. A visualização [login]

Esta é a visualização exibida abaixo do jumbotron na página inicial:

Image

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 visualização não possui modelo nem eventos.

8.6.5.4. A visualização [navbar-run]

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

Image

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" />
                <!-- botões à direita -->
                <form class="navbar-form navbar-right" role="form">
                    <!-- sair -->
                    <button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
                    <!-- idiomas -->
                    <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>
    <!-- inicialização da página -->
    <script th:inline="javascript">
        /*<![CDATA[*/
             // inicializando a página
            initNavBarRun();
        /*]]>*/
    </script>
</section>

Essa visualização não possui modelo. Ela possui os seguintes manipuladores de eventos:

evt
manipulador
clique no botão de logout
deconnecter() - ligne 19
clique no link [Français]
setLang('fr') - ligne 29
clique no link [English]
setLang('en') - ligne 32

8.6.5.5. A visualização [accueil]

Esta é a visualização exibida logo abaixo da barra de navegação [navbar-run]:

Image

Seu código [accueil.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>
    <!-- script local -->
    <script th:inline="javascript">
        /*<![CDATA[*/
             // inicializando a página
            initChoixMedecinJour();
        /*]]>*/
    </script>
</html>

Seu modelo é o seguinte:

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

Em sua forma atual, a visualização não parece possuir um gerenciador de eventos. Na verdade, estes são definidos na função [initChoixMedecinJour]. Essa função foi apresentada no parágrafo 8.6.4.7, página 466 e, mais especificamente, na página 469. Nela encontram-se os seguintes gerenciadores de eventos:

event
gerenciador
escolha de um médico
getAgenda
escolha de uma data
getAgenda

8.6.5.6. A visualização [agenda]

A visualização [agenda] apresenta um dia da agenda de um médico:

Image

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>
            <!-- reserva -->
            <section th:include="resa" />
        </th:block>
        <!-- inicialização da página -->
        <script th:inline="javascript">
            /*<![CDATA[*/
             // inicializando a página
            initAgenda();
        /*]]>*/
        </script>
    </body>
</html>

O modelo dessa visualização possui apenas um elemento:

  • [agenda] (linha 4): um modelo um pouco complexo, criado especialmente para a exibição da agenda;

Ela possui os seguintes gerenciadores de eventos:

evt
gerenciador
clique no botão [Supprimer]
supprimerRv(idRv) - ligne 37
clique no link [Réserver]
reserverCreneau(idCreneau) - ligne 34

A visualização [resa] da linha 47 é a visualização exibida quando o usuário clica em um link [Réserver]:

Image

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">Título do modal</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 -->
        <!-- inicializar página -->
        <script th:inline="javascript">
            /*<![CDATA[*/
             // inicializando a página
            initResa();
        /*]]>*/
        </script>
    </body>
</html>

Seu modelo tem apenas um elemento:

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

Ele possui os seguintes gerenciadores de eventos:

event
gerenciador
clique no botão [Annuler]
cancelDialogResa() - ligne 30
clique no botão [Valider]
validerRv() - ligne 31

8.6.5.7. A tela [erreurs]

Esta é a tela exibida caso a ação solicitada pelo usuário não tenha sido concluída:

Image

O código [erreurs.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>

Seu modelo possui apenas um elemento:

  • [erreurs] (linha 8): a lista de erros a serem exibidos;

A visualização não possui gerenciador de eventos.

8.6.5.8. Résumé

A tabela a seguir apresenta as visualizações e seus modelos:

visualização
modelo
gerenciadores de eventos
navbar-start

connecter, setLang
jumbotron


login


navbar-run

deconnecter, setLang
página inicial
rdvmedecins.medecinItems (liste des médecins)
getAgenda
agenda
agenda (une journée de l'agenda)
supprimerRv, reserverCreneau
resa
clientItems (liste des clients)
cancelDialogResa, validerRv
erros
erreurs (liste d'erreurs)

8.6.6. Etapa 3: definição das ações

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

Veremos agora quais URL são expostas pelo [Web1] e sua implementação:

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

São as seguintes:

  • uma URL para cada uma das visualizações anteriores ou uma composição delas;
  • uma URL para adicionar uma RV;
  • um URL para excluir um RV;

Todas elas retornam uma resposta do tipo [Reponse], conforme segue:


public class Reponse {

    // ----------------- propriedades
    // status da operação
    private int status;
    // a barra de navegação
    private String navbar;
    // o jumbotron
    private String jumbotron;
    // o corpo da página
    private String content;
    // a agenda
    private String agenda;
...
}
  • linha 5: um status da resposta: 1 (OK), 2 (erro);
  • linha 7: o fluxo HTML das visualizações [navbar-start] ou [navbar-run], conforme o caso;
  • linha 9: o fluxo HTML da visualização [jumbotron];
  • linha 13: o fluxo HTML da visualização [agenda];
  • linha 9: o fluxo HTML das visualizações [accueil], [erreurs], [login], conforme o caso;

As visões URL apresentadas são as seguintes

/getNavbarStart
insere a visualização [navbar-start] na [Reponse.navbar]
/getNavbarRun
insere a visualização [navbar-run] na visualização [Reponse.navbar]
/getAccueil
coloca a visualização [accueil] na [Reponse.content]
/getJumbotron
coloca a visualização [jumbotron] em [Reponse.jumbotron]
/getAgenda
coloca a visualização [agenda] em [Reponse.agenda]
/getLogin
coloca a visualização [login] em [Reponse.content]
/getNavbarRunJumbotronAccueil
  • se a conexão for bem-sucedida, insere a visualização [navbar-run] na [Reponse.navbar], a vista [jumbotron] na vista [Reponse.jumbotron], a vista [accueil] na vista [Reponse.content]
  • se a conexão falhar, coloca a visualização [erreurs] em [Reponse.content] e [Reponse.status] em 2
/getNavbarRunJumbotronAccueilAgenda
coloca a visualização [navbar-run] em [Reponse.navbar], a visualização [jumbotron] em [Reponse.jumbotron], a visualização [accueil] na [Reponse.content], a visualização [agenda] na [Reponse.agenda]
/ajouterRv
adiciona o compromisso selecionado e insere a nova agenda em [Reponse.agenda]
/supprimerRv
exclui o compromisso selecionado e insere a nova agenda em [Reponse.agenda]

8.6.6.2. O singleton [ApplicationModel]

 

A classe [ApplicationModel] é instanciada em uma única instância e injetada no controlador do aplicativo. 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 isso para que as ações não precisem conhecer a camada [DAO], mas apenas o singleton [ApplicationModel]. A arquitetura de [Web1] passa 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 {

    // a camada [DAO]
    @Autowired
    private IDao dao;
    // a configuração
    @Autowired
    private AppConfig appConfig;

    // dados provenientes da camada [DAO]
    private List<ClientItem> clientItems;
    private List<MedecinItem> medecinItems;
    // dados de configuração
    private String userInit;
    private String mdpUserInit;
    private boolean corsAllowed;
    // exceção
    private RdvMedecinsException rdvMedecinsException;

    // fabricante
    public ApplicationModel() {
    }

    @PostConstruct
    public void init() {
        // configuração
        userInit = appConfig.getUSER_INIT();
        mdpUserInit = appConfig.getMDP_USER_INIT();
        dao.setTimeout(appConfig.getTIMEOUT());
        dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
        corsAllowed = appConfig.isCORS_ALLOWED();
        // armazenamos em cache as listas suspensas de médicos e clientes
        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) {
            // criam-se os itens das listas suspensas
            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 e setters
    ...

    // implementação da interface [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 da implementação da camada [DAO]. É essa referência que é usada para implementar a interface [IDao] (linhas 64-80);
  • linha 14: injeção da configuração do aplicativo;
  • linhas 33-37: uso dessa configuração para definir diversos elementos da arquitetura da aplicação;
  • linhas 38-46: armazenamos em cache as informações que alimentarão as listas suspensas de médicos e clientes. Partimos, portanto, do pressuposto de que, se um médico ou cliente for alterado, a aplicação deverá ser reiniciada. A ideia aqui é mostrar que um singleton do Spring pode servir 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 {

    // elemento de uma lista
    private Long id;
    private String texte;

    // construtor
    public PersonneItem() {

    }

    public PersonneItem(Personne personne) {
        id = personne.getId();
        texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
    }

    // getters e setters
...
}
  • linha 8: o campo [id] será o valor do atributo [value] de uma opção da lista suspensa;
  • linha 9: o campo [texte] 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 essa classe pai. Nela, reunimos métodos utilitários da classe [RdvMedecinsController] que não são essenciais, com exceção de um. Eles podem ser classificados em três grupos:

  1. os métodos utilitários;
  2. os métodos que renderizam as visualizações combinadas com seus modelos;
  3. o método de inicialização de uma ação

protected List<String>
getErreursForException(Exception exception)

protected List<String>
getErreursForModel(BindingResult result,
Locale locale,
WebApplicationContext ctx)
dois métodos utilitários que fornecem uma lista de mensagens de erro. Já os encontramos e utilizamos anteriormente;

protected String getPartialViewAccueil(WebContext
thymeleafContext)
exibe a visualização [accueil] sem modelo

protected String getPartialViewAgenda(ActionContext
actionContext,
AgendaMedecinJour agenda,
Locale locale)
gera a visualização [agenda] e seu modelo

protected String getPartialViewLogin(WebContext thymeleafContext)
exibe a visualização [login] sem modelo

protected Reponse getViewErreurs(WebContext thymeleafContext, List<String> erreurs)
retorna a resposta ao cliente quando a ação solicitada 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 de todas as ações do controlador [RdvMedecinsController]

Vamos examinar dois desses métodos.

O método [getPartialViewAgenda] é responsável pela geração da visualização mais complexa: a agenda. Seu código é o seguinte:


    // fluxo [agenda]
    protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
        // contextos
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        // construímos o modelo da página [agenda]
        ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
        // a agenda com seu modelo
        thymeleafContext.setVariable("agenda", modelAgenda);
        thymeleafContext.setVariable("clientItems", application.getClientItems());
        return engine.process("agenda", thymeleafContext);
}
  • linhas 9-10: os dois elementos do modelo da agenda:
    • linha 9: a agenda exibida.
    • linha 10: a lista de clientes exibida quando o usuário marca um compromisso;

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


// modelo da página [Agenda]
    private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
        // o título da página
        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);
        // horários disponíveis para agendamento
        ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
        int i = 0;
        for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
            // horário do médico
            Creneau créneau = creneauMedecinJour.getCreneau();
            ViewModelCreneau modelCréneau = new ViewModelCreneau();
            modelCréneaux[i] = modelCréneau;
            // ID
            modelCréneau.setId(créneau.getId());
            // intervalo horário
            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();
            // cliente e pedido
            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);
            }
            // próximo horário
            i++;
        }
        // exibição do modelo da agenda
        ViewModelAgenda modelAgenda = new ViewModelAgenda();
        modelAgenda.setTitre(titre);
        modelAgenda.setCreneaux(modelCréneaux);
        return modelAgenda;
    }
  • linha 6: a agenda tem um título:

Image

ou então:

Image

Percebe-se que o formato da data depende do idioma. Vamos buscar esse formato nos arquivos de mensagens (linha 4).

  • linhas 11-40: para cada intervalo, devemos exibir a visualização:

Image

ou a visualização:

Image

  • linhas 19-20: exibem o horário disponível;
  • linhas 25-28: o caso em que o horário está livre. Nesse caso, deve-se exibir o botão [Réserver];
  • linhas 31-36: o caso em que o horário está ocupado. Nesse caso, é preciso exibir tanto o cliente quanto o botão [Supprimer];

O outro método sobre o qual fornecemos mais explicações é o método [getActionContext]. Ele é chamado no início de cada uma das ações do [RdvMedecinsController]. Sua assinatura é a seguinte:


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

Ela retorna o tipo [ActionContext] a seguir:


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 eventual lista de mensagens de erro;

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 uma chamada entre domínios;
  • [request]: a solicitação HTTP em processamento, o que há algum tempo é chamado de ação;
  • [response]: a resposta que será enviada a essa solicitação;
  • [result]: cada ação de [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:


    // contexto de uma ação
    protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
        // idioma?
        if (lang == null) {
            lang = "fr";
        }
        // local
        Locale locale = null;
        if (lang.trim().toLowerCase().equals("fr")) {
            // francês
            locale = new Locale("fr", "FR");
        } else {
            // todo o restante em inglês
            locale = new Locale("en", "US");
        }
        // cabeçalhos CORS
        rdvMedecinsCorsController.sendOptions(origin, response);
        // ActionContext
        ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
        // erros de inicialização
        RdvMedecinsException e = application.getRdvMedecinsException();
        if (e != null) {
            actionContext.setErreurs(e.getMessages());
            return actionContext;
        }
        // erros de POST?
        if (result != null && result.hasErrors()) {
            actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
            return actionContext;
        }
        // sem erros
        return actionContext;
}
  • linhas 3-15: a partir do parâmetro [lang], define-se a localidade da ação;
  • linha 17: enviam-se os cabeçalhos HTTP necessários para as solicitações entre domínios. Não entramos em detalhes. A técnica utilizada é a descrita no parágrafo 8.4.14;
  • linha 19: construção de um objeto [ActionContext] sem erros;
  • linha 21: vimos no parágrafo 8.6.6.2 que o singleton [ApplicationModel] acessava o banco de dados para recuperar tanto os clientes quanto os médicos. Esse acesso pode falhar. Nesse caso, registramos a exceção que ocorre. Na linha 21, recuperamos essa exceção;
  • linhas 22-25: se ocorreu uma exceção durante a inicialização do aplicativo, nenhuma ação é possível. Nesse caso, retorna-se, para qualquer ação, um objeto [ActionContext] com as mensagens de erro da exceção;
  • linhas 27 a 20: analisamos o parâmetro [result] para verificar se o valor enviado era válido ou não. Se for inválido, retornamos um objeto [ActionContext] com as mensagens de erro apropriadas;
  • linha 32: caso sem erros;

Examinamos agora as ações do controlador [RdvMedecinsController]

8.6.6.4. A ação [/getNavBarStart]

A ação [/getNavBarStart] retorna a visualização [navbar-start]. 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)

Ela retorna o tipo [Reponse] a seguir:


public class Reponse {

    // ----------------- propriedades
    // status da operação
    private int status;
    // a barra de navegação
    private String navbar;
    // o jumbotron
    private String jumbotron;
    // o corpo da página
    private String content;
    // a agenda
    private String agenda;
...
}

e possui os seguintes parâmetros:

  • [PostLang postlang]: o valor postado a seguir:

public class PostLang {

    // data
    @NotNull
    private String lang;
...
}

A classe [PostLang] é a classe pai de todos os valores lançados. De fato, o cliente deve sempre especificar o idioma no qual 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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // erros?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // retornamos a visualização [navbar-start]
        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 tiver sinalizado erros, eles são enviados na resposta ao cliente (linha 12) com o status 2:
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • linhas 15-18: envia-se a visualização [navbar-start] com o status 1:
 {"status":1,"navbar": navbar-start, "jumbotron": null, "agenda":null, "content":null}

A seguir, detalhamos apenas as novidades.

8.6.6.5. A ação [/getNavbarRun]

A ação [/getNavBarRun] gera a visualização [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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // erros?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // retorna a visualização [navbar-run]
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        return reponse;
}

A ação pode retornar dois tipos de resposta:

  • a resposta com erro (linhas 10-13):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • a resposta com a visualização [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] gera a visualização [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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // erros?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // retorna a visualização [jumbotron]
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        return reponse;
}

A ação pode gerar dois tipos de resposta:

  • a resposta com erro (linhas 10-13):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • a 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] gera a visualização [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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // erros?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // retorna a visualização [login]
        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 gerar dois tipos de resposta:

  • a resposta com 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 [/getAccueil]

A ação [/getAccueil] retorna a visualização [accueil]. 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) 
  • na linha 3, o valor lançado é do tipo [PostUser], conforme segue:

public class PostUser extends PostLang {
    // dados
    @NotNull
    private User user;
...
}
  • linha 1: a classe [PostUser] estende a classe [PostLang] e, portanto, incorpora um idioma;
  • linha 4: o usuário que deseja acessar a visualização;

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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // erros?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // a visualização [accueil] está protegida
        try{
            // usuário
            User user = postUser.getUser();
            // verificando as credenciais [userName, password]
            application.authenticate(user);
        }catch(RdvMedecinsException e){
            // retorna um erro
            return getViewErreurs(thymeleafContext, e.getMessages());
        }
        // retorna a visualização [accueil]
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        return reponse;
}
  • linhas 15-22: observe-se que a página [accueil] está protegida e, portanto, o usuário deve estar autenticado;

A ação pode retornar dois tipos de resposta:

  • a resposta com erro (linhas 11 e 21):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • a resposta com a visualização [accueil] (linhas 24-27):
 {"status":1,"navbar": null, "jumbotron": null, "agenda":null, "content":accueil}

8.6.6.9. A ação [/getNavbarRunJumbotronAccueil]

A ação [/getNavbarRunJumbotronAccueil] gera as visualizações [navbar-run, jumbotron, accueil]. Ela possui 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 lançado é do tipo [PostUser];

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


// barra de navegação + jumbotron + página inicial
    @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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
                rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // erros?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // a visualização [accueil] está protegida
        try {
            // usuário
            User user = postUser.getUser();
            // verificando as credenciais [userName, password]
            application.authenticate(user);
        } catch (RdvMedecinsException e) {
            // é retornado um erro
            return getViewErreurs(thymeleafContext, e.getMessages());
        }
        // envia-se a resposta
        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 retornar dois tipos de resposta:

  • a resposta com erro (linhas 13, 23):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • a resposta com as visualizações [navbar-run, jumbotron, accueil] (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] retorna a visualização [agenda]. 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 lançado é do tipo [PostGetAgenda], conforme segue:

public class PostGetAgenda extends PostUser {

    // dados
    @NotNull
    private Long idMedecin;
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date jour;
...
}
  • linha 1: a classe [PostGetAgenda] estende a classe [PostUser] e, portanto, inclui um idioma e um usuário;
  • linha 5: o número do médico cuja agenda se deseja consultar;
  • linha 8: o dia da agenda desejada;

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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result,    rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        Locale locale = actionContext.getLocale();
        // erros?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // verifica-se a validade do post
        if (result != null) {
            new PostGetAgendaValidator().validate(postGetAgenda, result);
            if (result.hasErrors()) {
                // retorna-se a visualização [erreurs]
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        ...
}
  • até a linha 14, temos um código que já é clássico;
  • linhas 16-21: é feita uma verificação adicional do valor enviado. A data deve ser posterior ou igual à de hoje. Para verificar isso, utiliza-se 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) {
        // o dia escolhido para o compromisso
        Date jour = null;
        if (post instanceof PostGetAgenda) {
            jour = ((PostGetAgenda) post).getJour();
        } else {
            if (post instanceof PostValiderRv) {
                jour = ((PostValiderRv) post).getJour();
            }
        }
        // converte-se as datas para o formato aaaa-MM-dd
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String strJour = sdf.format(jour);
        String strToday = sdf.format(new Date());
        // o dia escolhido não deve ser anterior à data de hoje
        if (strJour.compareTo(strToday) < 0) {
            errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
        }
    }

}
  • linha 19: o validador atua em 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) {
        ...
                // ação
        try {
            // agenda do médico
            AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
                    new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
            // resposta
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException e1) {
            // retorna a visualização [erreurs]
            return getViewErreurs(thymeleafContext, e1.getMessages());
        } catch (Exception e2) {
            // retorna a visualização [erreurs]
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
}
  • linhas 9-10: com os parâmetros enviados, solicitamos a agenda do médico;
  • linhas 12-13: retorna-se a agenda:
 {"status":1,"navbar": null, "jumbotron": null, "agenda":agenda, "content":null}
  • linhas 17, 21: retorna-se uma resposta com erros:
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}

8.6.6.11. A ação [/getNavbarRunJumbotronAccueilAgenda]

A ação [/getNavbarRunJumbotronAccueilAgenda] retorna as visualizações [navbar-run, jumbotron, accueil, agenda]. 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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // erros?
        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;
        }
        // envia-se a resposta
        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: aproveita-se a existência da ação [/getAgenda] para chamá-la. Em seguida, verifica-se a ação status da resposta (linha 16). Se for detectado um erro, não se prossegue e retorna-se 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 [/supprimerRv] permite excluir um compromisso. 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 segue:

public class PostSupprimerRv extends PostUser {

    // dados
    @NotNull
    private Long idRv;
..
}
  • linha 1: a classe [PostSupprimerRv] estende a classe [PostUser] e, portanto, inclui um idioma e um usuário;
  • linha 5: o número do compromisso a ser excluído;

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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
                rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // erros?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // valores enviados
        User user = postSupprimerRv.getUser();
        long idRv = postSupprimerRv.getIdRv();
        // o compromisso é excluído
        AgendaMedecinJour agenda = null;
        try {
            // recuperando o compromisso
            Rv rv = application.getRvById(user, idRv);
            Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
            long idMedecin = creneau.getIdMedecin();
            Date jour = rv.getJour();
            // excluindo o compromisso associado
            application.supprimerRv(user, idRv);
            // regenerando a agenda do médico
            agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // retorna a nova agenda
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // retorna a visualização [erreurs]
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // retorna-se a visualização [erreurs]
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
}
  • linha 22: recupera-se a consulta a ser excluída. Se ela não existir, ocorre uma exceção;
  • linhas 23-25: a partir desse compromisso, identificam-se o médico e o dia em questão. Essas informações são necessárias para regenerar a agenda do médico;
  • linha 27: a consulta é excluída;
  • linha 29: solicita-se a nova agenda do médico. Isso é importante. Além do horário que acabou de ser liberado, outros usuários do aplicativo podem ter feito alterações na agenda. É importante enviar ao usuário a versão mais recente da mesma;
  • linhas 31-34: a agenda é devolvida:
 {"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 à agenda de um médico. 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 inserido é do tipo [PostValiderRv], conforme segue:

public class PostValiderRv extends PostUser {

    // dados
    @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, portanto, inclui um idioma e um usuário;
  • linha 5: o número do horário;
  • linha 7: o número do cliente para o qual a reserva foi feita;
  • linha 10: o dia do compromisso;

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


// confirmação de um compromisso
    @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) {
        // contextos da ação
        ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebApplicationContext springContext = actionContext.getSpringContext();
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // erros?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // verifica-se a validade da data da consulta
        if (result != null) {
            new PostGetAgendaValidator().validate(postValiderRv, result);
            if (result.hasErrors()) {
                // retorna a visualização [erreurs]
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        // valores lançados
        User user = postValiderRv.getUser();
        long idClient = postValiderRv.getIdClient();
        long idCreneau = postValiderRv.getIdCreneau();
        Date jour = postValiderRv.getJour();
        // ação
        try {
            // recuperam-se informações sobre o horário
            Creneau créneau = application.getCreneauById(user, idCreneau);
            long idMedecin = créneau.getIdMedecin();
            // adiciona-se o compromisso
            application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
            // regenera-se a agenda
            AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
                    new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // retorna a nova agenda
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // retorna a visualização [erreurs]
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // retorna a visualização [erreurs]
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
    }
}

O código é semelhante ao da ação [/supprimerRv].

8.6.7. Etapa 4: testes do servidor Spring/Thymeleaf

Agora, vamos testar as diferentes ações anteriores com o plugin do Chrome [Advanced Rest Client] (ver parágrafo 9.6).

8.6.7.1. Configuração dos testes

Todas as ações esperam um valor enviado por POST. Enviaremos variantes da seguinte string jSON:

{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Esse valor lançado inclui informações supérfluas para a maioria das ações. No entanto, essas informações são ignoradas pelas ações que as recebem e não causam erros. Esse valor lançado tem a vantagem de abranger os diferentes valores a serem lançados.

8.6.7.2. A ação [/getNavbarStart]

  • em [1], a ação testada;
  • em [2], o valor postado;
  • em [3], o valor lançado é uma cadeia de caracteres jSON;
  • em [4], a visualização [navbar-start] é solicitada em inglês;

O resultado obtido é o seguinte:

 

Recebemos a visualização [navbar-start] em inglês (campos destacados).

Agora, vamos cometer um erro. Alteramos o atributo [lang] do valor enviado para null. 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 o seguinte valor lançado:


{"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 o seguinte valor lançado:


{"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.6. A ação [/getAccueil]

Solicitamos a ação [getAccueil] com o seguinte valor lançado:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado obtido é o seguinte:

 

Repetimos o procedimento com um usuário desconhecido:


{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado obtido é o seguinte:

 

Vamos tentar novamente com um usuário existente, mas sem permissão para usar o aplicativo:


{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado obtido é 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:

 

Repetimos a operação com uma data anterior à de hoje:

 

Vamos tentar 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 obtido é 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 obtido é o seguinte:

 

O mesmo ocorre com um usuário desconhecido:

 

8.6.7.9. A ação [/getNavbarRunJumbotronAccueilAgenda]

Solicitamos a ação [getNavbarRunJumbotronAccueilAgenda] 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:

 

Inserimos um médico que não existe:

 

8.6.7.10. A ação [/supprimerRv]

Solicitamos a ação [supprimerRv] com o seguinte valor lançado:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O Rv nº 93 não existe. O resultado obtido é o seguinte:

 

Com um compromisso que existe:

 

É possível verificar no banco de dados que o compromisso foi efetivamente excluído. A nova agenda é reenviada.

8.6.7.11. A ação [/validerRv]

Solicitamos a ação [validerRv] com o seguinte valor lançado:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

O resultado obtido é o seguinte:

 

É possível verificar no banco de dados que o compromisso foi efetivamente criado. A nova agenda foi reenviada.

Fazemos o mesmo com um número de horário inexistente:

 

Fazemos o mesmo com um número de cliente inexistente:

 

8.6.8. Etapa 5: Codificação do cliente em JavaScript

Voltemos à arquitetura do servidor [Web1]:

O cliente [2] do servidor [Web1] é um cliente JavaScript do tipo APU (Aplicativo de Página Única):

  • o cliente solicita a página inicial a um servidor web (não necessariamente o [Web1]);
  • ele solicita as páginas seguintes ao servidor [Web1] por meio de chamadas Ajax;

Para construir esse cliente, vamos usar a ferramenta [Webstorm] (ver parágrafo 9.8). Essa ferramenta me pareceu mais prática do que a STS. Sua principal vantagem é que ela oferece autocompletar durante a digitação do código, além de algumas opções do refactoring. Isso evita muitos erros.

8.6.8.1. O projeto JS

O projeto JS possui a seguinte estrutura de diretórios:

  • em [1], o cliente JS como um todo. [boot.html] é a página inicial. Essa será a única página carregada pelo navegador;
  • em [2], as folhas de estilo dos componentes do Bootstrap;
  • em [3], as poucas imagens utilizadas pelo aplicativo;
  • em [4], os scripts JS. É aqui que se concentra nosso trabalho;
  • em [5], as bibliotecas JS utilizadas: principalmente jQuery e as dos componentes do Bootstrap;

8.6.8.2. A arquitetura do código

O código foi dividido em três camadas:

  • a camada [présentation] reúne as funções de inicialização da página [boot.xml], bem como as dos diversos componentes do Bootstrap. Ela é implementada pelo arquivo [ui.js];
  • a camada [événements] reúne todos os manipuladores de eventos da camada [présentation]. Ela é implementada pelo arquivo [evts.js];
  • a camada [DAO] envia as solicitações HTTP ao servidor [Web1]. Ela é implementada pelo arquivo [dao.js];

8.6.8.3. A camada [présentation]

  

A camada [présentation] é implementada pelo seguinte arquivo [ui.js]:


//a camada [présentation]
var ui = {
// variáveis globais;
  "agenda": "",
  "resa": "",
  "langue": "",
  "urlService": "http://localhost:8081",
  "page": "login",
  "jourAgenda": "",
  "idMedecin": "",
  "user": {},
  "login": {},
  "exceptionTitle": {},
  "calendar_infos": {},
  "erreur": "",
  "idCreneau": "",
  "done": "",
// componentes da visualização
  "body": "",
  "navbar": "",
  "jumbotron": "",
  "content": "",
  "exception": "",
  "exception_text": "",
  "exception_title": "",
  "loading": ""
};
// a camada de eventos
var evts = {};
// a camada [dao]
var dao = {};

// ------------ documento pronto
$(document).ready(function () {
  // inicialização do documento
  console.log("document.ready");
  // componentes da página
  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");
  // a página de login é armazenada para poder ser recriada
  ui.login.lang = ui.langue;
  ui.login.navbar = ui.navbar.html();
  ui.login.jumbotron = ui.jumbotron.html();
  ui.login.content = ui.content.html();
  // URL do serviço
  $("#urlService").val(ui.urlService);
});

// ------------------------ funções de inicialização dos componentes do Bootstrap
ui.initNavBarStart = function () {
...
};

ui.initNavBarRun = function () {
...
};

ui.initChoixMedecinJour = function () {
...
};

ui.updateCalendar = function (renew) {
...
};

// exibe o dia selecionado
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 [présentation] (linhas 2-27),
    • [evts] para a camada de gerenciamento de eventos (linha 29),
    • [dao] para a camada [DAO] (linha 31);

Essa separação das camadas em três objetos permite evitar uma série de conflitos de nomes de variáveis e funções. Cada camada utiliza variáveis e funções prefixadas pelo objeto que encapsula a camada.

  • linhas 38-44: armazenam-se os campos que estarão sempre presentes, independentemente das visualizações exibidas. Isso evita pesquisas repetidas e desnecessárias em jQuery;
  • linhas 46-49: memoriza-se localmente a página inicial para poder recriá-la quando o usuário se desconectar e não tiver alterado o idioma;
  • linhas 54-83: funções de inicialização dos componentes do Bootstrap. Todas elas foram apresentadas na análise desses componentes no parágrafo 8.6.4;

8.6.8.4. As funções utilitárias da camada [événements]

  

Os gerenciadores de eventos foram colocados no arquivo [evts.js]. Várias funções são utilizadas regularmente pelos gerenciadores de eventos. Apresentamos-as a seguir:


// início da espera
evts.beginWaiting = function () {
  // início da espera
  ui.loading = $("#loading");
  ui.loading.show();
  ui.exception.hide();
  ui.erreur.hide();
  evts.travailEnCours = true;
};

// fim da espera
evts.stopWaiting = function () {
  // fim da espera
  evts.travailEnCours = false;
  ui.loading = $("#loading");
  ui.loading.hide();
};

// exibição do resultado
evts.showResult = function (result) {
  // exibição dos dados recebidos
  var data = result.data;
  // análise do status
  switch (result.status) {
    case 1:
      // erro?
      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:
      // exibição de erro
      evts.showException(data);
      break;
  }
};

// ------------ funções diversas
evts.showException = function (data) {
  // exibição de erro
  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 assíncrona [DAO];
  • linhas 4-5: exibe-se a imagem animada de espera;
  • linhas 6-7: oculta-se a área de exibição de erros e exceções (não são a mesma coisa);
  • linha 8: observa-se que uma tarefa assíncrona está em andamento;
  • linha 12: a função [evts.stopwaiting] é chamada após uma ação assíncrona [DAO] ter retornado seu resultado;
  • linha 14: observa-se que o trabalho assíncrono foi concluído;
  • 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]. Isso ocorre quando o servidor [Web1] envia uma resposta com um cabeçalho de erro HTTP (por exemplo, 403 forbidden). Nesse caso, [data] é a sequência jSON enviada pelo servidor para sinalizar 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':erros};
  • linhas 28-29: a visualização [erreurs] é exibida;
  • linhas 31-33: possível exibição da barra de navegação;
  • linhas 34-36: exibição eventual do jumbotron;
  • linhas 37-39: exibição eventual do campo [data.content]. Representa, conforme o caso, uma das visualizações [accueil, agenda];
  • linhas 40-43: se a agenda tiver sido regenerada, recuperam-se certas referências de seus componentes para não precisar procurá-las toda vez que forem necessárias;
  • linha 54: a função [evts.showException] tem como função exibir o texto da exceção contida em 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 arquivo [evts.js] contém mais de 300 linhas de código, e não vou comentar todas elas. Vou apenas citar alguns exemplos para ilustrar a essência dessa camada.

8.6.8.5. Login de um usuário

Image

O login de um usuário é realizado pela seguinte função:


// ------------------------ conexão
evts.connecter = function () {
  // recuperando os valores a serem postados
  var login = $("#login").val().trim();
  var passwd = $("#passwd").val().trim();
  // definindo o URL do servidor
  ui.urlService = $("#urlService").val().trim();
  dao.setUrlService(ui.urlService);
  // parâmetros da solicitação
  var post = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "lang": ui.langue
  };
  var sendMeBack = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "caller": evts.connecterDone
  };
  // enviando a solicitação
  evts.execute([{
    "name": "accueil-sans-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • linhas 4-5: recuperam-se o login e a senha do usuário;
  • linhas 7-8: recupera-se o URL do serviço [Web1]. Ele é armazenado tanto na camada [ui] quanto na camada [dao];
  • linhas 10-16: o valor a ser enviado: o idioma atual e o usuário que está tentando se conectar;
  • linhas 17-23: o objeto [sendMeBack] é um objeto que é passado para a função [DAO], que será chamada e que deve retornar o valor para a função da linha 22. Aqui, o objeto [sendMeBack] encapsula o usuário que está tentando se conectar;
  • linhas 25-29: a função [evts.execute] é capaz de executar uma sequência de ações assíncronas. Aqui, é passada uma lista composta por uma única ação. Os campos dessa ação são os seguintes:
    • [name]: o nome da ação assíncrona a ser executada,
    • [post]: o valor a ser enviado ao servidor [Web1],
    • [sendMeBack]: o valor que a ação assíncrona deve retornar com seu resultado;

Antes de detalhar a função [evts.execute], vamos examinar a função [evts.connecterDone] na linha 22. Essa é a função à qual a função assíncrona [DAO], que foi chamada, deve retornar seu resultado:


evts.connecterDone = function (result) {
  // exibição do resultado
  evts.showResult(result);
  // conexão bem-sucedida?
  if (result.status == 1 && result.data.status == 1) {
    // página
    ui.page = "accueil-sans-agenda";
    // registro do usuário
    ui.user = result.sendMeBack.user;
  }
};
  • linha 3: o resultado retornado pelo servidor [Web1] é exibido;
  • linha 5: se esse resultado não contiver erros, então se armazena o tipo da nova página (linha 7) e também o usuário autenticado (linha 9);

A função [evts.execute] executa uma sequência de ações assíncronas:


// execução de uma sequência de ações
evts.execute = function (actions) {
  // trabalho em andamento?
  if (evts.travailEnCours) {
    // não está sendo feito nada
    return;
  }
  // em espera
  evts.beginWaiting();
  // execução das ações
  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ó é aceita se não houver outra já em andamento;
  • linha 9: inicia-se a espera;
  • linha 11: solicita-se à camada [DAO] que execute a sequência de ações. O segundo parâmetro é o nome da função a ser executada quando todas as ações da sequência tiverem retornado seus resultados;

Não vamos detalhar agora a função [dao.doActions]. Vamos examinar outro evento.

8.6.8.6. Mudança de idioma

Image

A mudança de idioma é realizada pela seguinte função:


// ------------------------ mudança de idioma
evts.setLang = function (lang) {
  // mudança de idioma?
  if (lang == ui.langue) {
    // não se faz nada
    return;
  }
  // novo idioma
  ui.langue = lang;
  // qual página deve ser traduzida?
  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 é realizada nenhuma ação;
  • linha 9: o novo idioma é armazenado;
  • linhas 12-20: no caso de uma mudança de idioma, é necessário regenerar a página atualmente exibida pelo navegador. Existem três páginas possíveis:
    • aquela chamada [login], em que a página exibida é a de autenticação,
    • aquela chamada [accueil-sans-agenda], que é a página exibida logo após uma autenticação bem-sucedida,
    • a chamada [accueil-avec-agenda], que é a página exibida assim que uma primeira agenda é exibida. Em seguida, ela permanece na tela até o usuário se desconectar;

Vamos abordar o caso da página [accueil-avec-agenda]. Existem três versões dessa 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]

É a seguinte função:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // parâmetros da solicitação
  var post = {
    "user": ui.user,
    "lang": ui.langue,
    "idMedecin": ui.idMedecin,
    "jour": ui.jourAgenda
  };
  var sendMeBack = {
    "caller": evts.getAccueilAvecAgendaDone
  };
  // solicitação
  evts.execute([{
    "name": "accueil-avec-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • linhas 4-9: o valor a ser enviado inclui o usuário conectado, o idioma desejado, o número do médico cuja agenda se deseja consultar e o dia da agenda desejada;
  • linhas 10-12: o objeto [sendMeBack] é o objeto que será devolvido à função da linha 11. Aqui, ele não contém nenhuma informação;
  • linhas 14-18: execução de uma sequência de uma ação assíncrona, denominada [accueil-avec-agenda] (linha 15);
  • linha 11: a função executada quando a ação assíncrona [accueil-avec-agenda] tiver retornado seu resultado;

A função [evts.getAccueilAvecAgendaDone] da linha 11 exibe o resultado da função assíncrona denominada [accueil-avec-agenda]:


evts.getAccueilAvecAgendaDone = function (result) {
  // exibição do resultado
  evts.showResult(result);
  // nova página?
  if (result.status == 1 && result.data.status == 1) {
    ui.page = "accueil-avec-agenda";
  }
};
  • linha 1: [result] é o resultado da função assíncrona denominada [accueil-avec-agenda];
  • linha 3: esse resultado é exibido;
  • linha 5: se for um resultado sem erros, registra-se a nova página (linha 6);

8.6.8.8. A função [ getAccueilAvecAgenda-parallel]

Trata-se da seguinte função:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // ações [navbar-run, jumbotron, accueil, agenda] em //
  // 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
  };
  // página inicial
  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,
    'dia': ui.jourAgenda,
    "caller": evts.getAgendaDone
  };
  // execução de ações em //
  evts.execute([navbarRun, jumbotron, accueil, agenda])
};
  • linha 51: desta vez, executam-se quatro ações assíncronas. Elas 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 ser executada quando a ação assíncrona [navbarRun] tiver retornado seu resultado;
  • linhas 15-23: definição da ação [jumbotron], que recupera a visualização [jumbotron];
  • linha 22: a função a ser executada quando a ação assíncrona [jumbotron] tiver retornado seu resultado;
  • linhas 25-34: definição da ação [accueil], que recupera a visualização [accueil];
  • linha 33: a função a ser executada quando a ação assíncrona [accueil] tiver retornado seu resultado;
  • linhas 36-49: definição da ação [agenda], que recupera a visualização [jumbotron];
  • linha 48: a função a ser executada quando a ação assíncrona [agenda] tiver retornado seu resultado;

8.6.8.9. A função [ getAccueilAvecAgenda-sequence]

Esta é a seguinte função:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // ações [navbar-run, jumbotron, accueil, agenda] na ordem
  // agenda
  var agenda = {
    "name" : "agenda"
  };
  agenda.post = {
    "user" : ui.user,
    "lang" : ui.langue,
    "idMedecin" : ui.idMedecin,
    "jour" : ui.jourAgenda
  };
  agenda.sendMeBack = {
    'idMedecin': ui.idMedecin,
    'dia' : ui.jourAgenda,
    "caller" : evts.getAgendaDone
  };
  // página inicial
  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
  };
  // barra de navegação-execução
  var navbarRun = {
    "name" : "navbar-run"
  };
  navbarRun.post = {
    "lang" : ui.langue
  };
  navbarRun.sendMeBack = {
    "caller" : evts.showResult,
    "next" : jumbotron
  };
  // execução de ações em sequência
  evts.execute([ navbarRun ])
};
  • linha 54: executa-se a ação [navbarRun]. Quando ela é concluída, passa-se para a seguinte: [jumbotron], linha 51. Essa ação é então executada por sua vez. Quando ela é concluída, passa-se para a seguinte: [accueil], linha 40. Esta é executada por sua vez. Quando ela é concluída, passa-se para a seguinte: [agenda], linha 29. Esta é executada por sua vez. Quando ela é concluída, o processo é interrompido, pois a ação [agenda] não possui uma ação seguinte.

8.6.8.10. A camada [DAO]

  

O arquivo [dao.js] reúne todas as funções da camada [DAO]. Vamos apresentá-las gradualmente:


// URL expostas pelo servidor
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
// URL do servidor
dao.setUrlService = function (urlService) {
  dao.urlService = urlService;
};
  • linhas 16-18: a função que permite definir o URL do serviço [Web1];
  • linhas 2-13: o dicionário que associa o nome de uma ação assíncrona ao URL do servidor [Web1] a ser consultado;

// ------------------ gerenciamento genérico de ações
// execução de uma sequência de ações assíncronas
dao.doActions = function (actions, done) {
  // processamento das ações
  dao.actionsCount = actions.length;
  dao.actionIndex = 0;
  for (var i = 0; i < dao.actionsCount; i++) {
    // solicitação assíncrona DAO
    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 quando todas as ações tiverem retornado seu resultado;
  • linhas 7-12: as ações assíncronas são executadas em paralelo. No entanto, caso uma delas tenha uma ação subsequente, esta será executada ao final da ação que a precede;
  • linha 9: o objeto [Deferred] no estado [pending];
  • linha 10: quando esse objeto passar para o estado [resolved], a função [dao.actionDone] será executada;
  • linha 11: a ação nº i da lista é executada de forma assíncrona. O parâmetro [done] da linha 3 é passado como parâmetro;

A função [dao.actionDone], executada ao final de cada ação assíncrona, é a seguinte:


// foi recebido um resultado
dao.actionDone = function (result) {
  // chamar?
  var sendMeBack = result.sendMeBack;
  if (sendMeBack && sendMeBack.caller) {
    sendMeBack.caller(result);
  }
  // próximo?
  if (sendMeBack && sendMeBack.next) {
    // solicitação assíncrona DAO
    var deferred = $.Deferred();
    deferred.done(dao.actionDone);
    dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
  }
  // concluído?
  dao.actionIndex++;
  if (dao.actionIndex == dao.actionsCount) {
    // concluído?
    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 da lista de ações a serem executadas;
  • linhas 4-7: se a ação assíncrona concluída tivesse especificado uma função à qual enviar o resultado, essa função é chamada;
  • linhas 9-14: se a ação assíncrona concluída tiver uma ação seguinte, essa ação é, por sua vez, executada;
  • 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 seguintes conta como uma ação;
  • linhas 19-21: se, inicialmente, uma função [done] tivesse sido especificada para ser executada quando todas as ações da sequência tivessem retornado seu resultado, então essa função é agora executada;

O método [dao.doAction] executa uma ação assíncrona:


// execução de uma ação
dao.doAction = function (deferred, action, done) {
  // função “done” a ser incorporada na ação
  if (action.sendMeBack) {
    action.sendMeBack.done = done;
  } else {
    action.sendMeBack = {
      "done": done
    };
  }
  // execução da ação
  dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
  • linhas 4-10: como acabamos de ver, a função que processará o resultado da ação assíncrona a ser executada deve ter acesso à função [done]. Para isso, colocamos essa última no objeto [sendMeBack], objeto que fará parte do resultado da operação assíncrona;
  • linha 12: executa-se a função [dao.executePost], que faz uma chamada à função HTTP no servidor [Web1]. O URL de destino é o URL associado ao nome da ação a ser executada;

A função [dao.executePost] executa uma chamada HTTP:


// solicitação HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
  // fazemos uma chamada Ajax manualmente
  $.ajax({
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    url: dao.urlService + url,
    type: 'POST',
    data: JSON3.stringify(post),
    dataType: 'json',
    success: function (data) {
      // retornamos o resultado
      deferred.resolve({
        "status": 1,
        "data": data,
        "sendMeBack": sendMeBack
      });
    },
    error: function (jqXHR, textStatus, errorThrown) {
      var data;
      if (jqXHR.responseText) {
        data = jqXHR.responseText;
      } else {
        data = textStatus;
      }
      // retornamos o erro
      deferred.resolve({
        "status": 2,
        "data": data,
        "sendMeBack": sendMeBack
      });
    }
  });
};

Já abordamos e comentamos essa função anteriormente. Basta observar, na linha 9, que a função URL tem como objetivo a concatenação da função URL do servidor [Web1] com a função URL associada ao nome da ação.

8.6.8.11. A página de inicialização

  

Image

A página de inicialização [boot.html] exibe a visualização acima. Essa é a única página carregada diretamente pelo navegador. As demais são obtidas por meio de chamadas Ajax. 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>
  <!-- Núcleo do Bootstrap 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"/>
  <!-- Estilos personalizados para este modelo -->
  <link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
  <!-- Núcleo do Bootstrap 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>
  <!-- scripts de usuários -->
  <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"/>
        <!-- formulário de identificação -->
        <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>
          <!-- idiomas -->
          <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">
  <!-- Jumbotron do Bootstrap -->
  <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>
  <!-- mensagens de erro -->
  <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>
  <!-- conteúdo -->
  <div id="content">
    <div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
  </div>
</div>
<!-- página inicial -->
<script>
  // inicializando a página
  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á encontramos esse tipo de página no capítulo sobre Bootstrap (parágrafo 8.6.4);
  • linhas 99-105: inicialização de alguns elementos da camada [présentation];
  • linha 27: o script [getAccueilAvecAgenda-sequence.js] é utilizado. Ao alterar o script dessa linha, obtemos três comportamentos diferentes para gerar a página [accueil-avec-agenda]:
    • [getAccueilAvecAgenda-one.js] obtém a página com uma única chamada a HTTP,
    • [getAccueilAvecAgenda-parallel.js] obtém a página por meio de quatro chamadas simultâneas de HTTP,
    • [getAccueilAvecAgenda-sequence.js] obtém a página com quatro chamadas sucessivas de HTTP;

8.6.8.12. Tests

Existem diferentes maneiras de realizar os testes. Aqui, vamos utilizar a ferramenta [Webstorm]:

  • no [1], abrimos um projeto. Basta indicar a pasta [2] que contém a estrutura estática (HTML, CSS, JS) do site a ser testado;
  • em [3], o site estático;
  • em [4-5], carrega-se a página [boot.html];
  • em [5], percebe-se que um servidor incorporado por [Webstorm] entregou a página [boot.html] a partir da porta [63342]. É importante compreender isso, pois significa que os scripts da página [boot.html] farão chamadas entre domínios para o servidor [Web1], que, por sua vez, opera na página [localhost:8081]. O navegador que carregou a página [boot.html] sabe que a carregou a partir da página [localhost:63342]. Portanto, ele não aceitará que essa página faça chamadas para o site [localhost:8081], pois não se trata da mesma porta. Portanto, ele implementará as chamadas entre domínios descritas no parágrafo 8.4.14. Por esse motivo, é necessário que o aplicativo [Web1] esteja configurado para aceitar essas chamadas entre domínios. É no arquivo [AppConfig] do servidor Spring/Thymeleaf que isso é definido:
 

@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";
    // raiz do serviço web / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // tempo limite em milissegundos
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;
...

Deixamos que o leitor realize os testes do cliente JS. Ele deve ser capaz de reproduzir as funcionalidades descritas no parágrafo 8.6.3.

Assim que o cliente JS for considerado correto, ele poderá ser implantado na pasta do servidor [Web1] para evitar a necessidade de autorizar solicitações entre domínios:

  

Acima, copiamos o site testado para a pasta [src / main / resources / static]. Em seguida, podemos solicitar o URL e o [http://localhost:8081/boot.html]:

Image

Agora não precisamos mais das solicitações entre domínios e podemos gravar no arquivo de configuração [AppConfig] do servidor [Web1]:


    // CORS
    private final boolean CORS_ALLOWED=false;

O aplicativo acima continuará funcionando. Se voltarmos ao aplicativo [Webstorm], ele não funcionará mais:

Image

Image

Se acessarmos o console de desenvolvimento (Ctrl-Shift-I), veremos a causa do erro:

Image

Trata-se de um erro de solicitação entre domínios não autorizada.

8.6.8.13. Conclusion

Implementamos a seguinte arquitetura JS:

  • as camadas estão bem separadas;
  • temos um aplicativo do tipo APU (aplicativo de página única). É essa característica que agora nos permitirá gerar um aplicativo nativo para diversos dispositivos móveis (Android, IoS, Windows Phone);
  • criamos um modelo capaz de executar ações assíncronas em paralelo, em sequência ou uma combinação das duas;

8.6.9. etapa 6: geração de um aplicativo nativo para Android

A ferramenta [Phonegap] [http://phonegap.com/] permite produzir um executável para dispositivos móveis (Android, IoS, Windows 8, ...) a partir de um aplicativo HTML / JS / CSS. Existem diferentes maneiras de atingir esse objetivo. Usamos a mais simples: uma ferramenta disponível online no site do Phonegap [http://build.phonegap.com/apps]. Essa ferramenta fará o upload do arquivo zip do site estático a ser convertido. A página inicial deve se chamar [index.html]. Portanto, renomeamos a página [boot.html] para [index.html]:

 

em seguida, compactamos a pasta, neste caso [rdvmedecins-client-js-03]. Depois, acessamos o site do Phonegap [http://build.phonegap.com/apps]:

  • antes de [1], talvez seja necessário criar uma conta;
  • em [1], começamos;
  • em [2], escolhe-se um plano gratuito que permite apenas um aplicativo Phonegap;
  • em [3], baixe o aplicativo compactado em [4];
  • em [5], dá-se um nome ao aplicativo;
  • em [6], compila-se o aplicativo. Essa operação pode levar 1 minuto. Aguarde até que os ícones das diferentes plataformas móveis indiquem que a compilação foi concluída;
  • apenas os binários do Android ([7]) e do Windows ([8]) foram gerados;
  • clique em [7] para baixar o binário do Android;
  • em [9], o binário [apk] baixado;

Inicie um emulador [GenyMotion] para um tablet Android (consulte o parágrafo 9.9):

 

Acima, iniciamos um emulador de tablet com o API 19 do Android. Assim que o emulador for iniciado,

  • desbloqueie-o deslizando a trava (se houver) para o lado e soltando-a;
  • com o mouse, arraste o arquivo [PGBuildApp-debug.apk] que você baixou e solte-o no emulador. Ele será então instalado e executado;

É preciso substituir o URL por [1]. Para isso, em uma janela de comando, digite o comando [ipconfig] (linha 1 abaixo), que exibirá os diferentes 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 Wi-Fi IP (linhas 6 a 9) ou o endereço na rede local IP (linhas 11 a 17). Em seguida, utilize esse endereço IP no URL do servidor web:

Feito isso, acesse o serviço web:

Teste o aplicativo no emulador. Ele deve funcionar. No lado do servidor, é possível autorizar ou não os cabeçalhos CORS na classe [ApplicationModel]:


    // CORS
    private final boolean CORS_ALLOWED=false;

Isso não faz diferença para o aplicativo Android. Ele não é executado em um navegador. A exigência dos cabeçalhos CORS vem do navegador e não do servidor.

8.6.10. Conclusão do estudo de caso

Desenvolvemos a seguinte arquitetura:

Trata-se de uma arquitetura de três camadas complexa. O objetivo era reutilizar a camada [Web2], que era a camada de servidor do aplicativo [AngularJS-Spring MVC] do documento [Tutoriel AngularJS / Spring 4] para oURL e [http://tahe.developpez.com/angularjs-spring4/]. É exclusivamente por esse motivo que temos uma arquitetura de três camadas. Já 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]. Aumentamos o número de camadas, portanto, teremos uma perda de desempenho.

A aplicação analisada aqui foi desenvolvida ao longo do tempo em três documentos diferentes:

  1. [Introduction aux frameworks JSF2, Primefaces et Primefaces mobile], URL e [http://tahe.developpez.com/java/primefaces/]. O estudo de caso havia sido desenvolvido com os frameworks JSF2 / Primefaces. O Primefaces é uma biblioteca de componentes com Ajax que dispensa a necessidade de escrever JavaScript. A aplicação desenvolvida na época era menos complexa do que a analisada aqui. Ela possuía uma versão web clássica para computadores e uma versão móvel para celulares;
  2. [Tutoriel AngularJS / Spring 4] para URL e [http://tahe.developpez.com/angularjs-spring4/]. A aplicação desenvolvida na época apresentava as mesmas características daquela estudada neste documento. A aplicação também havia sido portada para o Android;
  3. o presente documento;

Deste trabalho, destaco os seguintes pontos:

  • o aplicativo [Primefaces] foi, de longe, o mais simples de programar e sua versão web para dispositivos móveis se mostrou eficiente. Ele não requer conhecimentos de JavaScript. Não é possível portá-la nativamente para os OS dos diversos dispositivos móveis, mas será que isso é necessário? Parece difícil alterar o estilo da aplicação. De fato, trabalhamos com as folhas de estilo do Primefaces. Isso pode ser uma desvantagem;
  • a aplicação [AngularJS-Spring MVC] foi complexa de escrever. O framework [AngularJS] me pareceu bastante difícil de compreender quando se quer dominá-lo. A arquitetura [client Angular] / [service web / jSON implémenté par Spring MVC] é particularmente limpa e eficiente. Essa arquitetura é reproduzível para qualquer aplicativo web. É a arquitetura que me parece mais promissora, pois envolve competências diferentes tanto no lado do cliente quanto no lado do servidor (JS+HTML+CSS no lado do cliente, Java ou outra linguagem no lado do servidor), o que permite desenvolver o cliente e o servidor em paralelo;
  • para o aplicativo desenvolvido neste documento com uma arquitetura de três camadas [client jQuery] / [serveur Web1 / Spring MVC / Thymeleaf] / [serveur Web2 / Spring MVC], é possível que alguns considerem a tecnologia [jQuery+Spring MVC+Thymelaf] mais fácil de compreender do que a de [AngularJS]. A camada [DAO] do cliente JavaScript que escrevemos é reutilizável em outras aplicações;