8. Estudo de caso
8.1. Introduction
Propomos-nos a escrever uma aplicação web para marcação de consultas num consultório médico. Este problema foi abordado no documento «Tutorial AngularJS / Spring 4» na secção URL [http://tahe.developpez.com/angularjs-spring4/]. A arquitetura desta aplicação era a seguinte:
![]() |
- em [1], um servidor web fornece páginas estáticas a um navegador. Estas páginas contêm uma aplicação AngularJS construída com base no modelo MVC (Modelo – Vista – Controlador). O modelo, neste caso, é simultaneamente o das vistas e o do domínio, aqui representado pela camada [Services];
- o utilizador irá interagir com as vistas que lhe são apresentadas no navegador. As suas ações irão, por vezes, exigir a consulta do servidor Spring 4 [2]. Este irá processar o pedido e devolver uma resposta jSON (JavaScript Object Notation) [3]. Esta será utilizada para atualizar a vista apresentada ao utilizador.
Propomos retomar esta aplicação e implementá-la de ponta a ponta com o Spring MVC. A arquitetura passa então a ser a seguinte:
![]() |
O navegador irá ligar-se a uma aplicação [Web 1] implementada pelo Spring MVC, que irá buscar os seus dados a 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]:
![]() | ![]() |
Em primeiro lugar, vamos criar a base de dados MySQL 5 [dbrdvmedecins] com a ferramenta [Wamp Server] (ver parágrafo 9.5):
![]() |
- em [1], seleciona-se a ferramenta [phpMyAdmin] de WampServer;
- em [2], escolhe-se a opção [Importer];
![]() |
- no [3], seleciona-se o ficheiro [database/dbrdvmedecins.sql];
- no [4], executa-se o programa;
- em [5], a base de dados é criada.
Em seguida, temos de iniciar o servidor ligado à base de dados. Trata-se do projeto [rdvmedecins-webjson-server]
![]() |
O servidor ficará disponível em URL [http://localhost:8080]. Isto pode ser alterado no ficheiro [application.properties] do projeto:
![]() |
server.port=8080
As características de acesso à base de dados estão registadas 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 aceder ao SGBD MySQL com outros identificadores, é aqui que tudo acontece.
Em seguida, da mesma forma que o servidor anterior, inicia-se o servidor [rdvmedecins-springthymeleaf-server]:
![]() | ![]() |
Este servidor está, por predefinição, disponível em URL [http://localhost:8081]. Mais uma vez, isto é configurável no ficheiro [application.properties] do projeto:
server.port=8081
Além disso, este servidor deve conhecer o URL do servidor ligado à base de dados. Esta configuração encontra-se na classe [AppConfig] acima referida:
// 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 numa porta diferente da 8080, é necessário alterar a linha 5.
Em seguida, utilizando um navegador, acede-se ao URL e ao [http://localhost:8081/boot.html]:
![]() |
- em [1], a página de início de sessão da aplicação;
- em [2] e [3], o nome de utilizador e a palavra-passe de quem pretende utilizar a aplicação. Existem dois utilizadores: admin/admin (login/password) com uma função (ADMIN) e user/user com uma função (USER). Apenas a função ADMIN tem permissão para utilizar a aplicação. A função USER existe apenas para mostrar a resposta do servidor neste caso de utilização;
- em [4], o botão que permite ligar-se ao servidor;
- em [5], o idioma da aplicação. Existem dois: o francês, por predefinição, e o inglês;
- em [6], o URL do servidor [rdvmedecins-springthymeleaf-server];
![]() |
- em [1], efetua-se o início de sessão;
![]() |
- uma vez conectado, pode-se escolher o médico com quem se deseja marcar uma consulta [2] e o dia da mesma [3]. Assim que o médico e o dia forem indicados, a agenda é automaticamente apresentada:
![]() |
- Depois de obter a agenda do médico, é possível reservar um horário [5];
![]() |
- em [6], seleciona-se o doente para a consulta e confirma-se essa escolha em [7];
![]() |
Assim que a consulta for validada, é-se redirecionado automaticamente para a agenda, onde a nova consulta já se encontra registada. Esta consulta poderá ser posteriormente eliminada em [8].
As principais funcionalidades foram descritas. São simples. Terminemos com a gestão do idioma:
1

- em [1], muda-se do francês para o inglês;
![]() |
- em [2], a visualização passou para inglês, incluindo o calendário;
8.3. A base de dados
![]() |
A base de dados, doravante designada por [dbrdvmedecins], é uma base de dados MySQL5 com as seguintes tabelas:
![]() |
As consultas são geridas pelas seguintes tabelas:
- [medecins]: contém a lista dos médicos do consultório;
- [clients]: contém a lista de doentes do consultório;
- [creneaux]: contém os horários disponíveis de cada um dos médicos;
- [rv]: contém a lista das consultas dos médicos.
As tabelas [roles], [users] e [users_roles] são tabelas relacionadas com a autenticação. Por enquanto, não vamos ocupar-nos delas. As relações entre as tabelas que gerem as consultas são as seguintes:
![]() |
- um horário pertence a um médico – um médico tem 0 ou mais horários;
- uma consulta reúne simultaneamente um cliente e um médico através de um intervalo horário deste último;
- um cliente tem 0 ou mais consultas;
- a um intervalo horário estão associadas 0 ou mais consultas (em dias diferentes).
8.3.1. A tabela [MEDECINS]
Contém informações sobre os médicos geridos pela aplicação [RdvMedecins].
![]() | ![]() |
- ID: número que identifica o médico — chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
- NOM: o nome do médico
- PRENOM: o seu nome próprio
- TITRE: o seu título (Menina, Sra., Sr.)
8.3.2. A tabela [CLIENTS]
Os clientes dos diferentes médicos estão registados na tabela [CLIENTS]:
![]() | ![]() |
- ID: número de identificação do cliente — chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
- NOM: o nome do cliente
- PRENOM: o seu nome próprio
- TITRE: o seu título (Menina, Sra., Sr.)
8.3.3. A tabela [CRENEAUX]
Esta tabela lista os intervalos horários em que os RV são possíveis:
![]() |
![]() |
- ID: número que identifica o intervalo horário — chave primária da tabela (linha 8)
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 sempre que é feita uma alteração na linha.
- ID_MEDECIN: número que identifica o médico a quem pertence este intervalo horário – chave estrangeira na coluna MEDECINS (ID).
- HDEBUT: hora de início do horário
- MDEBUT: minutos de início do intervalo
- HFIN: hora de fim do intervalo
- MFIN: minutos de fim do intervalo
A segunda linha da tabela [CRENEAUX] (ver [1] acima) indica, por exemplo, que o intervalo n.º 2 começa às 8h20 e termina às 8h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER).
8.3.4. A tabela [RV]
Esta tabela lista os RV atribuídos a cada médico:
![]() |
- ID: número que identifica o RV de forma única – chave primária
- JOUR: dia do RV
- ID_CRENEAU: intervalo horário do RV – chave estrangeira no campo [ID] da tabela [CRENEAUX] – define simultaneamente o intervalo horário e o médico em questão.
- ID_CLIENT: número do cliente para quem é feita a reserva – chave estrangeira no campo [ID] da tabela [CLIENTS]
Esta tabela tem uma restrição de unicidade sobre os valores das colunas associadas (JOUR, ID_CRENEAU):
Se uma linha da tabela [RV] tiver o valor (JOUR1, ID_CRENEAU1) nas colunas (JOUR, ID_CRENEAU), esse valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que dois RV foram registados ao mesmo tempo para o mesmo médico. Do ponto de vista da programação Java, o controlador JDBC da base de dados lança um SQLException quando esta situação ocorre.
A linha de id igual a 3 (ver [1] acima) significa que um RV foi marcado para o intervalo n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [CRENEAUX] indica-nos que o horário n.º 20 corresponde ao intervalo horário das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] indica-nos que o cliente n.º 4 é a Srta. Brigitte BISTROU.
8.3.5. Criação da base de dados
Para criar a base de dados [dbrdvmedecins], é fornecido um script [dbrdvmedecins.sql] juntamente com os exemplos deste documento [1-3]:
![]() |
Utilizamos a ferramenta [PhpMyAdmin] da WampServer:
![]() |
- no [1], seleciona-se a ferramenta [phpMyAdmin] do WampServer;
- em [2], escolhe-se a opção [Importer];
![]() |
- no [3], seleciona-se o ficheiro [database/dbrdvmedecins.sql];
- no [4], executa-se o comando;
- em [5], a base de dados é criada.
8.4. O serviço web / jSON
![]() |
Na arquitetura acima, abordamos 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;
- depois, adicionaremos a parte da autenticação com o Spring Security.
O que se segue é uma reprodução do documento [http://tahe.developpez.com/angularjs-spring4/], com algumas alteraçõ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 existem vários tutoriais para dar os primeiros passos com o Spring [http://spring.io/guides]. Vamos utilizar um deles para apresentar o Spring Data. Para tal, utilizamos o Spring Tool Suite (STS).
![]() |
- em [1], importamos um dos tutoriais de [spring.io/guides];
![]() |
- em [2], selecionamos o tutorial [Accessing Data Jpa], que mostra como aceder a uma base de dados com o Spring Data;
- em [3], escolhe-se um projeto configurado pelo Maven;
- em [4], o tutorial pode ser apresentado de duas formas: [initial], que é uma versão em branco que se preenche seguindo o tutorial, ou [complete], que é a versão final do tutorial. Escolhemos esta última;
- em [5], é possível optar por visualizar o tutorial num navegador;
- em [6], o projeto final.
8.4.1.1. A configuração Maven do projeto
As dependências Maven do projeto estão configuradas no ficheiro [pom.xml]:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!-- utilizar UTF-8 para tudo -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- linhas 5-9: definem um projeto pai do Maven. É este que define a maior parte das dependências do projeto. Estas podem ser suficientes, caso em que não se adicionam mais, ou não, caso em que se adicionam as dependências em falta;
- linhas 12-15: definem uma dependência do [spring-boot-starter-data-jpa]. Este artefacto contém as classes do Spring Data;
- linhas 16-19: definem uma dependência do SGBD e do H2, que permitem criar e gerir bases de dados em memória.
Vejamos as classes fornecidas por estas dependências:
![]() | ![]() | ![]() |
São em grande número:
- algumas pertencem ao ecossistema Spring (as que começam por «spring»);
- outras pertencem ao ecossistema Hibernate (hibernate, jboss), cuja implementação JPA é aqui utilizada;
- outras são bibliotecas de testes (junit, hamcrest);
- outras são bibliotecas de registos (log4j, logback, slf4j);
Vamos mantê-las todas. Para uma aplicação em produção, seria necessário manter apenas as que são necessárias.
Na linha 26 do ficheiro [pom.xml], encontra-se a linha:
<start-class>hello.Application</start-class>
Esta linha está relacionada com as seguintes linhas:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Nas linhas 6 a 9, o plugin [spring-boot-maven-plugin] permite gerar o ficheiro JAR executável da aplicação. A linha 26 do ficheiro [pom.xml] indica, então, a classe executável desse ficheiro JAR.
8.4.1.2. A camada [JPA]
O acesso à base de dados é feito através de uma camada [JPA], Java Persistence API:
![]() |
![]() |
A aplicação é básica e gere clientes [Customer]. A classe [Customer] faz parte da camada [JPA] e é a seguinte:
package hello;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
}
}
Um cliente tem um identificador [id], um nome próprio [firstName] e um apelido [lastName]. Cada instância [Customer] representa uma linha de uma tabela da base de dados.
- linha 8: anotação JPA que faz com que a persistência das instâncias [Customer] (Create, Read, Update, Delete) venha a ser gerida por uma implementação JPA. De acordo com as dependências do Maven, verifica-se que é utilizada a implementação JPA / Hibernate;
- linhas 11-12: anotações JPA que associam o campo [id] à chave primária da tabela [Customer]. A linha 12 indica que a implementação JPA utilizará o método de geração da chave primária específico do SGBD utilizado, neste caso o H2;
Não existem outras anotações para JPA. Serão, então, utilizados valores por predefinição:
- a tabela [Customer] terá o nome da classe, ou seja, [Customer];
- as colunas desta tabela terão os nomes dos campos da classe: [id, firstName, lastName], tendo em conta que as maiúsculas e minúsculas não são distinguidas no nome de uma coluna da tabela;
Note-se que, em nenhum momento, a implementação JPA utilizada é referida pelo nome.
8.4.1.3. A camada [DAO]
![]() |
![]() |
A classe [CustomerRepository] implementa a camada [DAO]. O seu código é o seguinte:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
Trata-se, portanto, de uma interface e não de uma classe (linha 7). Ela estende a interface [CrudRepository], uma interface do Spring Data (linha 5). Esta interface é parametrizada por dois tipos: o primeiro é o tipo dos elementos geridos, neste caso o tipo [Customer]; o segundo é o tipo da chave primária dos elementos geridos, neste caso um tipo [Long]. A interface [CrudRepository] é a seguinte:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
}
Esta interface define as operações CRUD (Criar – Ler – Atualizar – Eliminar) que podem ser realizadas num tipo JPA T:
- linha 8: o método save permite persistir uma entidade T na base de dados. Este método persiste a entidade com a chave primária que lhe foi atribuída pelo SGBD. Permite também atualizar uma entidade T identificada pela sua chave primária id. A escolha de uma ou outra ação depende do valor da chave primária id: se este for nulo, é realizada a operação de persistência; caso contrário, é realizada a operação de atualização;
- linha 10: o mesmo, mas para uma lista de entidades;
- linha 12: o método findOne permite recuperar uma entidade T identificada pela sua chave primária id;
- linha 22: o método delete permite eliminar uma entidade T identificada pela sua chave primária id;
- linhas 24-28: variantes do método [delete];
- linha 16: o método [findAll] permite recuperar todas as entidades T persistentes;
- linha 18: o mesmo, mas limitado às entidades cuja lista de identificadores foi passada;
Voltemos à interface [CustomerRepository]:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
- a linha 9 permite recuperar um [Customer] pelo seu nome [lastName];
E é tudo no que diz respeito à camada [DAO]. Não existe uma classe de implementação da interface anterior. Esta é gerada em tempo de execução pelo [Spring Data]. Os métodos da interface [CrudRepository] são implementados automaticamente. Quanto aos métodos adicionados à interface [CustomerRepository], isso depende. Voltemos à definição de [Customer]:
private long id;
private String firstName;
private String lastName;
O método da linha 9 é implementado automaticamente por [Spring Data] porque faz referência ao campo [lastName] (linha 3) de [Customer]. Quando encontra um método [findBySomething] na interface a implementar, o Spring Data implementa-o através da seguinte consulta JPQL (Java Persistence Query Language):
É, portanto, necessário que o tipo T tenha um campo denominado [something]. Assim, o método
será implementado por um código semelhante ao seguinte:
return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()
onde [em] designa o contexto de persistência JPA. Isto só é possível se a classe [Customer] tiver um campo denominado [lastName], o que é o caso.
Em conclusão, em casos simples, o Spring Data permite-nos implementar a camada [DAO] com uma interface simples.
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);
// guardar alguns clientes
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// recuperar todos os clientes
Iterable<Customer> customers = repository.findAll();
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : customers) {
System.out.println(customer);
}
System.out.println();
// recuperar um cliente específico através de ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// recuperar clientes pelo apelido
List<Customer> bauers = repository.findByLastName("Bauer");
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : bauers) {
System.out.println(bauer);
}
context.close();
}
}
- na linha 10: indica que a classe serve para configurar o Spring. As versões recentes do Spring podem, de facto, ser configuradas em Java em vez de em XML. Ambos os métodos podem ser utilizados em simultâneo. No código de uma classe com a anotação [Configuration], encontram-se normalmente beans do Spring, ou seja, definições de classes a instanciar. Aqui, não está definido nenhum bean. É importante recordar que, ao trabalhar com um SGBD, devem ser definidos vários beans do Spring:
- um [EntityManagerFactory] que define a implementação JPA a utilizar,
- um [DataSource] que define a fonte de dados a utilizar,
- um [TransactionManager] que define o gestor de transações a utilizar;
Aqui, nenhum destes beans está definido.
- na linha 11: a anotação [EnableAutoConfiguration] é uma anotação proveniente do projeto [Spring Boot] (linhas 5-6). Esta anotação solicita ao Spring Boot, através da classe [SpringApplication] (linha 16), que configure a aplicação de acordo com as bibliotecas encontradas no seu Classpath. Como as bibliotecas do Hibernate estão no Classpath, o bean [entityManagerFactory] será implementado com o Hibernate. Como a biblioteca SGBD H2 se encontra no Classpath, o bean [dataSource] será implementado com H2. No bean [dataSource], é necessário definir também o utilizador e a sua palavra-passe. Aqui, o Spring Boot utilizará o administrador predefinido de H2, que não tem palavra-passe. Como a biblioteca [spring-tx] se encontra no Classpath, será utilizado o gestor de transações do Spring.
Além disso, a pasta onde se encontra a classe [Application] será analisada à procura de beans reconhecidos implicitamente pelo Spring ou definidos explicitamente por anotações do Spring. Assim, as classes [Customer] e [CustomerRepository] serão inspecionadas. Como a primeira possui a anotação [@Entity], será catalogada como uma entidade a ser gerida pelo Hibernate. Como a segunda estende a interface [CrudRepository], será registada como um bean do Spring.
Analisemos as linhas 16-17 do código:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- linha 16: o método estático [run] da classe [SpringApplication] do projeto Spring Boot é executado. O seu parâmetro é a classe que possui uma anotação [Configuration] ou [EnableAutoConfiguration]. Tudo o que foi explicado anteriormente irá então ocorrer. O resultado é um contexto de aplicação Spring, ou seja, um conjunto de beans geridos pelo Spring;
- linha 17: solicita-se a este contexto Spring um bean que implemente a interface [CustomerRepository]. Aqui, recuperamos a classe gerada pelo Spring Data para implementar esta interface.
As operações que se seguem limitam-se a utilizar os métodos do bean que implementa a interface [CustomerRepository]. Note-se, na linha 50, que o contexto é encerrado. Os resultados apresentados na consola são os seguintes:
- linhas 1-8: o logótipo do projeto Spring Boot;
- linha 9: a classe [hello.Application] é executada;
- linha 10: [AnnotationConfigApplicationContext] é uma classe que implementa a interface [ApplicationContext] do Spring. Trata-se de um contentor de beans;
- linha 11: o bean [entityManagerFactory] é implementado pela classe [LocalContainerEntityManagerFactory], uma classe do Spring;
- linha 15: surge a [Hibernate]. Foi esta implementação, a JPA, que foi escolhida;
- linha 19: um dialeto do Hibernate é a variante SQL a utilizar com o SGBD. Aqui, o dialeto [H2Dialect] indica que o Hibernate irá trabalhar com o SGBD e o H2;
- linhas 21-22: a base de dados é criada. A tabela [CUSTOMER] é criada. Isto significa que o Hibernate foi configurado para gerar as tabelas a partir das definições JPA; neste caso, a definição JPA da classe [Customer];
- linhas 27-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: registos do encerramento 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 basear-nos na configuração automática feita pelo Spring Boot. Vamos fazê-la manualmente. Isto pode ser útil se as configurações predefinidas não nos servirem.
Em primeiro lugar, vamos especificar as dependências necessárias no ficheiro [pom.xml]:
...
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.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 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>
<!-- Base 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 gerir transações com uma base de dados;
- linhas 30-35: a biblioteca do Spring para trabalhar com um ORM (Object Relational Mapper);
- linhas 36-41: Spring Data utilizado para aceder à base de dados;
- linhas 42-47: o Spring Boot para iniciar a aplicação;
- linhas 54-59: o SGBD H2;
- linhas 60-70: as bases de dados são frequentemente utilizadas com conjuntos de ligações abertas, o que evita a abertura e o encerramento repetidos de ligações. Aqui, a implementação utilizada é a de [commons-dbcp];
Ainda no [pom.xml], altera-se o nome da classe executável:
<properties>
...
<start-class>demo.console.Main</start-class>
</properties>
No novo projeto, a entidade [Customer] e a interface [CustomerRepository] não sofrem alterações. Vamos alterar a classe [Application], que será dividida em duas classes:
- [Config], que será a classe de configuração:
- [Main], que será a classe executável;
![]() |
A classe executável [Main] é a mesma que a anterior, sem as anotações de configuração:
package demo.console;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
...
context.close();
}
}
- linha 12: a classe [Main] já não tem anotações de configuração;
- linha 16: a aplicação é iniciada com o Spring Boot. O parâmetro [Config.class] é a nova classe de configuração do projeto;
A classe [Config] que configura o projeto é a seguinte:
package demo.config;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
// a fonte de dados H2
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// o fornecedor JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("demo.entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Gestor de transações
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- linha 22: a anotação [@Configuration] transforma a classe [Config] numa classe de configuração do Spring;
- linha 21: a anotação [@EnableJpaRepositories] permite indicar as pastas onde se encontram as interfaces Spring Data [CrudRepository]. Estas interfaces tornar-se-ão componentes Spring e estarão disponíveis no seu contexto;
- linha 20: a anotação [@EnableTransactionManagement] indica que os métodos das interfaces [CrudRepository] devem ser executados no âmbito de uma transação;
- linha 19: a anotação [@EntityScan] permite indicar as pastas onde as entidades JPA devem ser procuradas. Aqui, foi colocada em comentário, porque esta informação foi fornecida explicitamente na linha 50. Esta anotação deve estar presente se se utilizar o modo [@EnableAutoConfiguration] e as entidades JPA não se encontrarem na mesma pasta que a classe de configuração;
- linha 18: a anotação [@ComponentScan] permite listar as pastas onde os componentes Spring devem ser procurados. Os componentes Spring são classes marcadas com anotações Spring, tais como @Service, @Component, @Controller, ... Aqui, não existem outros além dos que estão definidos na classe [Config], pelo que a anotação foi colocada em comentário;
- linhas 25-33: definem a fonte de dados, a base de dados H2. É a anotação @Bean na linha 25 que torna o objeto criado por este método um componente gerido pelo Spring. O nome do método pode ser qualquer um. No entanto, deve ser chamado [dataSource] se o EntityManagerFactory da linha 47 estiver ausente e for definido por autoconfiguração;
- linha 29: a base de dados chamar-se-á [demo] e será gerada na pasta do projeto;
- linhas 36-43: definem a implementação JPA utilizada, neste caso uma implementação do Hibernate. O nome do método pode ser qualquer um;
- linha 39: sem registos SQL;
- linha 30: a base de dados será criada caso não exista;
- linhas 46-54: definem o EntityManagerFactory que irá gerir a persistência do JPA. O método deve chamar-se obrigatoriamente [entityManagerFactory];
- linha 47: o método recebe dois parâmetros com o tipo dos dois beans definidos anteriormente. Estes serão então criados e, em seguida, injetados pelo Spring como parâmetros do método;
- linha 49: define a implementação JPA utilizada;
- linha 50: define as pastas onde se encontram as entidades JPA;
- linha 51: define a fonte de dados a gerir;
- linhas 57-62: o gestor de transações. O método deve chamar-se obrigatoriamente [transactionManager]. Recebe como parâmetro o bean das linhas 46-54;
- linha 60: o gestor de transações é associado ao EntityManagerFactory;
Os métodos anteriores podem ser definidos em qualquer ordem.
A execução do projeto produz os mesmos resultados. Aparece um novo ficheiro na pasta do projeto, o da base de dados H2:
![]() |
Por fim, é possível prescindir do Spring Boot. Cria-se uma segunda classe executável, [Main2]:
![]() |
A classe [Main2] tem o seguinte código:
package demo.console;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
....
context.close();
}
}
- linha 15: a classe de configuração [Config] é agora utilizada pela classe Spring [AnnotationConfigApplicationContext]. Pode-se ver na linha 5 que já não existem 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 executar (utilize o botão Browse);
- em [4]: indica a classe a executar;
- em [5]: o nome da configuração de execução – pode ser qualquer um;
![]() |
- em [6]: exporta-se o projeto;
- em [7]: sob a forma de um arquivo executável JAR;
- em [8]: indica o caminho e o nome do ficheiro executável a criar;
- em [9]: o nome da configuração de execução criada em [5];
Feito isto, abre-se um terminal na pasta que contém o arquivo executável:
O arquivo é executado da seguinte forma:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
Os resultados obtidos no terminal são os seguintes:
8.4.1.7. Criar um novo projeto Spring Data
Para criar um esqueleto de projeto Spring Data, pode-se proceder da seguinte forma:
![]() |
- em [1], cria-se um novo projeto;
- em [2]: do tipo [Spring Starter Project];
- o projeto gerado será um projeto Maven. Em [3], indica-se o nome do grupo do projeto;
- no [4]: indica-se o nome do artefacto (um jar, neste caso) que será criado durante a compilação do projeto;
- em [5]: indica-se o pacote da classe executável que será criada no projeto;
- em [6]: o nome do projeto no Eclipse – pode ser qualquer um (não tem de ser idêntico a [4]);
- em [7]: indica-se que se vai criar um projeto com uma camada [JPA]. As dependências necessárias para esse projeto serão então incluídas no ficheiro [pom.xml];
![]() |
- em [8]: o projeto criado;
O ficheiro [pom.xml] integra as dependências necessárias para um projeto JPA:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath/> <!-- pesquisar o pai no repositório -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- linhas 9-12: as dependências necessárias para o JPA – irão incluir o [Spring Data];
- linhas 13-17: as dependências necessárias para os testes JUnit integrados com o Spring;
A classe executável [Application] não faz nada, mas está pré-configurada:
package istia.st;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
A classe de testes [ApplicationTests] não faz nada, mas está pré-configurada:
package istia.st;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Test
public void contextLoads() {
}
}
- linha 9: a anotação [@SpringApplicationConfiguration] permite utilizar o ficheiro de configuração [Application]. A classe de teste beneficiará assim de todos os beans que forem definidos por este ficheiro;
- linha 8: a anotação [@RunWith] permite a integração do Spring com JUnit: a classe poderá ser executada como um teste JUnit. [@RunWith] é uma anotação JUnit (linha 4), enquanto a classe [SpringJUnit4ClassRunner] é uma classe Spring (linha 6);
Agora que temos um esqueleto de aplicação JPA, podemos completá-lo para escrever o projeto da camada de persistência do servidor da nossa aplicação de gestão de compromissos.
8.4.2. O projeto Eclipse do servidor
![]() |
![]() |
Os principais elementos do projeto são os seguintes:
- [pom.xml]: ficheiro de configuração Maven do projeto;
- [rdvmedecins.entities]: as entidades JPA;
- [rdvmedecins.repositories]: as interfaces Spring Data de acesso às entidades JPA;
- [rdvmedecins.metier]: a camada [métier];
- [rdvmedecins.domain]: as entidades manipuladas pela camada [métier];
- [rdvmdecins.config]: as classes de configuração da camada de persistência;
- [rdvmedecins.boot]: uma aplicação de consola básica;
8.4.3. A configuração do Maven
![]() | ![]() | ![]() |
O ficheiro [pom.xml] do projeto é o seguinte:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Teste 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>
<!-- utilize 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 baseia-se no projeto pai [spring-boot-starter-parent]. Para as dependências já presentes no projeto pai, não se especifica a versão. Será utilizada a versão definida no projeto pai. Quanto às restantes dependências, estas são declaradas normalmente;
- linhas 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 encriptação de palavras-passe;
- linhas 31-34: controlador JDBC do SGBD MySQL5;
- linhas 36-39: pool de ligações do Tomcat JDBC. Um pool de ligações reúne ligações abertas a uma base de dados. Quando o código pretende abrir uma ligação, esta é solicitada ao pool. Quando o código encerra a ligação, esta não é encerrada, mas devolvida ao pool. Tudo isto ocorre de forma transparente ao nível do código. Ganha-se em desempenho, uma vez que a abertura e o encerramento repetidos de uma ligação têm um custo em termos de tempo. Aqui, o pool de ligações estabelece um determinado número de ligações com a base de dados logo após a sua instanciação. Posteriormente, não há abertura nem encerramento de ligações, a menos que o número de ligações armazenadas no pool se revele insuficiente. Nesse caso, o pool cria automaticamente novas ligações;
- linhas 41-44: biblioteca Jackson para gestão do jSON;
- linhas 46-50: biblioteca do Google para gestão de coleções;
8.4.4. As entidades JPA
![]() |
As entidades JPA são os objetos que irão encapsular as linhas das tabelas da base de dados.
![]() |
A classe [AbstractEntity] é a classe pai das entidades [Personne, Creneau, Rv]. A sua definição é a seguinte:
package rdvmedecins.entities;
import java.io.Serializable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.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 desta chave primária é gerado pelo SGBD e que o modo de geração [IDENTITY] é imposto. Para o SGBD MySQL, isto significa que as chaves primárias serão geradas pelo SGBD com o atributo [AUTO_INCREMENT]
- linhas 18-19: definem a versão de cada entidade. A implementação JPA irá incrementar este número de versão sempre que a entidade for alterada. Este número serve para impedir a atualização simultânea da entidade por dois utilizadores diferentes: dois utilizadores, U1 e U2, leem a entidade E com um número de versão igual a V1. U1 altera E e grava essa alteração na base de dados: o número de versão passa então para V1+1. U2, por sua vez, altera E e grava essa alteração na base de dados: receberá uma exceção, pois possui uma versão (V1) diferente da que consta na base de dados (V1+1);
- linhas 29-33: o método [build] permite inicializar os dois campos de [AbstractEntity]. Este método torna a referência da instância [AbstractEntity] assim inicializada;
- linhas 36-44: o método [equals] da classe é redefinido: duas entidades serão consideradas iguais se tiverem o mesmo nome de classe e o mesmo identificador id;
- linhas 21-26: quando se redefine o método [equals] de uma classe, é necessário redefinir o seu método [hashCode] (linhas 21-26). A regra é que duas entidades consideradas iguais pelo método [equals] devem, por conseguinte, ter o mesmo [hashCode]. Neste caso, o [hashCode] de uma entidade é igual à sua chave primária [id]. O [hashCode] de uma classe é utilizado, nomeadamente, na gestão 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 por predefinição
public Personne() {
}
// construtor com parâmetros
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// getters e setters
...
}
- linha 6: a anotação [@MappedSuperclass] indica que a classe anotada é a classe-pai das entidades JPA e [@Entity];
- linhas 10-15: uma pessoa tem um título (Melle), um nome próprio (Jacqueline) e um apelido (Tatou). Não é fornecida qualquer informação sobre as colunas da tabela. Por conseguinte, estas terão, por predefinição, os mesmos nomes que os campos;
A entidade [Medecin] é a seguinte:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
private static final long serialVersionUID = 1L;
// construtor por predefinição
public Medecin() {
}
// construtor com parâmetros
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
- linha 6: a classe é uma entidade JPA;
- linha 7: associada à tabela [MEDECINS] da base de dados;
- linha 8: a entidade [Medecin] deriva da entidade [Personne];
Um médico pode ser inicializado da seguinte forma:
Se, além disso, se pretender atribuir-lhe um identificador e uma versão, poderá escrever-se:
onde o método [build] é aquele definido em [AbstractEntity].
A entidade [Client] é a seguinte:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "clients")
public class Client extends Personne {
private static final long serialVersionUID = 1L;
// construtor por predefinição
public Client() {
}
// construtor com parâmetros
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// identidade
public String toString() {
return String.format("Client[%s]", super.toString());
}
}
- linha 6: a classe é uma entidade JPA;
- linha 7: associada à tabela [CLIENTS] da base de dados;
- linha 8: a entidade [Client] deriva da entidade [Personne];
A entidade [Creneau] é a seguinte:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// características de um horário de RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// um horário está associado a um médico
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// chave estrangeira
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// fabricante por predefinição
public Creneau() {
}
// construtor com parâmetros
public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
this.medecin = medecin;
this.hdebut = hdebut;
this.mdebut = mdebut;
this.hfin = hfin;
this.mfin = mfin;
}
// toString
public String toString() {
return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
}
// chave estrangeira
public long getIdMedecin() {
return idMedecin;
}
// setter - getter
...
}
- linha 10: a classe é uma entidade JPA;
- linha 11: associada à tabela [CRENEAUX] da base de dados;
- linha 12: a entidade [Creneau] deriva da entidade [AbstractEntity] e, por conseguinte, herda o identificador [id] e a versão [version];
- linha 16: hora de início do intervalo (14);
- linha 17: minutos de início do intervalo (20);
- linha 18: hora de fim do intervalo (14);
- linha 19: minutos de fim do intervalo (40);
- linhas 22-24: o médico titular do horário. A tabela [CRENEAUX] possui uma chave estrangeira na tabela [MEDECINS]. Esta relação é representada pelas linhas 22-24;
- linha 22: a anotação [@ManyToOne] indica uma relação de muitos (intervalos) para um (médico). O atributo [fetch=FetchType.LAZY] indica que, quando se solicita uma entidade [Creneau] ao contexto de persistência e esta tem de ser pesquisada na base de dados, a entidade [Medecin] não é devolvida juntamente com ela. A vantagem deste modo é que a entidade [Medecin] só é pesquisada se o programador o solicitar. Desta forma, poupa-se memória e ganha-se em desempenho;
- linha 23: indica o nome da coluna-chave estrangeira na tabela [CRENEAUX];
- linhas 27-28: a chave estrangeira na tabela [MEDECINS];
- linha 27: a coluna [ID_MEDECIN] já foi utilizada na linha 23. Isto significa que pode ser alterada de duas formas diferentes, o que não é permitido pela norma JPA. Adicionam-se, portanto, os atributos [insertable = false, updatable = false], o que faz com que a coluna só possa ser lida;
A entidade [Rv] é a seguinte:
package rdvmedecins.entities;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// características de um Rv
@Temporal(TemporalType.DATE)
private Date jour;
// um RV está associado a um cliente
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// um RV está associado a um intervalo de tempo
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// chaves externas
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
// fabricante por predefinição
public Rv() {
}
// com parâmetros
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// chaves externas
public long getIdCreneau() {
return idCreneau;
}
public long getIdClient() {
return idClient;
}
// getters e setters
...
}
- linha 14: a classe é uma entidade JPA;
- linha 15: associada à tabela [RV] da base de dados;
- linha 16: a entidade [Rv] deriva da entidade [AbstractEntity] e, por conseguinte, herda o identificador [id] e a versão [version];
- linha 21: a data do compromisso;
- linha 20: o tipo [Date] de Java contém tanto uma data como uma hora. Aqui especifica-se que apenas a data é utilizada;
- linhas 24-26: o cliente para quem esta marcação foi efetuada. A tabela [RV] possui uma chave estrangeira na tabela [CLIENTS]. Esta relação é representada pelas linhas 24-26;
- linhas 29-31: o intervalo horário do compromisso. A tabela [RV] possui uma chave estrangeira na tabela [CRENEAUX]. Esta relação é representada pelas linhas 29-31;
- linhas 34-35: a chave estrangeira [idClient];
- linhas 36-37: a chave estrangeira [idCreneau];
8.4.5. A camada [DAO]
![]() |
Vamos implementar a camada [DAO] com o Spring Data:
![]() |
A camada [DAO] é implementada com quatro interfaces Spring Data:
- [ClientRepository]: dá acesso às entidades JPA e [Client];
- [CreneauRepository]: dá acesso às entidades JPA e [Creneau];
- [MedecinRepository]: dá acesso às entidades JPA e [Medecin];
- [RvRepository]: dá acesso às entidades JPA e [Rv];
A interface [MedecinRepository] é a seguinte:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
- linha 7: a interface [MedecinRepository] limita-se a herdar os métodos da interface [CrudRepository], sem adicionar outros;
A interface [ClientRepository] é a seguinte:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
- linha 7: a interface [ClientRepository] limita-se a herdar os métodos da interface [CrudRepository], sem adicionar outros;
A interface [CreneauRepository] é a seguinte:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Creneau;
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
// lista dos horários de atendimento de um médico
@Query("select c from Creneau c where c.medecin.id=?1")
Iterable<Creneau> getAllCreneaux(long idMedecin);
}
- linha 8: a interface [CreneauRepository] herda os métodos da interface [CrudRepository];
- linhas 10-11: o método [getAllCreneaux] permite obter os horários disponíveis de um médico;
- linha 11: o parâmetro é o identificador do médico. O resultado é uma lista de horários disponíveis na forma de um objeto [Iterable<Creneau>];
- linha 10: a anotação [@Query] permite especificar a consulta JPQL (Java Persistence Query Language) que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método;
A interface [RvRepository] é a seguinte:
package rdvmedecins.repositories;
import java.util.Date;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Rv;
public interface RvRepository extends CrudRepository<Rv, Long> {
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
- linha 10: a interface [RvRepository] herda os métodos da interface [CrudRepository];
- linhas 12-13: o método [getRvMedecinJour] permite obter as consultas de um médico para um determinado dia;
- linha 13: os parâmetros são o identificador do médico e o dia. O resultado é uma lista de consultas na forma de um objeto [Iterable<Rv>];
- linha 12: a anotação [@Query] permite especificar a consulta JPQL que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método e o parâmetro [?2] será substituído pelo parâmetro [jour] do método. Não basta utilizar a seguinte consulta JPQL:
uma vez que os campos da classe Rv, dos tipos [Client] e [Creneau], são obtidos no modo [FetchType.LAZY], o que significa que têm de ser solicitados explicitamente para serem obtidos. Isto é feito na consulta JPQL com a sintaxe [left join fetch entité], que solicita que seja efetuada uma junção com a tabela para a qual aponta a chave estrangeira, a fim de recuperar a entidade referenciada;
8.4.6. A camada [métier]
![]() |
![]() |
- [IMetier] é a interface da camada [métier] e [Metier] é a sua implementação;
- [AgendaMedecinJour] e [CreneauMedecinJour] são duas entidades de negócio;
8.4.6.1. As entidades
A entidade [CreneauMedecinJour] associa um intervalo horário e o eventual compromisso marcado nesse intervalo:
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campos
private Creneau creneau;
private Rv rv;
// construtores
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau, rv);
}
// getters e setters
...
}
- linha 12: o intervalo horário;
- linha 13: a eventual consulta – null caso contrário;
A entidade [AgendaMedecinJour] é a agenda de um médico para um determinado dia, ou seja, a lista das suas consultas:
package rdvmedecins.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.entities.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// campos
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// construtores
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters e setters
...
}
- linha 13: o médico;
- linha 14: o dia na agenda;
- Linha 15: os seus horários de atendimento, com ou sem marcação;
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, num determinado dia
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// encontrar um cliente identificado pelo seu ID
public Client getClientById(long id);
// encontrar um cliente identificado pelo seu ID
public Medecin getMedecinById(long id);
// encontrar uma consulta identificada pelo seu ID
public Rv getRvById(long id);
// encontrar um intervalo horário identificado pelo seu ID
public Creneau getCreneauById(long id);
// adicionar um RV
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// eliminar um RV
public void supprimerRv(Rv rv);
// função
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
Os comentários explicam a função de cada um dos métodos.
A implementação da interface [IMetier] é a seguinte classe [Metier]:
package rdvmedecins.metier;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("métier")
public class Metier implements IMetier {
// repositórios
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// implementação da interface
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
return rvRepository.save(new Rv(jour, client, créneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
}
- linha 24: a anotação [@Service] é uma anotação do Spring que torna a classe anotada um componente gerido pelo Spring. É possível atribuir ou não um nome a um componente. Este é denominado [métier];
- linha 25: a classe [Metier] implementa a interface [IMetier];
- linha 28: a anotação [@Autowired] é uma anotação do Spring. O valor do campo assim anotado será inicializado (injetado) pelo Spring com a referência de um componente do Spring do tipo ou com o nome especificados. Neste caso, a anotação [@Autowired] não especifica nenhum nome. Por conseguinte, será efetuada uma injeção por tipo;
- linha 29: o campo [medecinRepository] será inicializado com a referência a um componente Spring do tipo [MedecinRepository]. Trata-se da referência à classe gerada pelo Spring Data para implementar a interface [MedecinRepository] que já apresentámos;
- linhas 30-35: este processo é repetido para as outras três interfaces analisadas;
- linhas 39-41: implementação do método [getAllClients];
- linha 40: utilizamos o método [findAll] da interface [ClientRepository]. Este método devolve um tipo [Iterable<Client>], que transformamos em [List<Client>] com o método estático [Lists.newArrayList]. A classe [Lists] está definida na biblioteca Google Guava. Em [pom.xml], esta dependência foi importada:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- linhas 38-86: os métodos da interface [IMetier] são implementados com a ajuda das classes da camada [DAO];
Apenas o método da linha 88 é específico da camada [métier]. Foi colocado aqui porque realiza um processamento de negócio que não se resume a um simples acesso aos dados. Sem este método, não haveria motivo para criar uma camada [métier]. O método [getAgendaMedecinJour] é o seguinte:
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
// lista de horários disponíveis do médico
List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
// lista de marcações desse mesmo médico para esse mesmo dia
List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
// cria-se um dicionário a partir das consultas marcadas
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// cria-se a agenda para o dia solicitado
AgendaMedecinJour agenda = new AgendaMedecinJour();
// o médico
agenda.setMedecin(getMedecinById(idMedecin));
// o dia
agenda.setJour(jour);
// os intervalos de reserva
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// preenchimento dos intervalos de marcação
for (int i = 0; i < creneauxHoraires.size(); i++) {
// linha i da agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// intervalo horário
Creneau créneau = creneauxHoraires.get(i);
long idCreneau = créneau.getId();
creneauxMedecinJour[i].setCreneau(créneau);
// o intervalo está livre ou reservado?
if (hReservations.containsKey(idCreneau)) {
// o intervalo está ocupado — regista-se a reserva
Rv resa = hReservations.get(idCreneau);
creneauxMedecinJour[i].setRv(resa);
}
}
// retornamos o resultado
return agenda;
}
Convidamos o leitor a ler os comentários. O algoritmo é o seguinte:
- recuperam-se todos os horários disponíveis do médico indicado;
- recuperam-se todas as suas consultas para o dia indicado;
- com estas duas informações, é possível determinar se um intervalo horário está livre ou ocupado;
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("");
// ligações inicialmente abertas
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();
}
// Gestor 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 Spring;
- linha 19: adiciona ao contexto do Spring todas as classes do pacote [rdvmedecins] e das suas subclasses que possuam uma anotação do Spring. No pacote [rdvmdecins.metier], a classe [Metier], com a sua anotação [@Service], será encontrada e adicionada ao contexto Spring;
- linhas 26-39: configuram o pool de ligações do Tomcat JDBC (linha 5);
- linha 36: o pool de ligações terá, por predefinição, 5 ligações abertas. Esta linha é apresentada a título de exemplo. No nosso caso, bastaria uma ligação. Caso a camada [DAO] fosse utilizada por vários threads, esta linha seria necessária. Será esse o caso mais tarde, quando a camada [DAO] servir de suporte a uma aplicação web que, por natureza, suporta vários utilizadores atendidos em simultâneo;
- linhas 42-49: a implementação JPA utilizada é uma implementação do Hibernate;
- linha 45: não há registos 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 deste objeto, obtém-se o objeto [EntityManager], que permite realizar as operações JPA;
- linha 57: indicam-se o(s) pacote(s) onde se encontram as entidades JPA;
- linha 58: indica a fonte de dados a ligar à camada JPA;
- linhas 64-69: o gestor de transações associado ao EntityManagerFactory anterior. Por predefinição, os métodos das interfaces [CrudRepository] do Spring Data decorrem no interior de uma transação. A transação é iniciada antes da entrada no método e é concluída (através 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(){
// visualização de clientes
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
// visualização de médicos
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
// Visualização dos horários de um médico
Medecin médecin = medecins.get(0);
List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
// Lista de consultas de um médico, num determinado dia
Date jour = new Date();
display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// adicionar um RV
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// verificação
Rv rv2 = métier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// adicionar um RV no mesmo horário do mesmo dia
// deve provocar uma exceção
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
Boolean erreur = false;
try {
rv = métier.ajouterRv(jour, créneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// regista-se o erro
erreur = true;
}
// verifica-se se ocorreu um erro
Assert.assertTrue(erreur);
// lista de RV
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// exibição da agenda
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// eliminar um RV
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// verificação
rv2 = métier.getRvById(rv.getId());
Assert.assertNull(rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
}
// método utilitário - apresenta os elementos de uma coleção
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- linha 22: a anotação [@SpringApplicationConfiguration] permite utilizar o ficheiro de configuração [DomainAndPersistenceConfig] analisado anteriormente. A classe de teste beneficia assim de todos os beans definidos por este ficheiro;
- linha 23: a anotação [@RunWith] permite a integração do Spring com o JUnit: a classe poderá ser executada como um teste JUnit. [@RunWith] é uma anotação JUnit (linha 9), enquanto a classe [SpringJUnit4ClassRunner] é uma classe Spring (linha 12);
- linhas 26-27: injeção na classe de teste de uma referência à camada [métier];
- muitos testes são apenas testes visuais simples:
- linhas 32-33: lista de clientes;
- linhas 35-36: lista de médicos;
- linhas 39-40: lista dos horários disponíveis de um médico;
- linha 43: lista das consultas de um médico;
- linha 50: adição de uma nova consulta. O método [ajouterRv] devolve a consulta com uma informação adicional, a sua chave primária id;
- linha 53: utiliza-se esta chave primária para procurar o compromisso na base de dados;
- linha 54: verifica-se se o compromisso procurado e o compromisso encontrado são os mesmos. Recorde-se que o método [equals] da entidade [Rv] foi redefinido: dois compromissos são iguais se tiverem o mesmo ID. Neste caso, isto mostra-nos que o compromisso adicionado foi efetivamente inserido na base de dados;
- linhas 61-73: tenta-se adicionar pela segunda vez o mesmo compromisso. Isto deve ser rejeitado pelo SGBD, uma vez que existe uma restrição de unicidade:
CREATE TABLE IF NOT EXISTS `rv` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`JOUR` date NOT NULL,
`ID_CLIENT` bigint(20) NOT NULL,
`ID_CRENEAU` bigint(20) NOT NULL,
`VERSION` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`),
UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;
A linha 8 acima indica que a combinação [JOUR, ID_CRENEAU] deve ser única, o que impede a inserção de dois compromissos no mesmo dia e no mesmo intervalo horário.
- linha 73: verifica-se se ocorreu efetivamente uma exceção;
- linha 77: solicita-se a agenda do médico para quem acabou de ser adicionado um compromisso;
- linha 79: verifica-se se a consulta adicionada consta efetivamente da agenda;
- linha 82: elimina-se a consulta adicionada;
- linha 84: procura-se na base de dados a consulta eliminada;
- linha 85: verifica-se se foi recuperado um ponteiro null, o que indica que a consulta procurada não existe;
A execução do teste foi bem-sucedida:
![]() |
8.4.9. O programa de consola
![]() |
O programa de consola é básico. Ilustra como recuperar uma chave externa:
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
public class Boot {
// arranque
public static void main(String[] args) {
// prepara-se a configuração
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// iniciamos a configuração
ConfigurableApplicationContext context = app.run(args);
// função
IMetier métier = context.getBean(IMetier.class);
try {
// adicionar um RV
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// verificação
créneau = métier.getCreneauById(1L);
long idMedecin = créneau.getIdMedecin();
display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
} catch (Exception ex) {
System.out.println("Exception : " + ex.getCause());
}
// encerramento do contexto Spring
context.close();
}
// método utilitário - apresenta os elementos de uma coleção
private static <T> void display(String message, Iterable<T> elements) {
System.out.println(message);
for (T element : elements) {
System.out.println(element);
}
}
}
O programa adiciona um compromisso e, em seguida, verifica se este foi adicionado.
- linha 19: a classe [SpringApplication] irá utilizar a classe de configuração [DomainAndPersistenceConfig];
- linha 20: supressão dos registos de arranque da aplicação;
- linha 22: a classe [SpringApplication] é executada. Ela devolve um contexto Spring, ou seja, a lista de beans registados;
- linha 24: obtém-se uma referência ao bean que implementa a interface [IMetier]. Trata-se, portanto, de uma referência à camada [métier];
- linhas 27-31: adição de um novo compromisso para hoje, para o cliente n.º 1 no horário n.º 1. O cliente e o horário foram criados de raiz para demonstrar que apenas os identificadores são utilizados. Inicializou-se aqui a versão, mas poderia ter-se colocado qualquer valor. Ela não é utilizada aqui;
- linha 34: queremos saber qual é o médico associado ao intervalo n.º 1. Para isso, precisamos de consultar a base de dados para obter o intervalo n.º 1. Como estamos no modo [FetchType.LAZY], o médico não é apresentado juntamente com o intervalo. No entanto, tivemos o cuidado de prever um campo [idMedecin] na entidade [Creneau] para recuperar a chave primária do médico;
- linha 35: recuperamos a chave primária do médico;
- linha 36: apresenta-se a lista de consultas do médico;
Os resultados na consola são os seguintes:
8.4.10. Gestão de registos
Os registos da consola são configurados por dois ficheiros: [application.properties] e [logback.xml] [1]:
![]() |
O ficheiro [application.properties] é utilizado pelo framework Spring Boot. É possível definir nele inúmeros parâmetros para alterar os valores predefinidos do Spring Boot (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Aqui, o seu conteúdo é o seguinte:
logging.level.org.hibernate=OFF
spring.main.show-banner=false
- linha 1: controla o nível de registos do Hibernate — neste caso, não há registos
- linha 2: controla a exibição do banner do Spring Boot — neste caso, não há banner
O ficheiro [logback.xml] é o ficheiro de configuração do framework de registos [logback] [2]:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- por predefinição, aos codificadores é atribuído o tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- controlo do nível dos registos -->
<root level="info"> <!-- desativado, informação, depuração, aviso -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- o nível geral de registos é controlado pela linha 9 — aqui, registos de nível [info];
Isto dá o seguinte resultado:
Se alterarmos o nível de registos do Hibernate para [info] (sem alterar mais nada):
logging.level.org.hibernate=INFO
spring.main.show-banner=false
obtém-se o seguinte resultado:
Se alterarmos o nível de registo para [debug] (sem alterar mais nada):
logging.level.org.hibernate=DEBUG
spring.main.show-banner=false
obtém-se o seguinte resultado:
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'clientRepository' to allow for resolving potential circular references
10:35:13.522 [main] DEBUG o.s.b.f.annotation.InjectionMetadata - Processing injected element of bean 'clientRepository': PersistenceElement for public void org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.setEntityManager(javax.persistence.EntityManager)
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'entityManagerFactory'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'jpaMappingContext'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name 'clientRepository'
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$ThreadBoundTargetSource@723ed581
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [org.springframework.data.jpa.repository.support.SimpleJpaRepository@796065aa]
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'clientRepository'
10:35:13.522 [main] DEBUG o.s.b.f.a.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'métier' to bean named 'clientRepository'
...
8.4.11. A camada [web / jSON]
![]() |
![]() |
Vamos construir a camada [web / jSON] em várias etapas:
- etapa 1: uma camada web operacional sem autenticação;
- etapa 2: implementação da autenticação com o Spring Security;
- 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 predefinição, não poderá, portanto, aceder ao serviço, a menos que este o autorize. Veremos como;
8.4.11.1. Configuração do Maven
O ficheiro [pom.xml] do projeto é o seguinte:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webjson-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- 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: as dependências do projeto relativas à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 limitado de URL com uma sintaxe específica;
- em [4], recebe uma resposta jSON;
As respostas do nosso serviço web terão todas o mesmo formato, correspondente à transformação jSON de um objeto do tipo [Response], como se segue:
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- propriedades
// estado 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, caso contrário: KO;
- linha 11: uma lista de mensagens de erro, caso haja algum erro;
- linha 13: o corpo da resposta;
Apresentamos agora as capturas de ecrã que ilustram a interface do serviço web / jSON:
Lista de todos os pacientes do consultório médico [/getAllClients]
![]() |
Lista de todos os médicos do consultório médico [/getAllMedecins]
![]() |
Lista dos horários disponíveis de um médico [/getAllCreneaux/{idMedecin}]
![]() |
Lista das consultas de um médico [/getRvMedecinJour/{idMedecin}/{aaaa-mm-jj}
![]() |
Agenda de um médico [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]
![]() |
Para adicionar/eliminar uma consulta, utilizamos a extensão do Chrome [Advanced Rest Client], uma vez que estas operações são realizadas com um POST.
Adicionar uma consulta [/ajouterRv]
![]() |
- no [0], o URL do serviço web;
- em [1], é utilizado o método POST;
- em [2], o texto jSON das informações transmitidas ao serviço web na forma {dia, idClient, idCreneau};
- em [3], o cliente indica ao serviço web que lhe está a enviar informações no formato jSON;
A resposta é então a seguinte:
![]() |
- em [4]: o cliente envia o cabeçalho indicando que os dados que está a enviar estão no formato jSON;
- em [5]: o serviço web responde que também envia jSON;
- em [6]: a resposta jSON do serviço web. O campo [body] contém a forma jSON do compromisso adicionado;
É possível verificar a existência do novo compromisso:
![]() |
Registe-se o ID [50] do compromisso. Vamos eliminá-lo.
Eliminar um compromisso [/supprimerRv]
![]() |
- em [1], o URL do serviço web;
- em [2], é utilizado o método POST;
- em [3], o texto jSON das informações transmitidas ao serviço web na forma {idRv};
- em [4], o cliente indica ao serviço web que lhe está a enviar informações jSON;
A resposta é então a seguinte:
![]() |
- em [5]: o campo [status] está a 0, indicando assim que a operação foi bem-sucedida;
É possível verificar a eliminação do compromisso:
![]() |
Acima, a consulta do doente [Mme GERMAIN] já não está presente.
O serviço web também permite recuperar entidades através do seu identificador:
![]() |
![]() |
![]() |
![]() |
Todas estas entidades URL são processadas pelo controlador [RdvMedecinsController], que iremos apresentar 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 Spring sejam procurados no pacote [rdvmedecins.web] e nos seus descendentes. É assim que serão encontrados os componentes:
- [@RestController RdvMedecinsController] no pacote [rdvmedecins.web.controllers];
- [@Component ApplicationModel] no pacote [rdvmedecins.web.models];
- linha 11: importa-se a classe [DomainAndPersistenceConfig], que configura o projeto [rdvmedecins-metier-dao] para permitir o acesso aos beans desse projeto;
- 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. Desempenha a função de [FrontController]: intercepta os pedidos dirigidos ao site Spring MVC e encaminha-os para serem processados por um dos controladores (Controller) do site;
- linha 22: instanciação da classe;
- linha 23: esta linha pode ser ignorada por enquanto;
- linhas 27-30: o servlet [dispatcherServlet] processa todas as URL;
- linhas 27-30: ativam o servidor Tomcat incorporado nas dependências do projeto. Este 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/deserializa um objeto [Creneau], ignorando o campo [Creneau.medecin];
- linhas 51-59: o mapeador jSON [jsonMapperLongRv] serializa/deserializa um objeto [Rv], ignorando o campo [Rv.creneau.medecin];
- linhas 61-67: o mapeador jSON [jsonMapperShortRv] serializa / 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 de 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] num componente Spring. Tal como todos os componentes Spring vistos até agora (com exceção de @Controller), será instanciado apenas um objeto deste tipo (singleton);
- linha 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 imediatamente após a instanciação da classe [ApplicationModel];
- linhas 38-39: recuperam-se as listas de médicos e de clientes a partir 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) comunicam com o singleton [ApplicationModel];
Esta estratégia proporciona flexibilidade na gestão da cache. Atualmente, os horários dos médicos não são armazenados na cache. Para os incluir, basta alterar a classe [ApplicationModel]. Isto não tem qualquer impacto no controlador, que continuará a utilizar o método [List<Creneau> getAllCreneaux(long idMedecin)] tal como fazia anteriormente. É a implementação deste método na classe [ApplicationModel] que será alterada.
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 qualquer caráter «de negócio» ou «web»:
![]() |
O 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() {
// 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);
}
}
O método cria um objeto [List<String>] com as mensagens de erro [exception.getMessage()] de uma exceção [exception] e das mensagens que esta contém, [exception.getCause()].
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 neste 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 da aplicação
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 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 das 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] num controlador Spring, o C do MVC;
- linhas 38-39: um objeto do tipo [ApplicationModel] será injetado aqui pelo Spring. Já o apresentámos;
- linhas 41-42: um objeto do tipo [RdvMedecinsCorsController] será injetado aqui pelo Spring. Só apresentaremos este objeto mais tarde;
- linhas 48-58: os mapeadores jSON definidos na classe de configuração [WebConfig];
- linha 60: a anotação [@PostConstruct] marca um método a ser executado 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]. Este objeto foi instanciado no arranque da aplicação e tentou armazenar em cache os médicos e os clientes. Se essa tentativa falhar, obtém-se o [messages!=null]. Isto 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 devolvem a cadeia jSON de um objeto do tipo [Response<T>], como se segue:
![]() |
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- propriedades
// estado 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;
Este objeto é serializado para jSON antes de ser enviado para o navegador do cliente;
- linha 67: o URL exposto é [/getAllMedecins]. O cliente deve utilizar um método [GET] para efetuar o seu pedido (method = RequestMethod.GET). Se este URL fosse solicitado por um POST, seria recusado e o Spring MVC enviaria um código de erro HTTP ao cliente web. O próprio método devolve a resposta ao cliente (linha 68). Trata-se de uma cadeia de caracteres (linha 67). O cabeçalho HTTP [Content-type : application/json; charset=UTF-8] será enviado ao cliente para lhe indicar que irá receber uma cadeia jSON (linha 67);
- linha 77: o URL é definido pelo {idMedecin}. Este parâmetro é recuperado com a anotação [@PathVariable] na linha 79;
- linha 79: o parâmetro [long idMedecin] recebe o seu valor do parâmetro {idMedecin} do URL [@PathVariable("idMedecin")]. O parâmetro no URL e o do método podem ter nomes diferentes. É importante referir aqui que o [@PathVariable("idMedecin")] é do tipo String (todo o URL é um String), enquanto o parâmetro [long idMedecin] é do tipo [long]. A alteração de tipo é efetuada automaticamente. É devolvido um código de erro HTTP se essa alteração de tipo falhar;
- linha 105: a anotação [@RequestBody] designa o corpo da consulta. Numa solicitação GET, quase nunca há corpo (mas é possível incluir um). Numa solicitação POST, na maioria das vezes há corpo (mas é possível não incluir nenhum). No caso do URL [ajouterRv], o cliente web envia no seu POST a seguinte cadeia jSON:
A sintaxe [@RequestBody PostAjouterRv post] (linha 105) , juntamente com o facto de o método esperar o jSON [consumes = "application/json; charset=UTF-8"] na linha 103, fará com que a cadeia jSON enviada pelo cliente web seja deserializada num objeto do tipo [PostAjouterRv]. Este é o seguinte:
package rdvmedecins.web.models;
public class PostAjouterRv {
// dados da publicação
private String jour;
private long idClient;
private long idCreneau;
// getters e setters
...
}
Também aqui as alterações de tipo necessárias ocorrerão automaticamente;
- nas linhas 107-109, encontramos um mecanismo semelhante para o URL e o [/supprimerRv]. A cadeia jSON enviada é a seguinte:
e o tipo [PostSupprimerRv] é o seguinte:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// dados da publicação
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 da aplicação
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 a aplicação foi inicializada corretamente (messages==null). Se não for esse o caso, é devolvida 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], porque a classe [Medecin]possui um 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 limita-se a devolver uma lista que se encontra em cache. No entanto, manteremos esta gestão de exceções para o caso de os médicos já não serem armazenados em cache;
Ainda não ilustrámos o caso em que a aplicação não foi inicializada corretamente. Vamos parar o SGBD e o MySQL5, iniciar o serviço web e, em seguida, solicitar o URL e o [/getAllMedecins]:

Recebemos, de facto, um erro. Num contexto normal, 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;
// estado da aplicação
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);
}
É análoga ao método [getAllMedecins] já analisado. 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;
// estado da aplicação
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// recuperar o 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] é solicitado a um método local:
private Response<Medecin> getMedecin(long id) {
// recuperação do 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);
}
Retorna-se deste 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: recuperam-se os horários disponíveis desse médico;
- linha 25: envia-se como resposta um objeto [List<Creneau>]. Recorde-se a definição da classe [Creneau]:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// características de um horário de RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// um horário está associado a um médico
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// chave estrangeira
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- linha 13: o médico é procurado no modo [FetchType.LAZY];
Recordemos a consulta JPQL que implementa o método [getAllCreneaux] na camada [DAO]:
@Query("select c from Creneau c where c.medecin.id=?1")
A notação [c.medecin.id] força a junção entre as tabelas [CRENEAUX] e [MEDECINS]. Assim, a consulta devolve todos os horários do médico, indicando o nome do médico em cada um deles. Quando se serializam 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:
- ter acesso ao objeto que está a ser serializado;
- configurar o objeto a serializar;
O ponto 1 é verificado com a injeção do conversor jSON adequado ao 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. Esta cria um filtro denominado [creneauFilter]. Com este filtro, poderemos definir programaticamente os campos que devem ou não ser serializados;
A serialização do objeto [Creneau] é efetuada na linha seguinte do método [getAllCreneaux]:
// resposta
return jsonMapperShortCreneau.writeValueAsString(response);
O mapeador jSON [jsonMapperShortCreneau] foi definido na classe [WebConfig] da seguinte forma:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
- linha 5: o filtro denominado [creneauFilter] está associado ao filtro [creneauFilter] da linha 4. Este filtro serializa o objeto [Creneau] sem o seu campo [medecin];
O resultado devolvido pelo método [getAllCreneaux] é a cadeia jSON de tipo [Response<List<Creneau>].
Os resultados obtidos são os seguintes:
![]() |
ou estes, caso o intervalo não exista:
![]() |
Deste exemplo, retemos a seguinte regra:
- os métodos do servidor web / jSON devolvem um objeto do tipo [Response<T>], que é serializado em jSON;
- se o tipo T tiver um ou mais filtros jSON, para o serializar 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;
// estado 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) {
// recuperar 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 das 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 este campo for serializado, será encontrado o filtro jSON [creneauFilter];
- linha 47: o objeto do tipo [Response<List<Rv>>] da linha 7 é serializado em jSON;
Analisemos o caso em que a lista de compromissos foi obtida na linha 42. A classe [Rv] no projeto [rdvmedecins-metier-dao] está 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á associada a um cliente
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// uma consulta está associada a um intervalo de tempo
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// chaves externas
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
}
- linha 11: o cliente é pesquisado com o modo [FetchType.LAZY];
- linha 18: o intervalo de tempo é pesquisado com o modo [FetchType.LAZY];
Recordemos a consulta JPQL, que procura os compromissos:
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
São efetuadas junções explicitamente para recuperar os campos [client] e [creneau]. Além disso, devido à junção [cr.medecin.id=?1], teremos também o médico. O médico irá, portanto, aparecer na cadeia jSON de cada consulta. No entanto, esta informação duplicada é, além disso, desnecessária. Vimos como resolver este problema utilizando um filtro jSON no objeto [Creneau]. Devido aos modos [FetchType.LAZY] dos campos [client] e [creneau] da classe [Rv], em breve iremos constatar 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 {
...
Iremos verificar a serialização do objeto [Rv] com o filtro [rvFilter]. Aparentemente, neste caso, não precisamos de filtrar, pois necessitamos de todos os campos do objeto do tipo [Rv]. No entanto, como indicámos que a classe tinha um filtro jSON, temos de definir esse filtro para qualquer serialização de um objeto do tipo [Rv]; caso contrário, ocorrerá uma exceção. Para tal, utilizamos o mapeador jSON a seguir, definido na classe [rdvMedecinsController]:
@Autowired
private ObjectMapper jsonMapperLongRv;
Este mapeador está 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 marcação:
![]() |
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;
// estado da aplicação
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// verifica-se a data
Date jourAgenda = null;
if (!erreur) {
// verifica-se 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);
}
}
// a recuperação do médico
Medecin médecin = null;
if (!erreur) {
// recuperar 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();
}
}
// recuperar 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 num tipo [AgendaMedecinJour] encapsulado num 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] têm filtros jSON que têm de 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, a 28/01/2015, o Dr. PELISSIER tem uma consulta com a Sra. Brigitte BISTROU às 8h20;
ou estes, caso a data esteja errada:
![]() |
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;
// estado da aplicação
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 num tipo [Medecin]. Este tipo não possui qualquer anotação de filtro jSON. Assim, 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) {
// procura-se 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 seguinte 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 da aplicação
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 num tipo [Client]. Este tipo não possui qualquer anotação de filtros jSON. Assim, 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) {
// a recuperar 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;
// estado da aplicação
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// devolve-se o horário
response = getCreneau(id);
}
// resposta
return jsonMapperShortCreneau.writeValueAsString(response);
}
- linhas 5, 14: o método transforma a cadeia jSON num tipo [Response<Creneau>];
Na linha 8, o método [getCreneau] é o seguinte:
private Response<Creneau> getCreneau(long id) {
// recuperamos o intervalo
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (RuntimeException e1) {
return new Response<Creneau>(1, Static.getErreursForException(e1), null);
}
// intervalo 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);
}
Recorde-se 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á associado a um médico
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// chave estrangeira
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
- linhas 14-16: uma vez que o campo [medecin] está no modo [fetch = FetchType.LAZY], não é recuperado quando se procura um intervalo de tempo através do seu [id]. Por isso, é necessário excluí-lo da serialização. Sem essa exclusão, ocorre uma exceção. Esta deve-se ao facto de o objeto de serialização [mapper] ir 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] devolveu um objeto [Creneau] cujo método [getMedecin] está programado para ir buscar o médico no contexto JPA. A isto chama-se um objeto [proxy]. Ora, recordemos a arquitetura da aplicação web:
![]() |
O controlador encontra-se no bloco [Contrôleurs / Actions]. Quando se está neste bloco, já não existe a noção de contexto JPA. Este último é criado durante as operações da camada [DAO]. Não subsiste para além disso. Assim, quando o controlador tenta aceder ao contexto JPA, ocorre uma exceção a indicar que este está fechado. Para evitar esta 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;
// estado da aplicação
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// obtém-se a consulta
response = getRv(id);
}
// resposta
return jsonMapperShortRv.writeValueAsString(response);
}
- linhas 5, 14: o método devolve a cadeia jSON de um tipo [Response<Rv>];
Na linha 11, o método [getRv] é o seguinte:
private Response<Rv> getRv(long id) {
// recuperação do Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (RuntimeException e1) {
return new Response<Rv>(1, Static.getErreursForException(e1), null);
}
// Rv existente?
if (rv == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le rendez-vous d'id [%s] n'existe pas", id));
return new Response<Rv>(2, messages, null);
}
// ok
return new Response<Rv>(0, null, rv);
}
A classe [Rv] tem dois campos com a anotação [fetch = FetchType.LAZY]: os campos [creneau] e [client]. Por conseguinte, estes campos não são recuperados quando se procura um [Rv] através da sua chave primária. Por isso, pelas mesmas razões que 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 da consulta 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;
}
// recuperam-se os valores lançados
String jour;
long idCreneau = -1;
long idClient = -1;
Date jourAgenda = null;
if (!erreur) {
// recuperação dos 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 intervalo
Response<Creneau> responseCréneau = null;
if (!erreur) {
// recupera-se o intervalo
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 a marcação
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 devolver a cadeia jSON de tipo [Response<Rv>];
- linha 3: a anotação [@RequestBody PostAjouterRv post] recupera o corpo do POST e coloca-o no parâmetro [PostAjouterRv post]. Este corpo provém do jSON [consumes = "application/json; charset=UTF-8"], que será deserializado automaticamente no tipo [PostAjouterRv] seguinte:
public class PostAjouterRv {
// dados da publicação
private String jour;
private long idClient;
private long idCreneau;
...
- depois, há código que já foi encontrado de uma forma ou de outra;
- linha 67: a configuração dos filtros jSON, [creneauFilter] e [rvFilter]. O método converte a cadeia jSON num 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] tem uma dependência [FetchType.LAZY] de um objeto [Medecin] e foi obtido nas linhas 36-44. Foi pesquisado no contexto JPA através da sua chave primária e foi obtido sem a sua dependência [FetchType.LAZY]. Por fim,
- o objeto [Rv] possui todas as suas dependências. Estas podem ser serializadas;
- o objeto [Creneau] não possui a sua dependência [medecin]. Por conseguinte, esta não deve ser serializada;
O mapeador jSON [jsonMapperLongRv] definido na classe [WebConfig] cumpre estas restrições:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
Os resultados obtidos são semelhantes aos seguintes com o cliente [Advanced Rest Client]:
![]() |
- em [1], o URL do POST;
- em [2], o POST;
- em [3], o valor publicado;
- em [4a], este valor lançado provém do jSON;
![]() |
- em [4b], o cliente indica que está a enviar jSON;
- em [5], o servidor indica que está a devolver jSON;
![]() |
- em [6], a resposta jSON do servidor, que representa o compromisso adicionado. Nela vê-se o identificador [id] do compromisso adicionado;
Obtém-se o seguinte com um número de intervalo 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) {
// eliminaçã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 devolve 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] seguinte:
public class PostSupprimerRv {
// dados da publicação
private long idRv;
- linha 28: quando a eliminação for bem-sucedida, é enviada uma resposta com [status=0];
Os resultados obtidos são os seguintes:
![]() |
![]() |
- em [5], o campo [status=0] indica que a eliminação foi bem-sucedida;
Com um número de marcação que não existe, obtém-se o seguinte:
![]() |
Já terminámos com o controlador. Vamos agora 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 com, como primeiro parâmetro, a classe [AppConfig] de configuração do projeto. Este método irá proceder à autoconfiguração do projeto, iniciar o servidor Tomcat incorporado nas dependências e implementar nele o controlador [RdvMedecinsController].
Os registos são controlados pelos seguintes ficheiros [2]:
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- por predefinição, aos codificadores é atribuído o tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- controlo do nível dos registos -->
<root level="info"> <!-- desligado, informação, depuração, aviso -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- linha 9: o nível geral de registos é 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 registo específico para determinados elementos da aplicação:
- linha 1: pretendemos os registos da camada [web];
- linha 2: não queremos os registos da camada [JPA];
- linha 3: sem banner do Spring Boot;
Os registos durante a execução são os seguintes:
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,342 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:06:04,357 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:06:04,404 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:06:04,420 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point
11:06:04.732 [main] INFO rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 420 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:06:04.775 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:05.538 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:06:05.688 [main] INFO o.a.catalina.core.StandardService - Starting service Tomcat
11:06:05.689 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:06:05.833 [localhost-startStop-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:06:05.833 [localhost-startStop-1] INFO o.s.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1061 ms
11:06:06.231 [localhost-startStop-1] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:06:09.234 [localhost-startStop-1] INFO o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@12d14fa, org.springframework.security.web.context.SecurityContextPersistenceFilter@29823fb6, org.springframework.security.web.header.HeaderWriterFilter@662d93b2, org.springframework.security.web.authentication.logout.LogoutFilter@2d81ee0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52aa47ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@60bd7a74, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a374232, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ddb4452, org.springframework.security.web.session.SessionManagementFilter@2cd9855f, org.springframework.security.web.access.ExceptionTranslationFilter@2263f0a2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@192ce7f6]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Medecin> rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Client> rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Client>> rdvmedecins.web.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Medecin>> rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
11:06:09.677 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:09.770 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:06:09.786 [main] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:06:09.802 [main] INFO o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:06:09.817 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:06:09.817 [main] INFO rdvmedecins.web.boot.Boot - Started Boot in 5.319 seconds (JVM running for 6.053)
- linha 18: o servidor Tomcat está ativo;
- linha 21: o contexto Spring está a ser inicializado;
- linhas 27-38: os URL expostos pelo serviço web são detetados;
- linha 44: o servidor Tomcat está pronto e aguarda pedidos na porta 8080;
Se alterarmos o ficheiro [application.properties] da seguinte forma:
logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false
obtêm-se os seguintes registos:
Além disso, se alterarmos o ficheiro [logback.xml] da seguinte forma:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- por predefinição, é atribuído aos codificadores o tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- controlo do nível dos registos -->
<root level="off"> <!-- desativado, informação, depuração, aviso -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
obtêm-se os seguintes registos:
Vemos, portanto, que temos algum controlo sobre os registos que aparecem na consola. O nível [info] é frequentemente o nível adequado para os registos.
Temos agora um serviço web operacional que pode ser consultado através de um cliente web. Passamos agora à segurança deste serviço: queremos que apenas determinadas pessoas possam gerir as consultas dos médicos. Para tal, vamos utilizar o framework Spring Security, um ramo do ecossistema Spring.
8.4.12. Introdução ao Spring Security
Vamos importar novamente um guia do Spring, seguindo os passos 1 a 3 abaixo:
![]() |
![]() |
O projeto é composto pelos seguintes elementos:
- na pasta [templates], 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 analisar o seu ficheiro [pom.xml] para conhecer as suas dependências:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-securing-web</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- tag::security[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- end::security[] -->
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- linhas 10-14: o projeto é um projeto Spring Boot;
- linhas 17-20: dependência do framework [Thymeleaf];
- linhas 22-25: dependência do framework Spring Security;
8.4.12.2. As vistas Thymeleaf
![]() |
A vista [home.html] é a seguinte:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- linha 12: o atributo [th:href="@{/hello}"] irá gerar o atributo [href] da baliza <a>. O valor [@{/hello}] irá gerar o caminho [<context>/hello], em que [context] é o contexto da aplicação web;
O código HTML gerado é o seguinte:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click
<a href="/hello">here</a>
to see a greeting.
</p>
</body>
</html>
A vista [hello.html] é a seguinte:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- linha 9: O atributo [th:inline="text"] irá gerar o texto da baliza <h1>. Este texto contém uma expressão $ que deve ser avaliada. O elemento [[${#httpServletRequest.remoteUser}]] é o valor do atributo [RemoteUser] da consulta HTTP atual. Trata-se do nome do utilizador que está ligado;
- linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da baliza [form]. O valor [@{/logout}] irá gerar o caminho [<context>/logout], em que [context] é o contexto da aplicação web;
O código HTML gerado é o seguinte:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello user!</h1>
<form method="post" action="/logout">
<input type="submit" value="Sign Out" />
<input type="hidden" name="_csrf" value="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 denominado (atributo name) _csrf;
A última vista [login.html] é a seguinte:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- linha 9: o atributo [th:if="${param.error}"] faz com que a baliza <div> só seja gerada se o URL, que apresenta a página de início de sessão, contiver o parâmetro [error] (http://context/login?error);
- linha 10: o atributo [th:if="${param.logout}"] faz com que a tag <div> só seja gerada se o URL, que apresenta a página de início de sessão, contiver o parâmetro [logout] (http://context/login?logout);
- linhas 11-23: um formulário HTML;
- linha 11: o formulário será enviado para o URL [<context>/login], em que <context> é o contexto da aplicação web;
- linha 13: um campo de introdução de dados denominado [username];
- linha 17: um campo de introdução de dados denominado [password];
O código HTML gerado é o seguinte:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<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>
Note-se, na linha 28, que o Thymeleaf adicionou um campo oculto denominado [_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] numa classe de configuração;
- linha 8: a classe [MvcConfig] estende a classe [WebMvcConfigurerAdapter] para redefinir alguns dos seus métodos;
- linha 10: redefinição de um método da classe pai;
- linhas 11-16: o método [addViewControllers] permite associar URL a vistas HTML. São feitas as seguintes associações:
URL | vista |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
O sufixo [html] e a pasta [templates] são os valores predefinidos utilizados pelo Thymeleaf. Podem ser alterados através da configuração. A pasta [templates] deve estar na raiz do Classpath do projeto:
![]() |
Acima de [1], as pastas [java] e [resources] são ambas pastas de origem (source folders). Isto significa que o seu conteúdo estará na raiz do Classpath do projeto. Assim, no [2], as pastas [hello] e [templates] estarão na raiz do Classpath.
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] numa classe de configuração;
- linha 10: a anotação [@EnableWebSecurity] torna a classe [WebSecurityConfig] uma classe de configuração do Spring Security;
- linha 11: a classe [WebSecurity] estende a classe [WebSecurityConfigurerAdapter] para redefinir alguns dos seus métodos;
- linha 12: redefinição de um método da classe pai;
- linhas 13-16: o método [configure(HttpSecurity http)] é redefinido para definir os direitos de acesso às diferentes URL da aplicação;
- linha 14: o método [http.authorizeRequests()] permite associar URL a direitos de acesso. São feitas as seguintes associações:
URL | regra | código |
acesso sem autenticação | | |
Acesso apenas com autenticação |
- linha 15: define o método de autenticação. A autenticação é feita através de um formulário URL [/login] acessível a todos [http.formLogin().loginPage("/login").permitAll()]. O logout também está acessível a todos;
- linhas 19-21: redefinem o método [configure(AuthenticationManagerBuilder auth)] que gere os utilizadores;
- linha 20: a autenticação é feita com utilizadores definidos de forma «estática» [auth.inMemoryAuthentication()]. Um utilizador é aqui definido com o nome de utilizador [user], a palavra-passe [password] e a função [USER]. É possível conceder os mesmos direitos a utilizadores com a mesma função;
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 efetue a configuração que o programador não terá feito explicitamente;
- linha 9: transforma a classe [Application] numa classe de configuração do Spring;
- linha 10: solicita a análise da pasta da classe [Application] para procurar componentes Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão assim detetadas, uma vez que possuem a anotação [@Configuration];
- linha 13: o método [main] da classe executável;
- linha 14: o método estático [SpringApplication.run] é executado com a classe de configuração [Application] como parâmetro. Já nos deparámos com este processo e sabemos que o servidor Tomcat incluído nas dependências Maven do projeto será iniciado e que o projeto será implementado nesse servidor. Vimos que quatro instâncias de URL eram geridas por [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.
8.4.12.6. Testes da aplicação
Comecemos por solicitar o URL [/], que é um dos quatro URL aceites. Está associado à vista [/templates/home.html]:
![]() |
A URL solicitada, [/], está acessível a todos. Foi por isso que a obtivemos. O link [here] é o seguinte:
O URL [/hello] será solicitado quando clicarmos no link. Este está protegido:
URL | regra | código |
acesso sem autenticação | | |
Acesso apenas para utilizadores autenticados |
É necessário estar autenticado para o obter. O Spring Security irá então redirecionar o navegador do cliente para a página de autenticação. De acordo com a configuração apresentada, trata-se da página URL [/login]. Esta página está acessível a todos:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Assim, obtemos [1]:
![]() |
O código-fonte da página obtida é o seguinte:
- na linha 7, surge um campo oculto que não consta na página original [login.html]. Foi o Thymeleaf que o adicionou. Este código, denominado CSRF (Cross Site Request Forgery), tem como objetivo eliminar uma falha de segurança. Este token deve ser reenviado ao Spring Security juntamente com a autenticação para que esta seja aceite;
Recordamos que apenas o utilizador «user/password» é reconhecido pelo Spring Security. Se introduzirmos outra coisa em [2], obtemos a mesma página com uma mensagem de erro em [3]. O Spring Security redirecionou o navegador para o URL [http://localhost:8080/login?error]. A presença do parâmetro [error] provocou a exibição da baliza:
<div th:if="${param.error}">Invalid username and password.</div>
Agora, introduzamos os valores esperados para user/password [4]:
![]() |
- em [4], identificamo-nos;
- em [5], o Spring Security redireciona-nos para URL [/hello], pois era URL que estávamos a solicitar quando fomos redirecionados para a página de início de sessão. A identidade do utilizador foi apresentada na seguinte linha de [hello.html]:
A página [5] apresenta o seguinte formulário:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
Ao clicar no botão [Sign Out], será efetuado um POST no URL [/logout]. Este, tal como o URL e o [/login], está acessível a todos:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Na nossa associação URL / vistas, não definimos nada para o URL [/logout]. O que irá acontecer? Vamos experimentar:
![]() |
- em [6], clicamos no botão [Sign Out];
- em [7], vemos que fomos redirecionados para o URL [http://localhost:8080/login?logout]. Foi o Spring Security que solicitou este redirecionamento. A presença do parâmetro [logout] no URL fez com que fosse exibida a seguinte linha na vista:
<div th:if="${param.logout}">You have been logged out.</div>
8.4.12.7. Conclusion
No exemplo anterior, poderíamos ter escrito primeiro a aplicação web e, só depois, implementado a segurança. O Spring Security não é intrusivo. É possível implementar a segurança numa aplicação web já escrita. Além disso, descobrimos os seguintes pontos:
- é possível definir uma página de autenticação;
- a autenticação deve ser acompanhada pelo token CSRF emitido pelo Spring Security;
- se a autenticação falhar, o utilizador é redirecionado para a página de autenticação, com um parâmetro «error» adicional no token URL;
- se a autenticação for bem-sucedida, o utilizador é redirecionado para a página solicitada no momento em que a autenticação ocorreu. Se se aceder diretamente à página de autenticação sem passar por uma página intermédia, o Spring Security redireciona-nos para o URL [/] (este caso não foi apresentado);
- desautentificamo-nos ao aceder à página URL [/logout] com um POST. O Spring Security redireciona-nos então para a página de autenticação com o parâmetro «logout» no URL;
Todas estas conclusões baseiam-se nos comportamentos por predefinição do Spring Security. Estes comportamentos podem ser alterados através da configuração, redefinindo determinados métodos da classe [WebSecurityConfigurerAdapter].
O tutorial anterior será de pouca utilidade daqui em diante. Iremos, de facto, utilizar:
- uma base de dados para armazenar os utilizadores, as suas palavras-passe e as suas funções;
- uma autenticação por cabeçalho HTTP;
Existem poucos tutoriais sobre o que pretendemos fazer aqui. A solução que iremos propor é uma compilação de códigos encontrados aqui e ali.
8.4.13. Implementação da segurança no serviço web de marcação de consultas
8.4.13.1. A base de dados
A base de dados [rdvmedecins] é atualizada para incluir os utilizadores, as suas palavras-passe e as suas funções. Surgem três novas tabelas:

Tabela [USERS]: os utilizadores
- ID: chave primária;
- VERSION: coluna de controlo de versões da linha;
- IDENTITY: uma identidade descritiva do utilizador;
- LOGIN: o nome de utilizador do utilizador;
- PASSWORD: a sua palavra-passe;
Na tabela USERS, as palavras-passe não são armazenadas em texto simples:
![]() |
O algoritmo que encripta as palavras-passe é o algoritmo BCRYPT.
Tabela [ROLES]: as funções
- ID: chave primária;
- VERSION: coluna de controlo de versões da linha;
- NAME: nome da função. Por predefinição, o Spring Security espera nomes no formato ROLE_XX, por exemplo, ROLE_ADMIN ou ROLE_GUEST;
![]() |
Tabela [USERS_ROLES]: tabela de junção USERS / ROLES
Um utilizador pode ter várias funções, e uma função pode agrupar vários utilizadores. Existe uma relação muitos-para-muitos representada pela tabela [USERS_ROLES].
- ID: chave primária;
- VERSION: coluna de controlo de versões da linha;
- USER_ID: identificador de um utilizador;
- ROLE_ID: identificador de uma função;
![]() |
Uma vez que estamos a alterar a base de dados, todas as camadas do projeto [métier, DAO, JPA] têm de ser alteradas:
![]() |
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 implementação de medidas de segurança foram reunidas num único pacote, o [rdvmedecins.security]. Estes novos elementos pertencem às camadas [JPA] e [DAO], mas, por uma questão de simplicidade, foram reunidos num ú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 nenhum nome para as colunas, uma vez que estas têm o mesmo nome que os campos que lhes estão associados;
A classe [Role] é o reflexo da tabela [ROLES]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
private static final long serialVersionUID = 1L;
// 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] é enriquecida com três novos [Repository]:
![]() |
A interface [UserRepository] gere o acesso às entidades [User]:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
// lista das funções de um utilizador identificado pelo seu ID
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// lista de funções de um utilizador identificado pelo seu nome de utilizador e palavra-passe
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// pesquisa de um utilizador através do seu nome de utilizador
User findUserByLogin(String login);
}
- linha 9: a interface [UserRepository] estende a interface [CrudRepository] do Spring Data (linha 4);
- linhas 12-13: o método [getRoles(User user)] permite obter todas as funções de um utilizador identificado pelo seu [id]
- linhas 16-17: o mesmo, mas para um utilizador identificado pelo seu nome de utilizador e palavra-passe;
- linha 20: para encontrar um utilizador através do seu nome de utilizador;
A interface [RoleRepository] gere os acessos às entidades [Role]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// pesquisa de uma função através do seu nome
Role findRoleByName(String name);
}
- linha 5: a interface [RoleRepository] estende a interface [CrudRepository];
- linha 8: é possível pesquisar uma função pelo seu nome;
A interface [userRoleRepository] gere o acesso às entidades [UserRole]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- linha 5: a interface [UserRoleRepository] limita-se a estender a interface [CrudRepository] sem lhe adicionar novos métodos;
8.4.13.5. As classes de gestão de utilizadores e funções
![]() |
O Spring Security exige a criação de uma classe que implemente a seguinte interface [UsersDetail]:
![]() |
Esta interface é aqui implementada pela classe [AppUserDetails]:
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// propriedades
private User user;
private UserRepository userRepository;
// construtores
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interface
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// getters e setters
...
}
- linha 10: a classe [AppUserDetails] implementa a interface [UserDetails];
- linhas 15-16: a classe encapsula um utilizador (linha 15) e o repositório que permite obter os detalhes desse utilizador (linha 16);
- linhas 22-25: o construtor que instancia a classe com um utilizador e o seu repositório;
- linhas 28-35: implementação do método [getAuthorities] da interface [UserDetails]. Este método deve construir uma coleção de elementos do tipo [GrantedAuthority] ou derivado. Aqui, utilizamos o tipo derivado [SimpleGrantedAuthority] (linha 32), que encapsula o nome de uma das funções do utilizador da linha 15;
- linhas 31-33: percorre-se a lista de funções do utilizador da linha 15 para construir uma lista de elementos do tipo [SimpleGrantedAuthority];
- linhas 38-40: implementam o método [getPassword] da interface [UserDetails]. Retorna-se a palavra-passe do utilizador da linha 15;
- linhas 38-40: implementam o método [getUserName] da interface [UserDetails]. Retorna-se o nome de utilizador da linha 15;
- linhas 47-50: a conta do utilizador nunca expira;
- linhas 52-55: a conta do utilizador nunca é bloqueada;
- linhas 57-60: as credenciais do utilizador nunca expiram;
- linhas 62-65: a conta do utilizador está sempre ativa;
O Spring Security também exige a existência de uma classe que implemente a interface [AppUserDetailsService]:
![]() |
Esta interface é implementada pela seguinte classe [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 {
// procura do utilizador pelo nome de utilizador
User user = userRepository.findUserByLogin(login);
// Encontrado?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// apresenta-se os detalhes do utilizador
return new AppUserDetails(user, userRepository);
}
}
- linha 9: a classe será um componente Spring, pelo que estará disponível no seu contexto;
- linhas 12-13: o componente [UserRepository] será injetado aqui;
- linhas 16-25: implementação do método [loadUserByUsername] da interface [UserDetailsService] (linha 10). O parâmetro é o nome de utilizador;
- linha 18: o utilizador é procurado através do seu nome de utilizador;
- linhas 20-22: se não for encontrado, é lançada uma exceção;
- linha 24: é criado e renderizado um objeto [AppUserDetails]. Este é, de facto, do tipo [UserDetails] (linha 16);
8.4.13.6. Testes da camada [DAO]
![]() |
Em primeiro lugar, criamos uma classe executável [CreateUser] capaz de criar um utilizador com uma função:
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// sintaxe: nome de utilizador palavra-passe roleName
// são necessários três parâmetros
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// recuperam-se os parâmetros
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// contexto Spring
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// a função já existe?
Role role = roleRepository.findRoleByName(roleName);
// se não existir, criamo-lo
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// o utilizador já existe?
User user = userRepository.findUserByLogin(login);
// Se não existir, criamo-lo
if (user == null) {
// a palavra-passe é hashada com o bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// guardamos o utilizador
user = userRepository.save(new User(login, login, crypt));
// criamos a relação com a função
userRoleRepository.save(new UserRole(user, role));
} else {
// o utilizador já existe — tem a função solicitada?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// se não for encontrado, cria-se a relação com a função
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// encerramento do contexto Spring
context.close();
}
}
- linha 17: a classe espera três argumentos que definem um utilizador: o seu nome de utilizador, a sua palavra-passe e a sua função;
- linhas 25-27: os três parâmetros são recuperados;
- linha 29: o contexto Spring é construído a partir da classe de configuração [DomainAndPersistenceConfig]. Esta classe já existia no projeto inicial. Deve ser alterada da seguinte forma:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- linha 1: é necessário indicar que agora existem componentes [Repository] no pacote [rdvmedecins.security];
- linha 4: é necessário indicar que agora existem entidades JPA no pacote [rdvmedecins.security];
Voltemos ao código de criação de um utilizador:
- linhas 30-32: recuperamos as referências dos três [Repository] que nos podem ser úteis para criar o utilizador;
- linha 34: verifica-se se a função já existe;
- linhas 36-38: se não for o caso, criamo-lo na base de dados. Terá um nome do tipo [ROLE_XX];
- linha 40: verifica-se se o nome de utilizador já existe;
- linhas 42-49: se o nome de utilizador não existir, criamo-lo na base de dados;
- linha 44: encripta-se a palavra-passe. Aqui, utiliza-se a classe [BCrypt] do Spring Security (linha 4). Por isso, são necessários os ficheiros deste framework. O ficheiro [pom.xml] inclui uma nova dependência:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- linha 46: o utilizador é guardado na base de dados;
- linha 48: assim como a relação que o liga à sua função;
- linhas 51-57: caso em que o login já exista – verifica-se então se, entre as suas funções, já se encontra a função que se pretende atribuir-lhe;
- linhas 59-61: se a função procurada não for encontrada, cria-se uma linha na tabela [USERS_ROLES] para associar o utilizador à sua função;
- não se previu a ocorrência de eventuais exceções. Trata-se de uma classe de suporte para criar rapidamente um utilizador com uma função.
Ao executar a classe com os argumentos [x x guest], obtêm-se, na base de dados, os seguintes resultados:
Tabela [USERS]
![]() Tabela |
Tabela [ROLES]
![]() |
Tabela [USERS_ROLES]
![]() |
Consideremos agora a segunda classe [UsersTest], que é um teste JUnit:
![]() |
package rdvmedecins.security;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
@Test
public void findAllUsersWithTheirRoles() {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user);
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// recupera-se o utilizador [admin]
User user = userRepository.findUserByLogin("admin");
// verifica-se se a sua palavra-passe é [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// verifica-se a função de administrador / administrador
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// recupera-se o utilizador [admin]
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// verifica-se se a sua palavra-passe é [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// verifica-se a função de admin / admin
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// método utilitário — apresenta os elementos de uma coleção
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- linhas 27-34: teste visual. São apresentados todos os utilizadores com as respetivas funções;
- linhas 36-46: verifica-se se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN], utilizando o repositório [UserRepository];
- linha 41: [admin] é a palavra-passe em texto simples. Na base, está encriptada de acordo com o algoritmo BCrypt. O método [BCrypt.checkpw] permite verificar se a palavra-passe em texto simples, uma vez encriptada, é efetivamente igual à que se encontra na base;
- linhas 48-59: verifica-se se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN], utilizando o serviço [appUserDetailsService];
A execução dos testes é bem-sucedida com os seguintes registos:
8.4.13.7. Conclusão intermédia
A adição das classes necessárias ao Spring Security foi possível com poucas alterações ao projeto original. Recorde-se que:
- adição de uma dependência do Spring Security no ficheiro [pom.xml];
- criação de três tabelas adicionais na base de dados;
- criação das entidades JPA e dos componentes Spring no pacote [rdvmedecins.security];
Este cenário muito favorável decorre do facto de as três tabelas adicionadas à base de dados serem independentes das tabelas existentes. Teria sido até possível colocá-las numa base de dados separada. Isto foi possível porque se decidiu que um utilizador tinha uma existência independente dos médicos e dos clientes. Se estes últimos fossem utilizadores potenciais, teria sido necessário criar ligações entre a tabela [USERS] e as tabelas [MEDECINS] e [CLIENTS]. Tal teria, então, tido um impacto significativo no projeto existente.
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. Existem outras alterações, de menor importância, nas classes [AppConfig] e [ApplicationModel]. Já nos deparámos com uma classe de configuração do Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
Vamos seguir o mesmo procedimento:
- linha 11: definir uma classe que estenda a classe [WebSecurityConfigurerAdapter];
- linha 13: definir um método [configure(HttpSecurity http)] que define os direitos de acesso aos diferentes URL do serviço web;
- linha 19: definir um método [configure(AuthenticationManagerBuilder auth)] que define os utilizadores e as suas funções;
A configuração do Spring Security é assegurada pela classe [SecurityConfig]:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.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 palavra-passe é encriptada pelo algoritmo de hash BCrypt
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// aplicação segura?
if (application.isSecured()) {
// a palavra-passe é transmitida através do 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 a aplicação
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 utilizadores da aplicação, é injetada;
- linhas 21-22: é injetada a classe [ApplicationModel], que funciona como cache da aplicação web. Decide-se aqui utilizá-la também, para configurar a aplicação web num único local. É ela que define o valor booleano [isSecured] da linha 36. Este valor booleano protege (true) ou não (false) a aplicação web;
- linhas 25-29: o método [configure(HttpSecurity http)] define os utilizadores e as suas funções. Recebe como parâmetro um tipo [AuthenticationManagerBuilder]. Este parâmetro é complementado com duas informações (linha 28):
- uma referência ao serviço [appUserDetailsService] da linha 20, que dá acesso aos utilizadores registados. Note-se aqui que o facto de estarem registados numa base de dados não é mencionado. Podem, portanto, estar num cache, fornecidos por um serviço web, ...
- o tipo de encriptação utilizado para a palavra-passe. Recorde-se aqui que utilizámos 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 predefinição, o Spring Security geria um token CSRF (Cross Site Request Forgery) que o utilizador que pretendesse autenticar-se tinha de reenviar ao servidor. Aqui, este mecanismo está desativado. Isto, aliado ao valor booleano (isSecured=false), permite utilizar a aplicação web sem segurança;
- linha 38: ativa-se o modo de autenticação por cabeçalho HTTP. O cliente deverá enviar o seguinte cabeçalho HTTP:
onde «code» é a codificação da cadeia «login:password» através do algoritmo Base64. Por exemplo, a codificação Base64 da cadeia admin:admin é YWRtaW46YWRtaW4=. Assim, o utilizador com o nome de utilizador [admin] e a palavra-passe [admin] enviará o seguinte cabeçalho HTTP para se autenticar:
- linhas 40-42: indicam que todos os URL do serviço web estão acessíveis aos utilizadores com a função [ROLE_ADMIN]. Isto significa que um utilizador que não tenha essa função não pode aceder ao serviço web;
- linha 47: a palavra-passe do utilizador pode ou não ser guardada numa sessão. Se for guardada, o utilizador só precisa de se autenticar na primeira vez. Nas vezes seguintes, não lhe serão solicitados os seus dados de identificação. Aqui, optou-se por um modo sem sessão. Cada pedido deverá ser acompanhado dos dados de identificação de segurança;
A classe [AppConfig], que configura toda a aplicação, evolui da seguinte forma:
![]() |
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- A alteração ocorre na linha 11: adiciona-se a classe de configuração [SecurityConfig];
Por fim, a classe [ApplicationModel] é enriquecida com um valor booleano:
@Component
public class ApplicationModel implements IMetier {
...
// dados de configuração
private boolean secured = false;
public boolean isSecured() {
return secured;
}
- na linha 6: define-se o valor booleano [secured] como [true / false], consoante se pretenda ou não ativar a segurança.
8.4.13.9. Testes do serviço web
Vamos testar o serviço web com o cliente Chrome [Advanced Rest Client]. Teremos de especificar o cabeçalho de autenticação HTTP:
onde [code] é o código Base64 da cadeia [login:password]. Para gerar este código, pode utilizar-se o seguinte programa:
![]() |
package rdvmedecins.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// são esperados dois argumentos: nome de utilizador e palavra-passe
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// estão a ser recolhidos os dois argumentos
String chaîne = String.format("%s:%s", args[0], args[1]);
// codifica-se a cadeia
byte[] data = Base64.encode(chaîne.getBytes());
// e exibe a sua codificação Base64
System.out.println(new String(data));
}
}
Se executarmos este programa com os dois argumentos [admin admin]:
![]() |
obtemos o seguinte resultado:
Agora que sabemos como gerar o cabeçalho de autenticação HTTP, vamos iniciar 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], utilizando o método GET;
- em [3], fornecemos o cabeçalho HTTP da autenticação. O código [YWRtaW46YWRtaW4=] é a codificação Base64 da cadeia [admin:admin];
- em [4], enviamos o comando HTTP;
A resposta do servidor é a seguinte:
![]() |
- em [1], o cabeçalho de autenticação HTTP;
- em [2], o servidor devolve uma resposta jSON;
- em [3], uma lista de cabeçalhos HTTP relacionados com a segurança da aplicação web;
Conseguimos, de facto, 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 experimentar o utilizador user / user. Este existe, mas não tem acesso ao serviço web. Se executarmos o programa de codificação Base64 com os dois argumentos [user user]:
![]() |
obtemos o seguinte resultado:
![]() |
- em [1] e [3]: o cabeçalho de autenticação HTTP;
- em [2]: a resposta do serviço web. É diferente da anterior, que era [401 Unauthorized]. Desta vez, o utilizador autenticou-se corretamente, mas não possui direitos suficientes para aceder ao URL;
Um serviço web seguro está agora operacional. Vamos completá-lo para que autorize pedidos entre domínios. Esta necessidade surgiu no documento [Tutoriel AngularJS / Spring 4] e, embora essa necessidade não exista aqui, vamos, mesmo assim, dar-lhe resposta.
8.4.14. Implementação de pedidos entre domínios
Vamos analisar o problema das requisiçõ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 da aplicação Angular provêm do servidor [1];
- em [2], o serviço [dao] faz uma solicitação a outro servidor, o servidor [2]. Ora, isso é proibido pelo navegador que executa a aplicação Angular, porque constitui uma falha de segurança. A aplicação só pode consultar o servidor de onde provém, ou seja, o servidor [1];
Na verdade, não é correto dizer que o navegador proíbe a aplicação Angular de consultar o servidor [2]. Na realidade, ela consulta-o para perguntar se este autoriza que um cliente que não provém do seu próprio domínio o consulte. A esta técnica de partilha chama-se CORS (Cross-Origin Resource Sharing). O servidor [2] dá o seu consentimento enviando cabeçalhos HTTP específicos.
Para ilustrar os problemas que podem surgir, vamos criar uma aplicação cliente/servidor em que:
- o servidor será o nosso servidor web / jSON;
- o cliente será uma página simples HTML equipada com código JavaScript que enviará pedidos ao servidor web / jSON;
8.4.14.1. O projeto do cliente
![]() |
O projeto é um projeto Maven com o seguinte ficheiro [pom.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-cors</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-cors</name>
<description>Client for webjson server</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- procura 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 inclui um servidor Tomcat e o Spring MVC;
A página HTML é a seguinte:
![]() |
É gerada pelo código seguinte:
<!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 validaçã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: importa-se 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() {
// recuperam-se as informações do formulário
var urlValue = url.val();
var postedValue = posted.val();
method = document.forms[0].elements['method'].value;
// efetua-se manualmente uma chamada Ajax
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// efetua-se 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) {
// faz-se 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() {
// a recuperar as referências dos componentes da página
url = $("#url");
posted = $("#posted");
response = $("#response");
});
Deixamos que o leitor compreenda este código. Tudo já foi abordado em algum momento. Algumas linhas merecem, no entanto, uma explicação:
- linha 11:
- [document] designa o documento carregado pelo navegador, o que se denomina DOM (Document Object Model),
- [document.forms[0]] designa o primeiro formulário do documento, podendo um documento conter vários. Neste caso, existe apenas um,
- [document.forms[0].elements['method']] designa o 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 selecionado. Neste caso, será, portanto, uma das cadeias ['get', 'post'];
- linhas 23-25: contactamos um servidor que exige um cabeçalho HTTP [Authorization: Basic code]. Criamos este cabeçalho para o utilizador [admin / admin], que é o único a poder consultar o servidor;
- linha 26: o utilizador irá introduzir URL do tipo [/getAllMedecins, /supprimerRv, ...]. Por isso, é necessário preencher estes URL;
- linha 28: o servidor devolve jSON, que é um texto. Indica-se o tipo [text/plain] como tipo de resultado, para o apresentar tal como foi recebido;
- linha 33: exibição da resposta de texto do servidor;
- linha 39: exibição de uma eventual mensagem de erro em formato de texto;
- linha 52: para indicar que o cliente envia jSON;
Na aplicação cliente/servidor desenvolvida:
- o cliente é uma aplicação web disponível em URL [http://localhost:8081]. É a aplicação que estamos a construir;
- o servidor é uma aplicação web disponível em URL [http://localhost:8080]. Este é o nosso servidor web / jSON;
Como o cliente não é acedido a partir da 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 consola 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. Esta anotação implica uma série de configurações automáticas;
- linha 16: para redefinir determinados valores por predefiniçã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, ...) 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 incorporado funcionará na porta 8081;
8.4.14.2. O URL [/getAllMedecins]
Iniciamos:
- o servidor web/json na porta 8080;
- o cliente deste servidor na porta 8081;
depois 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 consultar a consola de desenvolvimento (Ctrl-Shift-I), descobrimos um erro:
![]() |
- em [1], estamos no separador [Network];
- Em [2], verifica-se que a solicitação HTTP que foi efetuada não é [GET], mas sim [OPTIONS]. No caso de um pedido entre domínios, o navegador verifica junto do servidor se um determinado número de condições está preenchido, enviando-lhe um pedido HTTP [OPTIONS]. Neste caso, as solicitações são as 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 é aceite;
- em [5], o navegador envia o cabeçalho HTTP [Origin: http://localhost:8081]. Este cabeçalho solicita uma resposta num cabeçalho HTTP [Access-Control-Allow-Origin] indicando que a origem indicada é aceite;
- em [6], o navegador pergunta se os cabeçalhos HTTP, [accept] e [authorization] são aceites. O cabeçalho do pedido [Access-Control-Request-Headers] aguarda uma resposta com um cabeçalho HTTP [Access-Control-Allow-Headers] indicando que os cabeçalhos solicitados são aceites;
- ocorre um erro em [3]. Ao clicar no ícone, surge 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 do pedido é aceite;
- em [7], verifica-se que o servidor efetivamente não enviou esse cabeçalho. Consequentemente, o navegador recusou-se a efetuar a solicitação HTTP GET inicialmente solicitada;
Temos de alterar o servidor web / jSON. Fazemos uma primeira alteraçã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 um valor booleano que indica se aceitamos ou não clientes externos ao domínio do servidor;
- linhas 10-12: o método de acesso a esta informação;
Em seguida, criamos um novo controlador Spring MVC:
![]() |
A classe [RdvMedecinsCorsController] é a seguinte:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// envio de opções para o 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);
// autorizam-se 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 este é 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. Este cabeçalho foi enviado pelo remetente da solicitação:
Indica-se que o cabeçalho HTTP [Origin] é opcional [required = false]. Neste caso, se o cabeçalho estiver ausente, o parâmetro [String origin] assumirá o valor null. Sendo [required = true] o valor por predefinição, é lançada uma exceção se o cabeçalho estiver ausente. Pretendemos evitar esta situação;
- linha 34:
- o objeto [HttpServletResponse response] que será enviado ao cliente que efetuou o pedido;
Estes dois parâmetros são injetados pelo Spring;
- linha 35: delega-se o tratamento do pedido 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 pedidos entre domínios e se o remetente tiver enviado o cabeçalho HTTP [Origin] e se essa origem começar por [http://localhost], então aceita-se a solicitação entre domínios; caso contrário, rejeita-se;
- linhas 25: se o cliente estiver no domínio [http://localhost:port], enviamos o cabeçalho HTTP:
o que significa que o servidor aceita a origem do cliente;
- linha 25: indicámos dois cabeçalhos HTTP específicos na solicitação HTTP [OPTIONS]:
Ao cabeçalho HTTP [Access-Control-Request-X], o servidor responde com um cabeçalho HTTP [Access-Control-Allow-X], no qual indica o que está autorizado. As linhas 23-26 limitam-se a repetir o pedido do cliente para indicar que este foi aceite;
Estamos agora 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 consola, esta nunca é exibida, o que demonstra que o método [getAllMedecins] da linha 34 nunca é chamado.
Após algumas pesquisas, descobrimos que o Spring MVC trata ele próprio os comandos HTTP e [OPTIONS] com um processamento por predefinição. Assim, é sempre o Spring que responde e nunca o método [getAllMedecins] da linha 34. Este comportamento por predefiniçã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 gere as solicitações dos clientes. Aqui, é do tipo [DispatcherServlet], o servlet do framework Spring MVC;
- linha 12: cria-se uma instância do tipo [DispatcherServlet];
- linha 13: solicita-se que a servlet encaminhe os comandos HTTP e [OPTIONS] para a aplicação;
- linha 14: a servlet é configurada desta forma;
Repetimos os testes com esta nova configuração. Obtemos o seguinte resultado:
![]() |
- em [1], verificamos que existem 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 acabámos de configurar na resposta do servidor;
Vamos agora analisar a segunda solicitação:
![]() |
- em [1], a solicitação analisada;
- em [2], trata-se da solicitação GET. Graças à primeira solicitação [OPTIONS], o navegador recebeu as informações que solicitava. Agora, efetua a solicitação [GET] inicialmente solicitada;
- em [3], a resposta do servidor;
- em [4], o servidor envia jSON;
- em [5], ocorreu um erro;
- em [6], a mensagem de erro;
É mais difícil explicar o que aconteceu aqui. A resposta [3] do servidor é normal [HTTP/1.1 200 OK]. Por isso, deveríamos ter o documento solicitado. É possível que o servidor tenha efetivamente enviado o documento, mas que seja o navegador a impedir a sua utilização porque pretende que, também para o pedido GET, a resposta inclua o cabeçalho HTTP [Access-Control-Allow-Origin:http://localhost:8081].
Alteramos o controlador [RdvMedecinsController] da seguinte forma:
@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);
// estado da aplicação
...
- 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]. Assim, irá enviar os mesmos cabeçalhos HTTP que para essa solicitação;
Após esta alteração, os resultados são os seguintes:
![]() |
Conseguimos, de facto, obter a lista de médicos.
8.4.14.3. Os outros URL [GET]
Apresentamos agora os restantes URL consultados através 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. Eis 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);
// estado da aplicação
...
Seguem-se agora capturas de ecrã da execução:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
8.4.14.4. Os URL [POST]
Analisemos o seguinte caso:
![]() |
- faz-se um POST [1] para o URL [2];
- no [3], o valor lançado. Trata-se de uma cadeia jSON;
- no total, pretende-se eliminar o compromisso com o valor [id] 100;
Por enquanto, não alteramos nenhum código. O resultado obtido é então o seguinte:
![]() |
- em [1], tal como nas solicitações [GET], o navegador efetua uma solicitação [OPTIONS];
- em [2], solicita uma autorização de acesso para uma solicitação [POST]. Anteriormente, era [GET];
- em [3], solicita autorização para enviar os cabeçalhos HTTP e [accept, authorization, content-type]. Anteriormente, existiam apenas os dois primeiros cabeçalhos;
Alteramos o método [RdvMedecinsCorsController.sendOptions] da seguinte forma:
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);
// autorizam-se 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: adicionámos os cabeçalhos HTTP e [Content-Type] (as maiúsculas e minúsculas não importam);
- linha 11: adicionámos o método HTTP [POST];
Assim, os métodos [POST] são tratados da mesma forma que as solicitações [GET]. Aqui está o exemplo do 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 da aplicação
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 [/ajouterRv], obtém-se o seguinte resultado:
![]() |
8.4.14.5. Conclusion
A nossa aplicação suporta agora os pedidos entre domínios. Estes podem ser autorizados ou não através 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 da aplicação que pretendemos desenvolver:
![]() |
A parte superior do esquema já foi escrita. Trata-se do servidor web / jSON. Vamos agora abordar a parte inferior e, em primeiro lugar, a sua camada [DAO]. Vamos escrever esta camada e, em seguida, testá-la com um cliente de consola. A arquitetura de teste será a seguinte:
![]() |
8.5.1. O projeto do cliente de consola
O projeto STS do cliente de consola será o seguinte:
![]() |
8.5.2. Configuração do Maven
O ficheiro [pom.xml] do cliente da consola é o seguinte:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webjson-client-console</name>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- pesquisa do 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 Spring Boot pai;
- linhas 24-27: o cliente de consola do servidor web / jSON baseia-se num componente denominado [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 utilizada pelo Spring Web;
- linhas 38-41: ao nível mais baixo, o componente [RestTemplate] comunica com o servidor através dos sockets TCP/IP. Queremos definir o [timeout], ou seja, o tempo máximo de espera por uma resposta do servidor. O componente [RestTemplate] não nos permite definir esse valor. Para o fazer, vamos passar ao construtor [RestTemplate] um componente de baixo nível fornecido pela dependência [org.apache.httpcomponents.httpclient]. É esta 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 através dos seus diversos URL. Não vamos voltar a detalhá-las. Limitar-nos-emos a referir que as entidades JPA e [Client, Creneau, Medecin, Rv, Personne] foram despojadas de todas as suas anotações JPA, bem como das suas anotações jSON. Eis, 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á associada a um cliente
private Client client;
// uma reunião está associada a um intervalo de tempo
private Creneau creneau;
// chaves externas
private long idClient;
private long idCreneau;
// fabricante por predefinição
public Rv() {
}
// com parâmetros
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// getters e setters
...
}
8.5.4. O pacote [rdvmedecins.client.requests]
![]() |
O pacote [rdvmedecins.client.requests] reúne as duas classes cujo valor jSON é atribuído às classes URL, [/ajouterRv] e [supprimerRv]. 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
// estado 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 consoante o URL do serviço web / jSON;
8.5.6. O pacote [rdvmedecins.client.dao]
![]() |
- [IDao] é a interface da camada [DAO] e [Dao] é a sua implementação. Voltaremos a esta implementação;
8.5.7. O pacote [rdvmedecins.client.config]
![]() |
A classe [DaoConfig] configura a aplicação. O seu código é o seguinte:
package rdvmedecins.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
@Bean
public RestTemplate restTemplate() {
// 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 para localizar componentes Spring. Nele encontrará-se o componente [Dao];
- linhas 17-24: definem um singleton do Spring com o nome [restTemplate] (o nome do método). Este método devolve uma instância [RestTemplate], que é a ferramenta básica que o Spring fornece para comunicar com um serviço web / 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 tal, injetamos no componente [RestTemplate] um componente de baixo nível do tipo [HttpComponentsClientHttpRequestFactory] (linha 20), que nos permitirá definir esses [timeout]. A dependência Maven necessária já foi apresentada;
- linhas 28-57: definem os mapeadores jSON. Estes 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>]. Estes mesmos conversores serão agora utilizados no lado do cliente para deserializar o tipo T;
8.5.8. A interface [IDao]
Voltemos à arquitetura da aplicação:
![]() |
A camada [DAO] é um adaptador entre a camada [console] e as URL expostas pelo serviço web / jSON. A sua interface [IDao] será a seguinte:
package rdvmedecins.client.dao;
import java.util.List;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
public interface IDao {
// 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);
// encontrar um cliente identificado pelo seu ID
public Client getClientById(User user, long id);
// encontrar um cliente identificado pelo seu ID
public Medecin getMedecinById(User user, long id);
// encontrar uma consulta identificada pelo seu 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);
// eliminar um RV
public void supprimerRv(User user, long idRv);
// lista das consultas de um médico, num 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] do lado do cliente. Pretende-se controlar este parâmetro porque alguns clientes HTTP demoram, por vezes, muito tempo à espera de uma resposta que não chegará;
- linha 20: o método que permite identificar um utilizador [login, passwd]. Lança uma exceção se o utilizador 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 decorre 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: verifica-se que [idMedecin] e [jour] são os parâmetros do URL. Estes serão os parâmetros de entrada do método associado a este URL do lado do cliente;
- linha 2: verifica-se que o método do servidor devolve um tipo [Response<String>]. Este tipo [String] é o tipo do valor jSON de um tipo [AgendaMedecinJour]. O tipo do resultado do método associado a este URL do lado do cliente será [AgendaMedecinJour];
No lado do cliente, declara-se o seguinte método:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
Esta assinatura é adequada quando o servidor envia uma resposta [int status, List<String> messages, String body] com [status==0]. Neste caso, temos [messages==null && body!=null]. Não é adequada quando se trata de [status!=0]. Nesse caso, temos [messages!=null && body==null]. Temos de, de alguma forma, indicar que ocorreu um erro. Para tal, lançaremos uma exceção do tipo [RdvMedecinsException] da seguinte forma:
package rdvmedecins.client.dao;
import java.util.List;
public class RdvMedecinsException extends RuntimeException {
private static final long serialVersionUID = 1L;
// 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 irá recuperar 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 é obrigatório tratá-la com um try/catch nem 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
...
}
Com efeito, cada troca de dados com o serviço web / jSON deve ser acompanhada por 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 a aplicação de consola.
![]() |
A classe [Main] é a seguinte:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
// serializador jSON
static private ObjectMapper mapper = new ObjectMapper();
// tempo de espera das ligaçõ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");
// definem-se 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);
}
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);
}
// eliminaçã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);
}
// 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);
}
}
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á apresentar 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 o 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 ao leitor a tarefa de associar os resultados ao código. Este mostra como chamar cada método da camada [DAO]. Basta referir alguns pontos:
- linhas 2-14: mostram que, em caso de erro de autenticação, o servidor devolve um estado HTTP, [403 Forbidden] ou [401 Unauthorized], consoante o caso;
- linhas 30-31: é adicionada uma consulta ao médico n.º 1;
- linhas 32-33: vemos esta consulta. É a única do dia;
- linhas 34-35: também se vê na agenda do médico;
- linhas 36-37: a consulta desapareceu. O código eliminou-a entretanto;
Os registos da consola são controlados pelos seguintes ficheiros:
![]() |
[application.properties]
logging.level.org.springframework.web=OFF
logging.level.org.hibernate=OFF
spring.main.show-banner=false
logging.level.httpclient.wire=OFF
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- os codificadores são, por predefinição, atribuídos ao tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- controlo do nível dos registos -->
<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 da sua interface [IDao]. Iremos fazê-lo de forma progressiva.
![]() |
A interface [IDao] é implementada pela classe abstrata [AbstractDao] e pela 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 utilizador e a sua palavra-passe 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())));
}
// pedido genérico
protected String getResponse(User user, String url, String jsonPost) {
...
}
}
- linha 20: a classe é abstrata, o que nos impede de a designar como um componente Spring. Será a 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 de espera do cliente enquanto aguarda uma resposta do servidor;
- linha 34: recuperamos o componente [HttpComponentsClientHttpRequestFactory] que tínhamos injetado no bean [restTemplate] aquando da sua criação (ver [AppConfig]);
- linha 36: definimos o tempo máximo de espera do cliente quando este estabelece uma ligação com o servidor;
- linha 37: definimos o tempo máximo de espera do cliente enquanto aguarda uma resposta a uma das suas solicitações;
A implementação dos métodos de comunicação com o servidor será fatorizada no seguinte método genérico:
// pedido genérico
protected String getResponse(User user, String url, String jsonPost) {
...
}
- linha 2: os parâmetros de [getResponse] são os seguintes:
- [User user]: o utilizador que estabelece a ligação;
- [String url]: o URL a consultar. Trata-se da parte final do URL, sendo a primeira parte fornecida pelo campo [urlServiceWebJson] da classe,
- [String jsonPost]: a cadeia jSON a enviar. Se este valor estiver presente, então o URL será solicitado com um POST; caso contrário, será com um GET;
Continuemos:
// pedido genérico
protected String getResponse(User user, String url, String jsonPost) {
// URL: URL para contactar
// jsonPost: o valor jSON a enviar
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á a ser 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 a sua resposta. O componente [RestTemplate] oferece um número significativo de métodos de interação com o servidor. Ter-se-ia 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 cadeia de caracteres 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 este. Da mesma forma, o tipo [RequestEntity] encapsula toda a solicitação do cliente, incluindo os cabeçalhos HTTP e o eventual valor enviado;
- linha 23: é o corpo do objeto [ResponseEntity<String>] que é devolvido ao método chamador, ou seja, a cadeia jSON enviada pelo servidor;
- linhas 9-21: temos de construir a solicitação do tipo [RequestEntity]. Esta varia consoante se utilize um GET ou um POST para efetuar a solicitação;
- linha 9: a consulta para um GET. A classe [RequestEntity] disponibiliza 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 irá enviar;
- o resultado desta cadeia 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 estas 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 pretende utilizar, neste caso o da autorização,
- o método [header] que se segue inclui na solicitação o cabeçalho [Content-Type: application/json] para indicar que o valor enviado chegará na forma de uma cadeia jSON;
- o método [accept] permite indicar que aceitamos o tipo [application/json] que o servidor irá enviar;
- linhas 17-19: no caso de o parâmetro [User user] não ser null, inclui-se o cabeçalho HTTP [Authorization] na solicitação;
- linha 20: o método [BodyBuilder.body] define o valor lançado. Este é o segundo parâmetro do método genérico [getResponse] (linha 2);
- linhas 25-28: se ocorrer 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) {
// recupera-se 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 apenas se esta for !=null e não estiver em branco
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// causa seguinte
cause = cause.getCause();
}
return erreurs;
}
O método privado [getBase64] fornece o código Base64 da cadeia «login:passwd» para o cabeçalho de autenticação HTTP:
private String getBase64(User user) {
// codifica o utilizador e a sua palavra-passe 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 forma:
package rdvmedecins.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
@Service
public class Dao extends AbstractDao implements IDao {
// 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 a utilizar a anotação [@Component] utilizada 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.
Em primeiro lugar, 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 utilizador;
- 2: o destino URL;
- 3: o valor a enviar. Neste caso, não há nenhum;
- linha 5: a chamada não foi colocada dentro de um bloco 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]. Este mesmo mapeador é utilizado para deserializar a cadeia 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, é apresentada a agenda do médico;
A consulta POST analisada será a seguinte:
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
// a resposta
Response<Rv> response;
try {
// o Rv
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 utilizador;
- 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: do lado do servidor, foi o mapeador jSON [jsonMapperLongRv] que serializou a resposta do servidor. Do lado do cliente, utiliza-se esse mesmo mapeador para a deserializar;
- linha 6: o URL [/ajouterRv] devolve o valor jSON de um tipo [Response<Rv>];
- linhas 4-11: aqui, o método [getResponse] foi colocado num 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 que se segue (linhas 13-24) é análogo ao que acabámos 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 lançar.
Os restantes métodos seguem o mesmo modelo.
8.5.11. Anomalia
Ao realizar vários testes, depara-se com uma anomalia resumida na seguinte classe [Anomalie]:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Anomalie {
// serializador jSON
static private ObjectMapper mapper = new ObjectMapper();
// tempo limite das ligaçõ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");
// definem-se 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 utilizador [admin, admin];
- linhas 40-47: autentica-se o utilizador [admin, x], que, portanto, tem uma palavra-passe errada;
- linhas 49-56: autentica-se o utilizador [user, user], que é um utilizador existente, mas não autorizado;
Eis os resultados:
- linha 2: contra todas as expectativas, o utilizador [admin, x] foi aceite;
Se colocarmos as linhas 33-38 do código como comentários, obtemos o seguinte resultado:
o que é o resultado esperado. É como se, quando o utilizador [admin, admin] se autenticou com sucesso pela primeira vez, a sua palavra-passe já não fosse necessária nas vezes seguintes. É mesmo esse o caso. O Spring Security utiliza, por predefinição, uma sessão que faz com que, uma vez que um utilizador se tenha autenticado, já não precise de o fazer novamente nas solicitações seguintes. É possível alterar a configuração de [Spring Security] no servidor web / jSON para que isso deixe de acontecer:
![]() |
O ficheiro [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 indica que não deve haver nenhuma sessão de segurança;
Isto 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 construir:
![]() |
- 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. Com efeito, o [Web1] irá fornecer fluxos HTML encapsulados numa cadeia jSON. A arquitetura cliente/servidor é a seguinte:
![]() |
- Temos uma arquitetura cliente [2] / servidor [1], em que o cliente e o servidor comunicam em jSON;
- em [1], a camada web Spring MVC / Thymeleaf fornece vistas, fragmentos de vista e dados em jSON. O servidor é, portanto, um servidor web / jSON, tal como o servidor [Web1]. Também é um servidor sem estado;
- em [2]: o código JavaScript incorporado na vista carregada no arranque da aplicação está estruturado em camadas:
- a camada [présentation] trata das interações com o utilizador,
- a camada [DAO] trata do acesso aos dados através do servidor [Web2];
- o cliente [2] irá armazenar determinadas vistas 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;
- escrita das vistas;
- criação do controlador;
Posteriormente, e separadamente, iremos construir o cliente JS do servidor [Web1]. Para demonstrar claramente que este cliente possui uma certa independência em relação ao servidor [Web1], iremos construí-lo com a ferramenta [Webstorm], em vez de com a STS.
A seguir, alguns detalhes serão ignorados, pois poderiam fazer-nos esquecer 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 vistas;
A configuração do Maven em [pom.xml] é a seguinte:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-springthymeleaf-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-springthymeleaf-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<start-class>rdvmedecins.springthymeleaf.server.boot.Boot</start-class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
...
</project>
- linhas 16-19: o projeto é um projeto Thymeleaf;
- linhas 20-24: que se baseia na camada [DAO] que acabámos de construir;
A configuração Java é assegurada por dois ficheiros:
![]() |
A camada [web] é configurada pelo seguinte ficheiro [WebConfig]:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {
// ----------------- 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 deparámos, em algum momento, com todos os elementos desta configuração. Recorde-se apenas que as linhas 42-47 são necessárias quando se pretende interrogar o servidor com pedidos entre domínios (CORS). Será esse 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 de espera 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 à aplicação aceder ao arranque da aplicação, a fim de 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 da aplicação;
- 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 funcionar na porta 8081:
![]() |
server.port=8081
8.6.3. As funcionalidades da aplicação
Foram descritas no parágrafo 8.2. Vamos agora recapitulá-las. Através de um navegador, solicita-se o URL [http://localhost:8081/boot.html]:
![]() |
- em [1], a página de início de sessão da aplicação;
- em [2] e [3], o nome de utilizador e a palavra-passe de quem pretende utilizar a aplicação. Existem dois utilizadores: admin/admin (login/password) com uma função (ADMIN) e user/user com uma função (USER). Apenas a função ADMIN tem permissão para utilizar a aplicação. A função USER existe apenas para mostrar a resposta do servidor neste caso de utilização;
- em [4], o botão que permite ligar-se ao servidor;
- em [5], o idioma da aplicação. Existem dois: o francês, por predefinição, e o inglês;
- em [6], o URL do servidor [rdvmedecins-springthymeleaf-server];
![]() |
- em [1], efetua-se a ligação;
![]() |
- uma vez conectado, é possível escolher o médico com quem se pretende marcar uma consulta [2] e o dia da mesma [3]. Assim que o médico e o dia forem indicados, a agenda é automaticamente apresentada:
![]() |
- Depois de obter a agenda do médico, é possível reservar um horário [5];
![]() |
- em [6], seleciona-se o doente para a consulta e confirma-se essa escolha em [7];
![]() |
Assim que a consulta for validada, é-se redirecionado automaticamente para a agenda, onde a nova consulta já se encontra registada. Esta consulta poderá ser posteriormente eliminada em [8].
As principais funcionalidades foram descritas. São simples. Terminemos com a gestão do idioma:
![]() |
- em [1], muda-se do francês para o inglês;
![]() |
- em [2], a visualização passa para inglês, incluindo o calendário;
8.6.4. Passo 1: introdução ao framework CSS Bootstrap
![]() |
No cliente web acima, as páginas HTML irão utilizar o framework CSS Bootstrap [http://getbootstrap.com/] que apresentamos agora.
8.6.4.1. O projeto dos exemplos
O projeto dos exemplos será o seguinte:
![]() |
- em [1]: o projeto na sua totalidade;
- em [2]: os códigos Java;
- em [3]: os scripts JavaScript;
![]() |
- em [4]: as bibliotecas JavaScript;
- em [5]: as vistas Thymeleaf;
- em [6]: as folhas de estilo;
8.6.4.1.1. Configuração do Maven
O ficheiro [pom.xml] é o 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 deparámos com este 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 apresentar vistas processadas pelo Thymeleaf.
8.6.4.1.4. O ficheiro [application.properties]
O ficheiro [application.properties] configura o servidor Tomcat incorporado:
server.port=8082
8.6.4.2. Exemplo n.º 1: o jumbotron
A ação [/bs-01] apresenta a seguinte vista [bs-01.xml]:
![]() |
A vista [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 ficheiro CSS do framework Bootstrap;
- linha 8: um ficheiro CSS local;
- linha 13: apresenta [1];
- linhas 19-21: exibem [2];
- linha 11: a classe CSS [container] define uma área de visualização no interior do navegador;
- linha 19: a classe CSS [alert] apresenta 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 vista [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: nestas 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] apresenta a seguinte vista [bs-02.xml]:
![]() |
A novidade é a barra de navegação [1] com o seu formulário de introdução de dados e os seus botões:
A vista [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>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- conteúdo -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- informação -->
<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 JS local;
- linha 16: a barra de navegação;
A barra de navegação é gerada pela seguinte vista [navbar1.xml]:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- 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] irá definir o estilo da barra de navegação. A classe [navbar-inverse] atribui-lhe um fundo preto. A classe [navbar-fixed-top] irá garantir que, quando se «desliza» a página apresentada pelo navegador, a barra de navegação permaneça na parte superior do ecrã;
- linhas 5-13: definem a área [1]. Trata-se, tipicamente, de uma série de classes que não compreendo. Utilizo o componente tal como está;
- linhas 14-26: definem uma área «responsiva» da barra de comandos. Num smartphone, esta área desaparece numa área de menu;
- linha 15: uma imagem atualmente oculta;
- linhas 17-25: a classe [navbar-form] define o estilo de um formulário da barra de comandos. A classe [navbar-right] posiciona-o à direita deste;
- linhas 21-23: as duas áreas de introdução de dados do formulário da linha 17, [2]. Estão dentro de uma classe [form-group] que define os elementos de um formulário e cada uma delas tem a classe [form-control];
- linha 24: a classe [btn], que define um botão, complementada pela classe [btn-success], que lhe confere a cor verde;
- linha 24: quando se clica no botão [Connexion], é executada a seguinte função JS:
function connecter() {
showInfo("Connexion demandée...");
}
function showInfo(message) {
$("#info").text(message);
}
Eis um exemplo:

8.6.4.4. Exemplo n.º 3: o botão com lista
A ação [/bs-03] apresenta a seguinte vista [bs-03.xml]:
![]() |
- A novidade é o controlo de lista [1], também conhecido como «dropdown»;
O código da vista [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 ficheiro JS do Bootstrap;
- linha 18: a nova barra de navegação;
A vista [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[*/
// inicialização da página
initNavBar2();
/*]]>*/
</script>
</section>
- linhas 25-40: definem o botão com lista;
- linha 27: a classe [btn-danger] atribui-lhe a 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 tem a classe [dropdown-toggle]. Trata-se do botão com lista (linha 28 da vista). Aplica-se-lhe a função JS [dropdown()], que está definida no ficheiro JS [bootstrap.js]. Só após esta operação é que o botão se comporta como um botão de lista;
- linhas 10-21: a função executada quando se seleciona um idioma;
Eis um exemplo:

8.6.4.5. Exemplo n.º 4: um menu
A ação [/bs-04] apresenta a seguinte vista [bs-04.xml]:
![]() |
Foi adicionado um menu [1].
A vista [bs-04.xml] é a seguinte:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- 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 vista [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[*/
// inicialização da página
initNavBar3();
/*]]>*/
</script>
</section>
- linhas 16-29: criam o menu com quatro opções, cada uma delas ligada 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");
// colocam-se num dicionário
var options = {
"lnkAccueil" : lnkAccueil,
"lnkAfficherAgenda" : lnkAfficherAgenda,
"lnkValiderRv" : lnkValiderRv,
"lnkRetourAgenda" : lnkRetourAgenda
}
// ocultam-se todos os links
for ( var key in options) {
options[key].hide();
}
// exibimos os 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 está oculta;
- linhas 26-48: uma função [setMenu] que permite indicar quais as opções que devem estar visíveis;
Vamos para a consola de desenvolvimento (Ctrl-Shift-I) e introduzamos o seguinte código [1]:
![]() |
Depois, voltemos ao navegador. O menu mudou [2]:
8.6.4.6. Exemplo n.º 5: uma lista suspensa
A ação [/bs-05] apresenta a seguinte vista [bs-05.xml]:
![]() |
A novidade está em [1]. Aqui, utilizamos um componente fornecido fora do Bootstrap, [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].
O código da vista [bs-05.xml] é o seguinte:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- 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 ficheiro JS necessário para a lista suspensa;
- linha 24: a lista suspensa;
A vista [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[*/
// inicializa a página
initChoixMedecin();
/*]]>*/
</script>
</section>
- linhas 7-12: trata-se de uma baliza [select] clássica, mas com uma classe específica [combobox]. O atributo [data-style="btn-primary"] atribui ao componente a sua cor azul;
- linhas 16-21: um script executado ao carregar a página;
O ficheiro JS [bs-05.js] é o seguinte:
...
function afficherAgenda() {
var idMedecin = $('#idMedecin option:selected').val();
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
function initChoixMedecin() {
// o menu de seleção 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 numa lista suspensa Bootstrap. O [$('#idMedecin')] faz referência ao [select] (linha 7 da vista [choixmedecin]) e a função JS [selectpicker] provém do ficheiro JS [bootstrap-select.js];
- linha 11: é apresentada 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, nesse 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;
Eis um exemplo de escolha de um médico:
![]() |
8.6.4.7. Exemplo n.º 6: um calendário
A ação [/bs-06] apresenta a seguinte vista [bs-06.xml]:

A seleção de um médico ou de uma data aciona uma função JS que apresenta tanto o médico como a data selecionados. Eis um exemplo:
![]() |
Através do botão «Lista de idiomas», é possível mudar o calendário (e apenas o calendário) para inglês:

Este é o exemplo mais complexo da série. O calendário é um componente [bootstrap-datepicker] [http://eternicode.github.io/bootstrap-datepicker].
A vista [bs-06.xml] é a seguinte:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- 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 ficheiro CSS do componente [bootstrap-datepicker];
- linha 16: o ficheiro JS do componente [bootstrap-datepicker];
- linha 17: o ficheiro JS para gerir um calendário francês. Por predefinição, está em inglês;
- linha 15: o ficheiro JS de uma biblioteca denominada [moment], que dá acesso a inúmeras funções de cálculo de tempo [http://momentjs.com/];
- linha 28: a vista do calendário;
A vista [choixmedecinjour.xml] é a seguinte:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
<div class="col-md-3">
<h2>Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- script local -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicialização da página
initChoixMedecinJour();
/*]]>*/
</script>
</section>
- linhas 17-23: o calendário;
- linha 18: a classe [btn-primary] atribui-lhe a cor azul;
- linha 18: o atributo [disabled="true"] impede que a data seja introduzida manualmente. É obrigatório utilizar o calendário;
- linha 16: o calendário foi colocado numa secção [id="calendar_container"]. Para alterar o idioma do calendário, é necessário eliminá-lo e, em seguida, regenerá-lo. Assim, eliminaremos 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 ficheiro 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 lista de seleção de médicos
$('#idMedecin').selectpicker();
$('#idMedecin').change(function(e) {
afficherAgenda();
})
// o menu
setMenu([]);
}
- linha 2: o calendário é gerido por várias funções JS. A variável [calendar_infos] irá reunir informações sobre o calendário. É global para poder ser vista pelas diferentes funções;
- linha 6: identifica-se o contentor do calendário;
- linhas 7-12: as informações armazenadas para o calendário;
- linha 8: uma referência ao seu contentor,
- linha 9: o código HTML do calendário. Com estas duas informações, é possível eliminar 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 neste 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) {
// regeneraçã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) {
// regista-se a nova data
calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
// exibição de informações da agenda
afficherAgenda();
// visualização do dia selecionado
displayJour();
});
// visualizaçã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: os seus parâmetros de inicialização;
- linha 9: o formato das datas geridas 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, então essa data é definida como a data atual do calendário;
- linhas 19-22: sempre que o calendário for fechado, será apresentada a data selecionada;
- linhas 23-30: sempre que houver uma alteração de data no calendário:
- linha 25: regista-se a data selecionada em [calendar_infos],
- linha 27: exibem-se informações sobre a agenda,
- linha 29: exibe-se o dia selecionado;
- linha 32: exibição do dia selecionado, caso exista;
O método [displayJour] que apresenta 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 já tiver sido selecionada uma data (no início, o calendário não tem nenhuma data selecionada);
- linha 4: localiza-se o componente onde se irá escrever a data;
- linha 5: esta 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: esta data é apresentada;
Eis dois exemplos:
![]() | ![]() |
Quando se altera o médico ou a data, é executado o método [afficherAgenda]:
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 «responsiva»
Nota: «responsive» é um termo em inglês que indica que um componente é capaz de se adaptar ao tamanho do ecrã em que é visualizado. Vamos mostrar um exemplo.
A ação [/bs-07] apresenta a seguinte vista [bs-07.xml] (ecrã inteiro):
![]() |
A novidade é a tabela HTML [1]. Esta tabela é gerida pela biblioteca JS [footable]: [https://github.com/fooplugins/FooTable].
Se reduzirmos o tamanho da janela do navegador, obtemos o seguinte:
![]() |
- a tabela HTML adaptou-se ao tamanho do ecrã;
- em [1], para ver o link [Réserver], é necessário clicar no símbolo [+];
- em [2], o que se vê ao clicar no símbolo [+];
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 vista [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[*/
// inicializa-se a página
initAgenda();
/*]]>*/
</script>
</body>
</html>
- linha 4: insere a tabela numa 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 o ecrã tiver o tamanho de um ecrã de telemóvel. Também é possível utilizar 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 ficheiro CSS [bootstrapDemo.css]:
@CHARSET "UTF-8";
#intervalos th {
text-align: center;
}
#intervalos td {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
Os estilos [status-*] provêm de um exemplo de utilização da tabela [footable] encontrado no site da biblioteca.
No ficheiro JS [bs-07.js], a página é inicializada da seguinte forma:
function initAgenda() {
// a tabela de horários
$("#creneaux").footable();
}
É tudo. O ficheiro [$("#creneaux")] faz referência à tabela HTML, que pretendemos tornar «responsiva». Além disso, encontram-se as funções JS associadas 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] apresenta a seguinte vista [bs-08.xml]:

Enquanto anteriormente, clicar na ligação [Réserver] exibia uma informação na caixa de informações, aqui vamos fazer aparecer uma caixa modal para selecionar um cliente para o RV:

O componente utilizado é o [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/].
A vista [bs-08.xml] é a seguinte:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- 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 ficheiro JS necessário para as caixas modais;
- linha 32: a vista [agenda-modal] é idêntica à vista [agenda], com uma única diferença: a função JS que gere o link [Réserver]:
<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>
A função [showDialogResa] é responsável por apresentar a caixa modal de seleção de um cliente;
- linha 33: a vista [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[*/
// inicializa-se a página
initResa();
/*]]>*/
</script>
</section>
- linhas 3-37: a caixa modal;
- linhas 13-30: o conteúdo desta caixa (o que será apresentado);
- linhas 31-34: os botões da caixa de diálogo;
- linha 32: um botão [Annuler] gerido pela função JS [cancelDialogResa];
- linha 33: um botão [Valider] gerido pela função JS [validateResa];
- linhas 39-44: o script de inicialização da caixa modal;
Isto resulta na seguinte visualização:
![]() |
Note-se que a caixa modal não é apresentada por predefinição. É por isso que não a vemos ao iniciar a aplicação, apesar de o seu código HTML estar presente no documento.
O ficheiro JS [bs-08.js] é o seguinte:
var idCreneau;
var idClient;
var resa;
function showDialogResa(idCreneau) {
// guarda-se o ID do horário
this.idCreneau = idCreneau;
// exibe-se a janela de reserva
var resa = $("#resa");
resa.modal('show');
// registo
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() {
// recuperam-se as informações
var idClient = $('#idClient option:selected').val();
// oculta 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() {
// o seletor 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 deve 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 é tornada visível;
- linha 12: regista-se uma informação na caixa de informações;
- linhas 15-18: gestão 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: recupera-se o atributo [value] do cliente selecionado;
- linha 25: oculta-se a caixa de diálogo;
- linha 27: registam-se as duas informações: o número do horário reservado e o nome do cliente;
8.6.5. Etapa 2: criação das vistas
Vamos agora descrever as vistas fornecidas pelo servidor [Web1], bem como os seus modelos.
![]() |
8.6.5.1. A vista [navbar-start]
Apresenta a barra de navegação da página inicial:

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[*/
// inicialização da página
initNavBarStart();
/*]]>*/
</script>
</section>
Esta vista não tem modelo. Possui os seguintes gestores de eventos:
event | gestor |
clique no botão de ligação | |
clique no link [Français] | |
clique na ligação [English] |
8.6.5.2. A vista [jumbotron]
Esta é a vista apresentada abaixo da barra de navegação [navbar-start] na página inicial:

O 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 vista [jumbotron] não tem modelo nem eventos.
8.6.5.3. A vista [login]
Esta é a vista que é apresentada sob o jumbotron na página inicial:

O seu código [login.xml] é o seguinte:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{identification}">Identification
</div>
</section>
A vista não tem modelo nem eventos.
8.6.5.4. A vista [navbar-run]
Esta é a barra de navegação apresentada quando a ligação é bem-sucedida:

O seu código [navbar-run.xml] é o seguinte:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- 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[*/
// inicializar a página
initNavBarRun();
/*]]>*/
</script>
</section>
Esta vista não tem modelo. Possui os seguintes gestores de eventos:
evt | gestor |
clique no botão de desligar | |
clique no link [Français] | |
clique na ligação [English] |
8.6.5.5. A vista [accueil]
Esta é a vista apresentada imediatamente abaixo da barra de navegação [navbar-run]:

O 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[*/
// inicialização da página
initChoixMedecinJour();
/*]]>*/
</script>
</html>
O seu modelo é o seguinte:
- [rdvmedecins.medecinItems] (linha 8): a lista de médicos;
Na sua forma atual, a vista não parece ter um gestor de eventos. Na realidade, estes estão definidos na função [initChoixMedecinJour]. Esta 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 gestores de eventos:
event | gestor |
escolha de um médico | |
escolha de uma data |
8.6.5.6. A vista [agenda]
A vista [agenda] apresenta um dia da agenda de um médico:

O seu código [agenda.xml] é o seguinte:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3 class="alert alert-info" th:text="${agenda.titre}">Agenda de Mme Pélissier le 13/10/2014</h3>
<h4 class="alert alert-danger" th:if="${agenda.creneaux.length}==0" th:text="#{agenda.medecinsanscreneaux}">Ce médecin n'a pas encore de créneaux
de consultation</h4>
<th:block th:if="${agenda.creneaux.length}!=0">
<div class="row tab-content alert alert-warning">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span th:text="#{agenda.creneauhoraire}">Créneau horaire</span>
</th>
<th>
<span th:text="#{agenda.client}">Client</span>
</th>
<th data-hide="phone">
<span th:text="#{agenda.action}">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr th:each="creneau,iter : ${agenda.creneaux}">
<td>
<span th:if="${creneau.action}==1" class="status-metro status-active" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
<span th:if="${creneau.action}==2" class="status-metro status-suspended" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
</td>
<td>
<span th:text="${creneau.client}">Client</span>
</td>
<td>
<a th:if="${creneau.action}==1" th:href="@{'javascript:reserverCreneau('+${creneau.id}+')'}" th:text="${creneau.commande}"
class="status-metro status-active">Réserver
</a>
<a th:if="${creneau.action}==2" th:href="@{'javascript:supprimerRv('+${creneau.idRv}+')'}" th:text="${creneau.commande}"
class="status-metro status-suspended">Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- reserva -->
<section th:include="resa" />
</th:block>
<!-- inicialização da página -->
<script th:inline="javascript">
/*<![CDATA[*/
// inicialização da página
initAgenda();
/*]]>*/
</script>
</body>
</html>
O modelo desta vista tem apenas um elemento:
- [agenda] (linha 4): um modelo um pouco complexo, criado especificamente para a visualização da agenda;
Possui os seguintes gestores de eventos:
evt | gestor |
clique no botão [Supprimer] | |
clique na ligação [Réserver] |
A vista [resa] da linha 47 é a vista que é apresentada quando o utilizador clica num link [Réserver]:

O seu código [resa.xml] é o seguinte:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">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[*/
// inicialização da página
initResa();
/*]]>*/
</script>
</body>
</html>
O seu modelo tem apenas um elemento:
- [clientItems] (linha 24): a lista de clientes;
Possui os seguintes gestores de eventos:
event | gestor |
clique no botão [Annuler] | |
clique no botão [Valider] |
8.6.5.7. A vista [erreurs]
Esta é a vista que é apresentada se a ação solicitada pelo utilizador não tiver sido bem-sucedida:

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>
O seu modelo tem apenas um elemento:
- [erreurs] (linha 8): a lista de erros a apresentar;
A vista não possui um gestor de eventos.
8.6.5.8. Résumé
A tabela seguinte apresenta as vistas e os respetivos modelos:
vista | modelo | gestores de eventos |
navbar-start | ||
jumbotron | ||
login | ||
navbar-run | ||
página inicial | ||
Agenda | ||
resa | ||
erros |
8.6.6. Passo 3: definição das ações
Voltemos à arquitetura do serviço web [Web1]:
![]() |
Vamos agora ver quais as URL que são expostas pelo [Web1] e a sua implementação:
8.6.6.1. As URL expostas pelo serviço [Web1]
São as seguintes:
- uma URL para cada uma das vistas anteriores ou uma composição das mesmas;
- uma URL para adicionar uma RV;
- uma URL para eliminar uma RV;
Todas elas devolvem uma resposta do tipo [Reponse], como se segue:
public class Reponse {
// ----------------- propriedades
// estado 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 estado da resposta: 1 (OK), 2 (erro);
- linha 7: o fluxo HTML das vistas [navbar-start] ou [navbar-run], conforme o caso;
- linha 9: o fluxo HTML da vista [jumbotron];
- linha 13: o fluxo HTML da vista [agenda];
- linha 9: o fluxo HTML das vistas [accueil], [erreurs], [login], conforme o caso;
As URL apresentadas são as seguintes
insere a vista [navbar-start] na vista [Reponse.navbar] | |
coloca a vista [navbar-run] na vista [Reponse.navbar] | |
coloca a vista [accueil] na vista [Reponse.content] | |
coloca a vista [jumbotron] na vista [Reponse.jumbotron] | |
coloca a vista [agenda] na vista [Reponse.agenda] | |
coloca a vista [login] na vista [Reponse.content] | |
| |
coloca a vista [navbar-run] na vista [Reponse.navbar], a vista [jumbotron] na vista [Reponse.jumbotron], a vista [accueil] na vista [Reponse.content], a vista [agenda] na vista [Reponse.agenda] | |
adiciona o compromisso selecionado e insere a nova agenda em [Reponse.agenda] | |
elimina o compromisso selecionado e coloca a nova agenda em [Reponse.agenda] |
8.6.6.2. O singleton [ApplicationModel]
![]() |
A classe [ApplicationModel] é instanciada numa única instância e injetada no controlador da aplicação. O seu código é o seguinte:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
....
}
- linha 6: [ApplicationModel] é um componente Spring;
- linha 7: que implementa a interface da camada [DAO]. Fazemos isto para que as ações não tenham de conhecer a camada [DAO], mas apenas o singleton [ApplicationModel]. A arquitetura de [Web1] passa então a ser a seguinte:
![]() |
Voltemos ao código da classe [ApplicationModel]:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
// 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 elementos 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]. É esta referência que é posteriormente utilizada para implementar a interface [IDao] (linhas 64-80);
- linha 14: injeção da configuração da aplicação;
- linhas 33-37: utilização desta configuração para configurar vários elementos da arquitetura da aplicação;
- linhas 38-46: armazenamos em cache as informações que irão alimentar as listas suspensas de médicos e clientes. Partimos, portanto, do princípio de que, se um médico ou um cliente mudar, a aplicação terá de ser reiniciada. A ideia aqui é mostrar que um singleton do Spring pode servir de 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 apresentado por uma opção da lista suspensa;
8.6.6.3. A classe [BaseController]
![]() |
A classe [BaseController] é a classe pai dos controladores [RdvMedecinsController] e [RdvMedecinsCorsController]. Não era obrigatório criar esta classe pai. Nela foram reunidos métodos utilitários da classe [RdvMedecinsController] que não são essenciais, com exceção de um. Podem ser classificados em três grupos:
- os métodos utilitários;
- os métodos que renderizam as vistas fundidas com os seus modelos;
- o método de inicialização de uma ação
| dois métodos utilitários que fornecem uma lista de mensagens de erro. Já os conhecemos e já os utilizámos; |
| exibe a vista [accueil] sem modelo |
| gera a vista [agenda] e o respetivo modelo |
| gera a vista [login] sem modelo |
| apresenta a resposta ao cliente quando a ação solicitada terminou com um erro |
| O método de inicialização de todas as ações do controlador [RdvMedecinsController] |
Vamos analisar dois desses métodos.
O método [getPartialViewAgenda] é responsável pela geração da vista mais complexa, a da agenda. O 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 o 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 apresentada.
- linha 10: a lista de clientes apresentada quando o utilizador marca uma consulta;
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);
// os horários de marcação
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++;
}
// apresenta-se o modelo da agenda
ViewModelAgenda modelAgenda = new ViewModelAgenda();
modelAgenda.setTitre(titre);
modelAgenda.setCreneaux(modelCréneaux);
return modelAgenda;
}
- linha 6: a agenda tem um título:

ou então:

Vemos que o formato da data depende do idioma. Vamos procurar esse formato nos ficheiros de mensagens (linha 4).
- linhas 11-40: para cada intervalo, devemos apresentar a visualização:
![]()
ou a visualização:
![]()
- linhas 19-20: exibem o intervalo horário;
- linhas 25-28: o caso em que o intervalo está livre. Nesse caso, deve-se apresentar o botão [Réserver];
- linhas 31-36: o caso em que o intervalo está ocupado. Nesse caso, é necessário apresentar tanto o cliente como o botão [Supprimer];
O outro método sobre o qual fornecemos mais explicações é o método [getActionContext]. É chamado no início de cada uma das ações do [RdvMedecinsController]. A sua assinatura é a seguinte:
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController)
Esta função devolve o tipo [ActionContext] seguinte:
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;
Os seus parâmetros são os seguintes:
- [lang]: o idioma solicitado para a ação «en» ou «fr»;
- [origin]: o cabeçalho HTTP [origin] no caso de uma chamada entre domínios;
- [request]: o pedido HTTP em processamento, o que há algum tempo se designa por «ação»;
- [response]: a resposta que será dada a esta 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) {
// língua?
if (lang == null) {
lang = "fr";
}
// local
Locale locale = null;
if (lang.trim().toLowerCase().equals("fr")) {
// francês
locale = new Locale("fr", "FR");
} else {
// tudo o resto 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 localização da ação;
- linha 17: enviam-se os cabeçalhos HTTP necessários para os pedidos entre domínios. Não entramos em pormenores. 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] acedia à base de dados para recuperar tanto os clientes como os médicos. Este acesso pode falhar. Nesse caso, registamos a exceção que ocorre. Na linha 21, recuperamos essa exceção;
- linhas 22-25: se tiver ocorrido uma exceção ao iniciar a aplicação, não é possível realizar qualquer ação. Nesse caso, para qualquer ação, devolvemos um objeto [ActionContext] com as mensagens de erro da exceção;
- linhas 27-20: analisamos o parâmetro [result] para determinar se o valor enviado era válido ou não. Se fosse inválido, devolvemos um objeto [ActionContext] com as mensagens de erro apropriadas;
- linha 32: caso sem erros;
Analisamos agora as ações do controlador [RdvMedecinsController]
8.6.6.4. A ação [/getNavBarStart]
A ação [/getNavBarStart] devolve a vista [navbar-start]. A sua assinatura é a seguinte:
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
Esta ação devolve o tipo [Reponse] seguinte:
public class Reponse {
// ----------------- propriedades
// estado 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 publicado seguinte:
public class PostLang {
// data
@NotNull
private String lang;
...
}
A classe [PostLang] é a classe pai de todos os valores lançados. Com efeito, o cliente deve sempre especificar o idioma em que a ação deve ser executada.
O método [getNavbarStart] está 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);
}
// retorna a vista [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, estes são enviados na resposta ao cliente (linha 12) com o estado 2:
- linhas 15-18: envia-se a vista [navbar-start] com o estado 1:
A seguir, detalhamos apenas as novidades.
8.6.6.5. A ação [/getNavbarRun]
A ação [/getNavBarRun] gera a vista [navbar-run]:
// navbar-run
@RequestMapping(value = "/getNavbarRun", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarRun(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// 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);
}
// é devolvida a vista [navbar-run]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
return reponse;
}
A ação pode gerar dois tipos de resposta:
- a resposta com erro (linhas 10-13):
- a resposta com a vista [navbar-run]:
8.6.6.6. A ação [/getJumbotron]
A ação [/getJumbotron] gera a vista [jumbotron]:
// jumbotron
@RequestMapping(value = "/getJumbotron", method = RequestMethod.POST)
@ResponseBody
public Reponse getJumbotron(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// 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);
}
// é devolvida a vista [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):
- a resposta com a vista [jumbotron]:
8.6.6.7. A ação [/getLogin]
A ação [/getLogin] devolve a vista [login]:
@RequestMapping(value = "/getLogin", method = RequestMethod.POST)
@ResponseBody
public Reponse getLogin(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// 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);
}
// é devolvida a vista [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):
- a resposta com a vista [login]:
8.6.6.8. A ação [/getAccueil]
A ação [/getAccueil] devolve a vista [accueil]. A sua assinatura é a seguinte:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- na linha 3, o valor lançado é do tipo [PostUser], conforme se segue:
public class PostUser extends PostLang {
// dados
@NotNull
private User user;
...
}
- linha 1: a classe [PostUser] estende a classe [PostLang] e, por conseguinte, inclui um idioma;
- linha 4: o utilizador que pretende aceder à vista;
O código de implementação é o seguinte:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// 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 vista [accueil] está protegida
try{
// utilizador
User user = postUser.getUser();
// estão a ser verificadas as credenciais [userName, password]
application.authenticate(user);
}catch(RdvMedecinsException e){
// é devolvido um erro
return getViewErreurs(thymeleafContext, e.getMessages());
}
// é devolvida a vista [accueil]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
- linhas 15-22: note-se que a página [accueil] está protegida e que, por isso, o utilizador tem de estar autenticado;
A ação pode devolver dois tipos de resposta:
- a resposta com erro (linhas 11 e 21):
- a resposta com a visualização [accueil] (linhas 24-27):
8.6.6.9. A ação [/getNavbarRunJumbotronAccueil]
A ação [/getNavbarRunJumbotronAccueil] gera as visualizações [navbar-run, jumbotron, accueil]. Tem a seguinte assinatura:
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser post, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- linha 3: o valor 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 vista [accueil] está protegida
try {
// utilizador
User user = postUser.getUser();
// estão a ser verificadas as credenciais [userName, password]
application.authenticate(user);
} catch (RdvMedecinsException e) {
// é devolvido 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 devolver dois tipos de resposta:
- a resposta com erro (linhas 13, 23):
- a resposta com as visualizações [navbar-run, jumbotron, accueil] (linhas 26-31):
8.6.6.10. A ação [/getAgenda]
A ação [/getAgenda] devolve a vista [agenda]. A sua assinatura é a seguinte:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- linha 3: o valor lançado é do tipo [PostGetAgenda], conforme se 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, por isso, inclui um idioma e um utilizador;
- linha 5: o número do médico cuja agenda se pretende consultar;
- linha 8: o dia da agenda pretendida;
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 envio
if (result != null) {
new PostGetAgendaValidator().validate(postGetAgenda, result);
if (result.hasErrors()) {
// retorna-se a vista [erreurs]
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
...
}
- até à 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 a consulta
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 pode ser anterior à data de hoje
if (strJour.compareTo(strToday) < 0) {
errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
}
}
}
- linha 19: o validador funciona para duas classes: [PostGetAgenda] e [PostValiderRv];
Voltemos ao código da ação [/getAgenda]:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
...
// 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-se a vista [erreurs]
return getViewErreurs(thymeleafContext, e1.getMessages());
} catch (Exception e2) {
// retorna a vista [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- linhas 9-10: com os parâmetros enviados, solicita-se a agenda do médico;
- linhas 12-13: devolve-se a agenda:
- linhas 17, 21: devolve-se uma resposta com erros:
8.6.6.11. A ação [/getNavbarRunJumbotronAccueilAgenda]
A ação [/getNavbarRunJumbotronAccueilAgenda] devolve as vistas [navbar-run, jumbotron, accueil, agenda]. A sua implementação é a seguinte:
@RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// 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 a chamar. Em seguida, analisa-se a ação status da resposta (linha 16). Se for detetado um erro, não se prossegue e devolve-se a resposta;
- linha 20: enviam-se as visualizações solicitadas:
8.6.6.12. A ação [/supprimerRv]
A ação [/supprimerRv] permite eliminar um compromisso. A sua assinatura é a seguinte:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- linha 3: o valor enviado é do tipo [PostSupprimerRv], conforme se segue:
public class PostSupprimerRv extends PostUser {
// dados
@NotNull
private Long idRv;
..
}
- linha 1: a classe [PostSupprimerRv] estende a classe [PostUser] e, por isso, inclui um idioma e um utilizador;
- linha 5: o n.º do compromisso a eliminar;
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 introduzidos
User user = postSupprimerRv.getUser();
long idRv = postSupprimerRv.getIdRv();
// elimina-se o compromisso
AgendaMedecinJour agenda = null;
try {
// recuperar
Rv rv = application.getRvById(user, idRv);
Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
long idMedecin = creneau.getIdMedecin();
Date jour = rv.getJour();
// elimina-se a consulta associada
application.supprimerRv(user, idRv);
// regenera-se 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-se a vista [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// retorna-se a vista [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- linha 22: recupera-se a consulta a eliminar. Se esta não existir, ocorre uma exceção;
- linhas 23-25: a partir deste compromisso, identificam-se o médico e o dia em questão. Estas informações são necessárias para regenerar a agenda do médico;
- linha 27: a consulta é eliminada;
- linha 29: solicita-se a nova agenda do médico. Isto é importante. Para além do horário que acabou de ser libertado, outros utilizadores da aplicação podem ter feito alterações na agenda. É importante devolver ao utilizador a versão mais recente da mesma;
- linhas 31-34: devolve-se a agenda:
8.6.6.13. A ação [/validerRv]
A ação [/validerRv] adiciona um compromisso à agenda de um médico. A sua assinatura é a seguinte:
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- linha 3: o valor inserido é do tipo [PostValiderRv], como se 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, por isso, inclui um idioma e um utilizador;
- linha 5: o n.º do intervalo horário;
- linha 7: o n.º do cliente para quem é feita a reserva;
- linha 10: o dia da marcação;
A implementação da ação é a seguinte:
// validaçã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 do dia da consulta
if (result != null) {
new PostGetAgendaValidator().validate(postValiderRv, result);
if (result.hasErrors()) {
// retorna-se a vista [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 vista [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// retorna-se a vista [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
}
O código é semelhante ao da ação [/supprimerRv].
8.6.7. Passo 4: testes do servidor Spring/Thymeleaf
Vamos agora 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. Iremos enviar variantes da seguinte cadeia jSON:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Este valor lançado inclui informações supérfluas para a maioria das ações. No entanto, estas são ignoradas pelas ações que as recebem e não provocam erros. Este valor lançado tem a vantagem de abranger os diferentes valores a lançar.
8.6.7.2. A ação [/getNavbarStart]
![]() |
- em [1], a ação testada;
- em [2], o valor lançado;
- em [3], o valor lançado é uma cadeia de caracteres jSON;
- em [4], a vista [navbar-start] é solicitada em inglês;
O resultado obtido é o seguinte:
![]() |
Recebemos a vista [navbar-start] em inglês (campos destacados).
Agora, vamos cometer um erro. Alteramos o valor do atributo [lang] do valor enviado para null. Recebemos o seguinte resultado:
![]() |
Recebemos uma resposta de erro (estado 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 processo com um utilizador desconhecido:
{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
O resultado obtido é o seguinte:
![]() |
Começamos de novo com um utilizador existente, mas sem autorização para utilizar a aplicação:
{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
O resultado 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 o processo com uma data anterior à de hoje:
![]() |
Começamos de novo 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 acontece com um utilizador 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:
![]() |
Introduzimos 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 na base de dados que o compromisso foi efetivamente eliminado. 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 na base de dados que o compromisso foi efetivamente criado. A nova agenda foi reenviada.
Fazemos o mesmo com um número de intervalo inexistente:
![]() |
Fazemos o mesmo com um número de cliente inexistente:
![]() |
8.6.8. Passo 5: Código do cliente em JavaScript
Voltemos à arquitetura do servidor [Web1]:
![]() |
O cliente [2] do servidor [Web1] é um cliente JavaScript do tipo APU (Aplicação de Página Única):
- o cliente solicita a página de inicialização a um servidor web (não necessariamente o [Web1]);
- solicita as páginas seguintes ao servidor [Web1] através de chamadas Ajax;
Para criar este cliente, vamos utilizar a ferramenta [Webstorm] (ver parágrafo 9.8). Esta ferramenta pareceu-me mais prática do que a STS. A sua principal vantagem é que oferece autocompletar durante a digitação do código, bem como algumas opções da ferramenta refactoring. Isto evita muitos erros.
8.6.8.1. O projeto JS
O projeto JS tem a seguinte estrutura:
![]() |
- em [1], o cliente JS na sua totalidade. [boot.html] é a página inicial. Esta será a única página carregada pelo navegador;
- em [2], as folhas de estilo dos componentes Bootstrap;
- em [3], as poucas imagens utilizadas pela aplicação;
![]() |
- em [4], os scripts JS. É aqui que reside o nosso trabalho;
- em [5], as bibliotecas JS utilizadas: principalmente a jQuery e as dos componentes 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. É implementada pelo ficheiro [ui.js];
- a camada [événements] reúne todos os gestores de eventos da camada [présentation]. É implementada pelo ficheiro [evts.js];
- a camada [DAO] envia as solicitações HTTP para o servidor [Web1]. É implementada pelo ficheiro [dao.js];
8.6.8.3. A camada [présentation]
![]() |
A camada [présentation] é implementada pelo seguinte ficheiro [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 vista
"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 início de sessão é guardada 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 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 gestão de eventos (linha 29),
- [dao] para a camada [DAO] (linha 31);
Esta 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: guardam-se os campos que estarão sempre presentes, independentemente das vistas apresentadas. Isto evita pesquisas repetidas e desnecessárias em jQuery;
- linhas 46-49: memoriza-se localmente a página inicial para poder recriá-la quando o utilizador terminar a sessão e não tiver alterado o idioma;
- linhas 54-83: funções de inicialização dos componentes Bootstrap. Todas elas foram apresentadas na análise destes componentes no parágrafo 8.6.4;
8.6.8.4. As funções utilitárias da camada [événements]
![]() |
Os gestores de eventos foram colocados no ficheiro [evts.js]. Várias funções são utilizadas regularmente pelos gestores de eventos. Apresentamo-las agora:
// 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 estado
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 do 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: é apresentada 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: indica-se que está em curso um processo assíncrono;
- linha 12: a função [evts.stopwaiting] é chamada após uma ação assíncrona [DAO] ter devolvido o seu resultado;
- linha 14: verifica-se que a tarefa assíncrona está concluída;
- linha 15: oculta-se a imagem animada de espera;
- linha 20: a função [evts.showResult] apresenta o resultado [result] de uma ação assíncrona [DAO]. O resultado é um objeto JS com o seguinte formato: {'status':status,'data':data,'sendMeBack':sendMeBack}.
- linhas 47-50: utilizadas se [result.status==2]. Isto acontece quando o servidor [Web1] envia uma resposta com um cabeçalho de erro HTTP (por exemplo, 403 forbidden). Neste caso, [data] é a cadeia jSON enviada pelo servidor para sinalizar o erro;
- linha 25: caso em que se tenha recebido 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 vista [erreurs] é apresentada;
- linhas 31-33: eventual exibição da barra de navegação;
- linhas 34-36: eventual exibição do jumbotron;
- linhas 37-39: eventual exibição do campo [data.content]. Representa, consoante o caso, uma das vistas [accueil, agenda];
- linhas 40-43: se a agenda tiver sido regenerada, recuperam-se certas referências dos seus componentes, para não ter de as procurar sempre que forem necessárias;
- linha 54: a função [evts.showException] tem como função apresentar o texto da exceção contida no seu parâmetro [data];
- linhas 57-58: o texto da exceção é apresentado;
- linha 58: o título da exceção depende do idioma selecionado no momento;
O ficheiro [evts.js] contém mais de 300 linhas de código, que não vou comentar na totalidade. Vou apenas apresentar alguns exemplos para ilustrar o espírito desta camada.
8.6.8.5. Início de sessão de um utilizador

A ligação de um utilizador é assegurada pela seguinte função:
// ------------------------ ligação
evts.connecter = function () {
// recuperam-se os valores a enviar
var login = $("#login").val().trim();
var passwd = $("#passwd").val().trim();
// define-se 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
};
// efetua-se a solicitação
evts.execute([{
"name": "accueil-sans-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- linhas 4-5: recuperam-se o nome de utilizador e a palavra-passe do utilizador;
- linhas 7-8: recupera-se o URL do serviço [Web1]. Este é armazenado tanto na camada [ui] como na camada [dao];
- linhas 10-16: o valor a enviar: o idioma atual e o utilizador que pretende iniciar sessão;
- linhas 17-23: o objeto [sendMeBack] é um objeto que é passado para a função [DAO], que será chamada e que esta deve devolver à função da linha 22. Aqui, o objeto [sendMeBack] encapsula o utilizador que pretende iniciar sessão;
- linhas 25-29: a função [evts.execute] é capaz de executar uma sequência de ações assíncronas. Aqui, é passada uma lista constituída por uma única ação. Os campos desta ação são os seguintes:
- [name]: o nome da ação assíncrona a executar,
- [post]: o valor a enviar para o servidor [Web1],
- [sendMeBack]: o valor que a ação assíncrona deve devolver com o seu resultado;
Antes de detalhar a função [evts.execute], vejamos a função [evts.connecterDone] na linha 22. É a função à qual a função assíncrona [DAO], que foi chamada, deve devolver o seu resultado:
evts.connecterDone = function (result) {
// exibição do resultado
evts.showResult(result);
// ligação bem-sucedida?
if (result.status == 1 && result.data.status == 1) {
// página
ui.page = "accueil-sans-agenda";
// registo do utilizador
ui.user = result.sendMeBack.user;
}
};
- linha 3: o resultado devolvido pelo servidor [Web1] é apresentado;
- linha 5: se este resultado não contiver erros, então é guardada a natureza da nova página (linha 7), bem como o utilizador autenticado (linha 9);
A função [evts.execute] executa uma sequência de ações assíncronas:
// execução de uma sequência de ações
evts.execute = function (actions) {
// trabalho em curso?
if (evts.travailEnCours) {
// não se está a fazer 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 executar;
- linhas 4-7: a execução só é aceite se não houver outra já em curso;
- 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 executar quando todas as ações da sequência tiverem devolvido o seu resultado;
Não vamos detalhar agora a função [dao.doActions]. Vamos analisar outro evento.
8.6.8.6. Mudança de idioma

A mudança de idioma é assegurada 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;
// que 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 se faz nada;
- linha 9: guarda-se o novo idioma;
- 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:
- a chamada [login], em que a página apresentada é a de autenticação,
- a chamada [accueil-sans-agenda], que é a página apresentada logo após uma autenticação bem-sucedida,
- a chamada [accueil-avec-agenda], que é a página apresentada assim que uma primeira agenda for exibida. Posteriormente, esta permanece ativa até o utilizador se desligar;
Vamos abordar o caso da página [accueil-avec-agenda]. Existem três versões desta função:
![]() |
- a versão [ getAccueilAvecAgenda-one] executa uma única ação assíncrona;
- a versão [ getAccueilAvecAgenda-parallel] executa quatro ações assíncronas em paralelo;
- a versão [ getAccueilAvecAgenda-sequence] executa quatro ações assíncronas, uma a seguir à 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 enviar inclui o utilizador conectado, o idioma pretendido, o número do médico cuja agenda se pretende consultar e o dia da agenda pretendida;
- linhas 10-12: o objeto [sendMeBack] é o objeto que será devolvido à função da linha 11. Aqui, não contém qualquer 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 devolvido o seu resultado;
A função [evts.getAccueilAvecAgendaDone] da linha 11 apresenta 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: este resultado é apresentado;
- linha 5: se for um resultado sem erros, regista-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. Estas serão executadas em paralelo;
- linhas 5-13: definição da ação [navbarRun], que recupera a barra de navegação [navbar-run];
- linha 12: a função a executar quando a ação assíncrona [navbarRun] tiver devolvido o seu resultado;
- linhas 15-23: definição da ação [jumbotron], que recupera a vista [jumbotron];
- linha 22: a função a executar quando a ação assíncrona [jumbotron] tiver devolvido o seu resultado;
- linhas 25-34: definição da ação [accueil], que recupera a vista [accueil];
- linha 33: a função a executar quando a ação assíncrona [accueil] tiver devolvido o seu resultado;
- linhas 36-49: definição da ação [agenda], que recupera a vista [jumbotron];
- linha 48: a função a executar quando a ação assíncrona [agenda] tiver devolvido o seu resultado;
8.6.8.9. A função [ getAccueilAvecAgenda-sequence]
Trata-se da seguinte função:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// ações [navbar-run, jumbotron, accueil, agenda] por 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 esta termina, passa-se para a seguinte: [jumbotron], linha 51. Esta ação é então executada por sua vez. Quando termina, passa-se para a seguinte: [accueil], linha 40. Esta é executada por sua vez. Quando esta estiver concluída, passa-se para a seguinte: [agenda], linha 29. Esta é executada por sua vez. Quando estiver concluída, o processo é interrompido, uma vez que a ação [agenda] não tem nenhuma ação seguinte.
8.6.8.10. A camada [DAO]
![]() |
O ficheiro [dao.js] reúne todas as funções da camada [DAO]. Vamos apresentá-las progressivamente:
// 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 consultar;
// ------------------ gestão genérica das 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++) {
// pedido assíncrono 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 executar quando todas as ações tiverem devolvido o 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 é executada após a conclusão da ação que a precede;
- linha 9: existe um objeto [Deferred] no estado [pending];
- linha 10: quando este 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], que é executada no 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);
}
// seguinte?
if (sendMeBack && sendMeBack.next) {
// pedido DAO assíncrono
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 executar;
- 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, tivesse sido especificada uma função [done] para ser executada quando todas as ações da sequência tivessem devolvido o 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 incorporar 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 acabámos de ver, a função que irá processar o resultado da ação assíncrona a ser executada deve ter acesso à função [done]. Para tal, colocamos esta ú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 efetua uma chamada HTTP ao servidor [Web1]. O URL de destino é o URL associado ao nome da ação a executar;
A função [dao.executePost] executa uma chamada HTTP:
// pedido HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
// faz-se 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á abordámos e comentámos esta função. Basta notar, 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 arranque
![]() |

A página de arranque [boot.html] apresenta a vista acima. É a única página carregada diretamente pelo navegador. As restantes são obtidas através de chamadas Ajax. O seu código é o seguinte:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>RdvMedecins</title>
<!-- 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 utilizador -->
<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>
// inicialização da 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á nos deparámos com este tipo de página no capítulo sobre o Bootstrap (parágrafo 8.6.4);
- linhas 99-105: inicialização de alguns elementos da camada [présentation];
- na linha 27, é utilizado o script [getAccueilAvecAgenda-sequence.js]. Ao alterar o script desta linha, obtêm-se três comportamentos diferentes para aceder à página [accueil-avec-agenda]:
- [getAccueilAvecAgenda-one.js] obtém a página com uma única chamada a HTTP,
- O [getAccueilAvecAgenda-parallel.js] obtém a página através de quatro chamadas simultâneas do HTTP,
- [getAccueilAvecAgenda-sequence.js] obtém a página com quatro chamadas sucessivas de HTTP;
8.6.8.12. Tests
Existem várias formas de realizar os testes. Vamos utilizar aqui a ferramenta [Webstorm]:
![]() |
- no [1], abre-se um projeto. Basta indicar a pasta [2] que contém a estrutura estática (HTML, CSS, JS) do site a testar;
![]() |
- em [3], o site estático;
- em [4-5], carrega-se a página [boot.html];
![]() |
- em [5], verifica-se que um servidor incorporado por [Webstorm] serviu a página [boot.html] a partir da porta [63342]. Este é um ponto importante a compreender, pois significa que os scripts da página [boot.html] irão efetuar chamadas entre domínios para o servidor [Web1], que, por sua vez, opera em [localhost:8081]. O navegador que carregou a página [boot.html] sabe que a carregou a partir da página [localhost:63342]. Por isso, não vai aceitar que essa página efetue chamadas para o site [localhost:8081], porque não se trata da mesma porta. Por isso, irá implementar as chamadas entre domínios descritas no parágrafo 8.4.14. Por esta razão, é necessário que a aplicação [Web1] esteja configurada para aceitar essas chamadas entre domínios. É no ficheiro [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 ao leitor a tarefa de testar o cliente JS. Este deve ser capaz de reproduzir as funcionalidades descritas no parágrafo 8.6.3.
Assim que o cliente JS for considerado correto, pode ser implementado na pasta do servidor [Web1] para evitar ter de autorizar pedidos entre domínios:
![]() |
Acima, copiámos o site testado para a pasta [src / main / resources / static]. Em seguida, podemos solicitar o URL e o [http://localhost:8081/boot.html]:

Agora já não precisamos das solicitações entre domínios e podemos escrever no ficheiro de configuração [AppConfig] do servidor [Web1]:
// CORS
private final boolean CORS_ALLOWED=false;
A aplicação acima continuará a funcionar. Se voltarmos à aplicação [Webstorm], esta já não funciona:


Se acedermos à consola de desenvolvimento (Ctrl-Shift-I), vemos a causa do erro:

Trata-se de um erro de pedido entre domínios não autorizado.
8.6.8.13. Conclusion
Implementámos a seguinte arquitetura JS:
![]() |
- as camadas estão bastante bem separadas;
- temos uma aplicação do tipo APU (Aplicação de Página Única). É esta característica que nos permitirá agora gerar uma aplicação nativa para vários dispositivos móveis (Android, IoS, Windows Phone);
- criámos um modelo capaz de executar ações assíncronas em paralelo, em sequência ou uma combinação de ambas;
8.6.9. etapa 6: geração de uma aplicação nativa para Android
A ferramenta [Phonegap] [http://phonegap.com/] permite produzir um executável para dispositivos móveis (Android, IoS, Windows 8, ...) a partir de uma aplicação HTML / JS / CSS. Existem várias formas de atingir este objetivo. Utilizamos a mais simples: uma ferramenta disponível online no site do Phonegap [http://build.phonegap.com/apps]. Esta ferramenta irá «carregar» o ficheiro zip do site estático a converter. A página inicial deve chamar-se [index.html]. Por isso, renomeamos a página [boot.html] para [index.html]:
![]() |
depois comprimimos a pasta, neste caso [rdvmedecins-client-js-03]. Em seguida, acedemos ao site do Phonegap [http://build.phonegap.com/apps]:
![]() |
- antes de [1], poderá ser necessário criar uma conta;
- em [1], começamos;
- em [2], escolhe-se um plano gratuito que permite apenas uma aplicação Phonegap;
![]() |
- em [3], descarregue a aplicação compactada [4];
![]() |
- em [5], atribui-se um nome à aplicação;
- em [6], compila-se a aplicação. Esta operação pode demorar 1 minuto. Aguarde até que os ícones das diferentes plataformas móveis indiquem que a compilação está concluída;
![]() |
- apenas os binários para Android ([7]) e para Windows ([8]) foram gerados;
- clique em [7] para descarregar o ficheiro binário do Android;
![]() |
- em [9], o ficheiro binário [apk] descarregado;
Inicie um emulador [GenyMotion] para um tablet Android (ver parágrafo 9.9):
![]() |
Acima, iniciamos um emulador de tablet com o API 19 do Android. Assim que o emulador estiver a funcionar,
- desbloqueie-o arrastando o fecho (se houver) para o lado e, em seguida, soltando-o;
- com o rato, arraste o ficheiro [PGBuildApp-debug.apk] que descarregou e solte-o no emulador. Este será então instalado e executado;
![]() |
É necessário alterar o URL para [1]. Para tal, numa janela de comando, digite o comando [ipconfig] (linha 1 abaixo), que irá apresentar 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 quer o endereço Wi-Fi IP (linhas 6-9), quer o endereço na rede local IP (linhas 11-17). Em seguida, utilize este endereço IP no URL do servidor web:
![]() |
Feito isto, aceda ao serviço web:
![]() |
Teste a aplicação no emulador. Deve funcionar. Do lado do servidor, é possível autorizar ou não os cabeçalhos CORS na classe [ApplicationModel]:
// CORS
private final boolean CORS_ALLOWED=false;
Isto não tem importância para a aplicação Android. Esta não é executada num navegador. Ora, a exigência dos cabeçalhos CORS provém 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 da aplicação [AngularJS-Spring MVC] do documento [Tutoriel AngularJS / Spring 4] para oURL e [http://tahe.developpez.com/angularjs-spring4/]. É exclusivamente por esta razão 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]. Aumentámos o número de camadas, pelo que vamos perder em termos de desempenho.
A aplicação aqui analisada foi desenvolvida ao longo do tempo em três documentos diferentes:
- [Introduction aux frameworks JSF2, Primefaces et Primefaces mobile], URL e [http://tahe.developpez.com/java/primefaces/]. O caso de estudo tinha sido então desenvolvido com os frameworks JSF2 / Primefaces. O Primefaces é uma biblioteca de componentes com Ajax que evita a necessidade de escrever JavaScript. A aplicação desenvolvida na altura era menos complexa do que a aqui analisada. Tinha uma versão web clássica para computador e uma versão móvel para telemóveis;
- [Tutoriel AngularJS / Spring 4] para o URL [http://tahe.developpez.com/angularjs-spring4/]. A aplicação desenvolvida na altura apresentava as mesmas características que a analisada neste documento. A aplicação também tinha sido portada para Android;
- o presente documento;
Deste trabalho, destacam-se para mim os seguintes pontos:
- a aplicação [Primefaces] foi, de longe, a mais simples de programar e a sua versão web móvel revelou-se eficiente. Não requer conhecimentos de JavaScript. Não é possível portá-la de forma nativa para os OS dos diferentes telemóveis, mas será que isso é necessário? Parece difícil alterar o estilo da aplicação. Na verdade, trabalhamos com as folhas de estilo do Primefaces. Isto pode ser uma desvantagem;
- a aplicação [AngularJS-Spring MVC] foi complexa de escrever. O framework [AngularJS] pareceu-me bastante difícil de compreender quando se pretende dominá-lo. A arquitetura [client Angular] / [service web / jSON implémenté par Spring MVC] é particularmente elegante e eficiente. Esta arquitetura é replicável para qualquer aplicação web. É a arquitetura que me parece mais promissora, pois envolve competências diferentes tanto do lado do cliente como do 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;
- no caso da aplicação desenvolvida 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 criámos é reutilizável noutras aplicações;

























































































































































































































































































