8. Caso di studio
8.1. Introduzione
Proponiamo di realizzare un'applicazione web per la pianificazione degli appuntamenti in uno studio medico. Questo problema è stato affrontato nel documento "AngularJS / Spring 4 Tutorial" all'URL [http://tahe.developpez.com/angularjs-spring4/]. L'architettura di questa applicazione era la seguente:
![]() |
- In [1], un server web fornisce pagine statiche a un browser. Queste pagine contengono un'applicazione AngularJS basata sul modello MVC (Model–View–Controller). Il modello qui comprende sia le viste che il dominio, rappresentato in questo caso dal livello [Services];
- l'utente interagisce con le viste presentate nel browser. Le sue azioni a volte richiederanno l'invio di una query al server Spring 4 [2]. Il server elaborerà la richiesta e restituirà una risposta JSON (JavaScript Object Notation) [3]. Questa risposta verrà utilizzata per aggiornare la vista presentata all'utente.
Proponiamo di prendere questa applicazione e implementarla end-to-end utilizzando Spring MVC. L'architettura diventa quindi la seguente:
![]() |
Il browser si collegherà a un'applicazione [Web 1] implementata con Spring MVC, che recupererà i propri dati da un servizio web [Web 2] anch'esso implementato con Spring MVC.
8.2. Funzionalità dell'applicazione
I lettori sono invitati a esplorare le funzionalità dell'applicazione provandola. Carichiamo i progetti Maven dalla cartella [case-study] in STS:
![]() | ![]() |
Per prima cosa, creeremo il database MySQL 5 [dbrdvmedecins] utilizzando lo strumento [Wamp Server] (vedere la sezione 9.5):
![]() |
- In [1], selezionare lo strumento [phpMyAdmin] da WampServer;
- In [2], selezionare l'opzione [Importa];
![]() |
- In [3], selezionare il file [database/dbrdvmedecins.sql];
- in [4], eseguirlo;
- in [5], il database viene creato.
Successivamente, dobbiamo avviare il server collegato al database. Si tratta del progetto [rdvmedecins-webjson-server]
![]() |
Il server sarà disponibile all'URL [http://localhost:8080]. Questo può essere modificato nel file [application.properties] del progetto:
![]() |
server.port=8080
Le credenziali di accesso al database sono memorizzate nella classe [DomainAndPersistenceConfig] del progetto [rdvmedecins-metier-dao]:
![]() |
// the MySQL data source
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
Se accedi al database MySQL utilizzando credenziali diverse, è qui che devi apportare le modifiche.
Successivamente, proprio come con il server precedente, avviamo il server [rdvmedecins-springthymeleaf-server]:
![]() | ![]() |
Questo server è disponibile per impostazione predefinita all'URL [http://localhost:8081]. Anche in questo caso, è possibile configurarlo nel file [application.properties] del progetto:
server.port=8081
Inoltre, questo server deve conoscere l'URL del server connesso al database. Questa configurazione si trova nella classe [AppConfig] sopra riportata:
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// racine service web / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// timeout en millisecondes
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
Se il primo server è stato avviato su una porta diversa da 8080, è necessario modificare la riga 5.
Quindi, utilizzando un browser, richiedere l'URL [http://localhost:8081/boot.html]:
![]() |
- in [1], la pagina di accesso dell'applicazione;
- nei campi [2] e [3], il nome utente e la password dell'utente che desidera utilizzare l'applicazione. Sono presenti due utenti: admin/admin (nome utente/password) con il ruolo (ADMIN) e user/user con il ruolo (USER). Solo il ruolo ADMIN dispone dell'autorizzazione per utilizzare l'applicazione. Il ruolo USER è presente solo per illustrare la risposta del server in questo caso d'uso;
- in [4], il pulsante che consente di connettersi al server;
- in [5], la lingua dell'applicazione. Sono disponibili due opzioni: francese (impostazione predefinita) e inglese;
- in [6], l'URL del server [rdvmedecins-springthymeleaf-server];
![]() |
- in [1], effettui l'accesso;
![]() |
- una volta effettuato l'accesso, puoi scegliere il medico che desideri consultare [2] e la data dell'appuntamento [3]. Non appena hai selezionato il medico e la data, viene visualizzato automaticamente il calendario:
![]() |
- una volta visualizzato il calendario del medico, è possibile prenotare una fascia oraria [5];
![]() |
- In [6], selezionare il paziente per l'appuntamento e confermare la selezione in [7];
![]() |
Una volta confermato l'appuntamento, si torna automaticamente al calendario, dove il nuovo appuntamento è ora elencato. Questo appuntamento può essere cancellato in un secondo momento [8].
Le funzionalità principali sono state descritte. Sono semplici. Concludiamo con le impostazioni della lingua:

- in [1], si passa dal francese all'inglese;
![]() |
- In [2], la visualizzazione passa all'inglese, compreso il calendario;
8.3. Il database
![]() |
Il database, di seguito denominato [dbrdvmedecins], è un database MySQL5 con le seguenti tabelle:
![]() |
Gli appuntamenti sono gestiti dalle seguenti tabelle:
- [doctors]: contiene l'elenco dei medici dello studio;
- [clients]: contiene l'elenco dei pazienti dello studio;
- [slots]: contiene le fasce orarie di ciascun medico;
- [rv]: contiene l'elenco degli appuntamenti dei medici.
Le tabelle [ruoli], [utenti] e [utenti_ruoli] sono relative all'autenticazione. Per ora non ce ne occuperemo. Le relazioni tra le tabelle che gestiscono gli appuntamenti sono le seguenti:
![]() |
- una fascia oraria appartiene a un medico – un medico ha 0 o più fasce orarie;
- un appuntamento mette in contatto un cliente e un medico tramite la fascia oraria del medico;
- un cliente ha 0 o più appuntamenti;
- una fascia oraria è associata a 0 o più appuntamenti (in giorni diversi).
8.3.1. La tabella [DOCTORS]
Contiene informazioni sui medici gestiti dall'applicazione [RdvMedecins].
![]() | ![]() |
- ID: numero che identifica il medico — chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- LAST_NAME: il cognome del medico
- FIRST_NAME: nome del medico
- TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)
8.3.2. La tabella [CLIENTS]
I clienti dei vari medici sono memorizzati nella tabella [CLIENTI]:
![]() | ![]() |
- ID: numero identificativo del cliente - chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- COGNOME: il cognome del cliente
- NOME: il nome del cliente
- TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)
8.3.3. La tabella [SLOTS]
Elenca le fasce orarie in cui sono disponibili gli appuntamenti:
![]() |
![]() |
- ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- DOCTOR_ID: numero ID che identifica il medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna DOCTORS(ID).
- START_TIME: ora di inizio della fascia oraria
- MSTART: minuto di inizio della fascia oraria
- HFIN: ora di fine della fascia oraria
- MFIN: minuti di fine della fascia oraria
La seconda riga della tabella [SLOTS] (vedi [1] sopra) indica, ad esempio, che la fascia oraria n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (dott.ssa Marie PELISSIER).
8.3.4. La tabella [RV]
Elenca gli appuntamenti prenotati per ciascun medico:
![]() |
- ID: identificatore univoco dell'appuntamento – chiave primaria
- DAY: giorno dell'appuntamento
- SLOT_ID: fascia oraria dell'appuntamento – chiave esterna sul campo [ID] della tabella [SLOTS] – determina sia la fascia oraria che il medico coinvolto.
- CLIENT_ID: ID del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]
Questa tabella ha un vincolo di unicità sui valori delle colonne unite (GIORNO, SLOT_ID):
Se una riga nella tabella [RV] ha il valore (DAY1, SLOT_ID1) per le colonne (DAY, SLOT_ID), questo valore non può comparire in nessun altro punto. In caso contrario, ciò significherebbe che sono stati prenotati due appuntamenti contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database genera un'eccezione SQLException quando ciò si verifica.
La riga con ID pari a 3 (vedi [1] sopra) indica che è stato prenotato un appuntamento per lo slot n. 20 e il cliente n. 4 il 23/08/2006. La tabella [SLOTS] ci dice che lo slot n. 20 corrisponde alla fascia oraria 16:20 – 16:40 e appartiene al medico n. 1 (Sig.ra Marie PELISSIER). La tabella [CLIENTS] ci dice che il cliente n. 4 è la sig.ra Brigitte BISTROU.
8.3.5. Creazione del database
Per creare il database [dbrdvmedecins], viene fornito uno script [dbrdvmedecins.sql] insieme agli esempi in questo documento [1-3]:
![]() |
Utilizziamo lo strumento [PhpMyAdmin] di WampServer:
![]() |
- In [1], selezionare lo strumento [phpMyAdmin] di WampServer;
- in [2], selezionare l'opzione [Importa];
![]() |
- in [3], selezionare il file [database/dbrdvmedecins.sql];
- in [4], eseguirlo;
- in [5], il database viene creato.
8.4. Il servizio Web / JSON
![]() |
Nell'architettura sopra descritta, ci occuperemo ora della realizzazione del servizio web / JSON basato sul framework Spring MVC. Lo scriveremo in diverse fasi:
- in primo luogo, i livelli [business] e [DAO] (Data Access Object). Qui utilizzeremo Spring Data;
- Successivamente, il servizio web JSON senza autenticazione. Qui useremo Spring MVC;
- quindi aggiungeremo il componente di autenticazione utilizzando Spring Security.
Quella che segue è una riproduzione del documento [http://tahe.developpez.com/angularjs-spring4/] con alcune modifiche.
8.4.1. Introduzione a Spring Data
Implementeremo il livello [DAO] del progetto utilizzando Spring Data, un componente dell'ecosistema Spring.
![]() |
Il sito web di Spring offre numerosi tutorial per iniziare a utilizzare Spring [http://spring.io/guides]. Ne useremo uno per introdurre Spring Data. A tal fine, utilizzeremo Spring Tool Suite (STS).
![]() |
- In [1], importiamo uno dei tutorial da [spring.io/guides];
![]() |
- In [2], selezioniamo il tutorial [Accessing Data JPA], che illustra come accedere a un database utilizzando Spring Data;
- In [3], selezioniamo un progetto configurato da Maven;
- in [4], il tutorial è disponibile in due forme: [initial], che è una versione vuota da compilare seguendo il tutorial, oppure [complete], che è la versione finale del tutorial. Scegliamo quest'ultima;
- In [5], è possibile scegliere di visualizzare il tutorial in un browser;
- In [6], il progetto finale.
8.4.1.1. La configurazione Maven del progetto
Le dipendenze Maven del progetto sono configurate nel file [pom.xml]:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!-- use UTF-8 for everything -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- righe 5–9: definiscono un progetto Maven padre. Questo progetto definisce la maggior parte delle dipendenze del progetto. Potrebbero essere sufficienti, nel qual caso non vengono aggiunte dipendenze aggiuntive, oppure potrebbero non esserlo, nel qual caso vengono aggiunte le dipendenze mancanti;
- righe 12–15: definiscono una dipendenza da [spring-boot-starter-data-jpa]. Questo artefatto contiene le classi Spring Data;
- Righe 16–19: definiscono una dipendenza dal DBMS H2, che consente di creare e gestire database in memoria.
Diamo un'occhiata alle classi fornite da queste dipendenze:
![]() | ![]() | ![]() |
Ce ne sono molte:
- alcune appartengono all'ecosistema Spring (quelle che iniziano con spring);
- altri fanno parte dell'ecosistema Hibernate (Hibernate, JBoss), e qui utilizziamo l'implementazione JPA;
- altri sono librerie di test (JUnit, Hamcrest);
- altri sono librerie di logging (log4j, logback, slf4j);
Le terremo tutte. Per un'applicazione di produzione, dovrebbero essere mantenute solo quelle necessarie.
Alla riga 26 del file [pom.xml], troviamo la riga:
<start-class>hello.Application</start-class>
Questa riga è collegata alle seguenti righe:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Righe 6–9: Il [spring-boot-maven-plugin] consente di generare il JAR eseguibile dell'applicazione. La riga 26 del file [pom.xml] specifica quindi la classe eseguibile di questo JAR.
8.4.1.2. Il livello [JPA]
L'accesso al database viene gestito tramite un livello [JPA], la Java Persistence API:
![]() |
![]() |
L'applicazione è semplice e gestisce le entità [Cliente]. La classe [Cliente] fa parte del livello [JPA] ed è la seguente:
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);
}
}
Un cliente ha un ID [id], un nome [firstName] e un cognome [lastName]. Ogni istanza [Customer] rappresenta una riga in una tabella del database.
- riga 8: annotazione JPA che garantisce che la persistenza delle istanze [Customer] (Creazione, Lettura, Aggiornamento, Eliminazione) sarà gestita da un'implementazione JPA. In base alle dipendenze Maven, possiamo vedere che viene utilizzata l'implementazione JPA/Hibernate;
- Righe 11–12: annotazioni JPA che associano il campo [id] alla chiave primaria della tabella [Customer]. La riga 12 indica che l'implementazione JPA utilizzerà il metodo di generazione della chiave primaria specifico del DBMS in uso, in questo caso H2;
Non ci sono altre annotazioni JPA. Verranno quindi utilizzati i valori predefiniti:
- la tabella [Customer] prenderà il nome dalla classe, ovvero [Customer];
- le colonne di questa tabella avranno i nomi dei campi della classe: [id, firstName, lastName], tenendo presente che nei nomi delle colonne della tabella non viene fatta distinzione tra maiuscole e minuscole;
Si noti che l'implementazione JPA utilizzata non viene mai menzionata.
8.4.1.3. Il livello [DAO]
![]() |
![]() |
La classe [CustomerRepository] implementa il livello [DAO]. Il suo codice è il seguente:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
Si tratta quindi di un'interfaccia e non di una classe (riga 7). Essa estende l'interfaccia [CrudRepository], un'interfaccia Spring Data (riga 5). Questa interfaccia è parametrizzata da due tipi: il primo è il tipo degli elementi gestiti, in questo caso il tipo [Customer]; il secondo è il tipo della chiave primaria degli elementi gestiti, in questo caso un tipo [Long]. L'interfaccia [CrudRepository] è la seguente:
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();
}
Questa interfaccia definisce le operazioni CRUD (Create – Read – Update – Delete) che possono essere eseguite su un tipo JPA T:
- Riga 8: Il metodo save consente di salvare un'entità T nel database. Salva l'entità utilizzando la chiave primaria assegnatale dal DBMS. Consente inoltre di aggiornare un'entità T identificata dal suo ID di chiave primaria. La scelta tra queste due azioni dipende dal valore dell'ID della chiave primaria: se è nullo, viene eseguita l'operazione di salvataggio; altrimenti, viene eseguita l'operazione di aggiornamento;
- riga 10: come sopra, ma per un elenco di entità;
- riga 12: il metodo findOne recupera un'entità T identificata dal suo ID chiave primaria;
- riga 22: il metodo delete consente di eliminare un'entità T identificata dal suo ID chiave primaria;
- righe 24–28: varianti del metodo [delete];
- riga 16: il metodo [findAll] recupera tutte le entità T persistenti;
- riga 18: come sopra, ma limitato alle entità per le quali è stato fornito un elenco di identificatori;
Torniamo all'interfaccia [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);
}
- La riga 9 consente di recuperare un [Cliente] in base al suo [cognome];
E questo è tutto per il livello [DAO]. Non esiste una classe di implementazione per l'interfaccia precedente. Viene generata in fase di esecuzione da [Spring Data]. I metodi dell'interfaccia [CrudRepository] vengono implementati automaticamente. Per i metodi aggiunti all'interfaccia [CustomerRepository], dipende. Torniamo alla definizione di [Customer]:
private long id;
private String firstName;
private String lastName;
Il metodo alla riga 9 viene implementato automaticamente da [Spring Data] poiché fa riferimento al campo [lastName] (riga 3) di [Customer]. Quando incontra un metodo [findBySomething] nell'interfaccia da implementare, Spring Data lo implementa utilizzando la seguente query JPQL (Java Persistence Query Language):
Pertanto, il tipo T deve avere un campo denominato [something]. Di conseguenza, il metodo
sarà implementato con un codice simile al seguente:
return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()
dove [em] si riferisce al contesto di persistenza JPA. Ciò è possibile solo se la classe [Customer] dispone di un campo denominato [lastName], cosa che effettivamente avviene.
In conclusione, nei casi semplici, Spring Data ci permette di implementare il livello [DAO] con una semplice interfaccia.
8.4.1.4. Il livello [console]
![]() |
![]() |
La classe [Application] è la seguente:
package hello;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// save a couple of customers
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// fetch all customers
Iterable<Customer> customers = repository.findAll();
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : customers) {
System.out.println(customer);
}
System.out.println();
// fetch an individual customer by ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// fetch customers by last name
List<Customer> bauers = repository.findByLastName("Bauer");
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : bauers) {
System.out.println(bauer);
}
context.close();
}
}
- Riga 10: indica che la classe viene utilizzata per configurare Spring. Le versioni recenti di Spring possono infatti essere configurate in Java anziché in XML. Entrambi i metodi possono essere utilizzati contemporaneamente. Nel codice di una classe annotata con [Configuration], normalmente si trovano i bean Spring, ovvero le definizioni delle classi da istanziare. Qui non sono definiti bean. È importante notare che quando si lavora con un DBMS, devono essere definiti vari bean Spring:
- un [EntityManagerFactory] che definisce l'implementazione JPA da utilizzare,
- un [DataSource] che definisce la fonte dati da utilizzare,
- un [TransactionManager] che definisce il gestore delle transazioni da utilizzare;
Qui, nessuno di questi bean è definito.
- Riga 11: L'annotazione [EnableAutoConfiguration] è un'annotazione del progetto [Spring Boot] (righe 5–6). Questa annotazione indica a Spring Boot, tramite la classe [SpringApplication] (riga 16), di configurare l'applicazione in base alle librerie presenti nel suo classpath. Poiché le librerie Hibernate si trovano nel classpath, il bean [entityManagerFactory] verrà implementato con Hibernate. Poiché la libreria DBMS H2 si trova nel classpath, il bean [dataSource] verrà implementato con H2. Nel bean [dataSource] dobbiamo anche definire il nome utente e la password. Qui, Spring Boot utilizzerà l'amministratore H2 predefinito, che non ha password. Poiché la libreria [spring-tx] si trova nel classpath, verrà utilizzato il gestore delle transazioni di Spring.
Inoltre, la directory contenente la classe [Application] verrà scansionata alla ricerca di bean riconosciuti implicitamente da Spring o definiti esplicitamente dalle annotazioni Spring. Pertanto, verranno ispezionate le classi [Customer] e [CustomerRepository]. Poiché la prima ha l'annotazione [@Entity], verrà catalogata come entità da gestire da Hibernate. Poiché la seconda estende l'interfaccia [CrudRepository], verrà registrata come bean Spring.
Esaminiamo le righe 16–17 del codice:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- Riga 16: Viene eseguito il metodo statico [run] della classe [SpringApplication] nel progetto Spring Boot. Il suo parametro è la classe che presenta un'annotazione [Configuration] o [EnableAutoConfiguration]. A questo punto avrà luogo tutto ciò che è stato spiegato in precedenza. Il risultato è un contesto applicativo Spring, ovvero un insieme di bean gestiti da Spring;
- Riga 17: Richiediamo un bean che implementi l'interfaccia [CustomerRepository] da questo contesto Spring. Qui, recuperiamo la classe generata da Spring Data per implementare questa interfaccia.
Le operazioni seguenti utilizzano semplicemente i metodi del bean che implementa l'interfaccia [CustomerRepository]. Si noti alla riga 50 che il contesto viene chiuso. L'output della console è il seguente:
- righe 1-8: il logo del progetto Spring Boot;
- riga 9: viene eseguita la classe [hello.Application];
- riga 10: [AnnotationConfigApplicationContext] è una classe che implementa l'interfaccia [ApplicationContext] di Spring. Si tratta di un contenitore di bean;
- riga 11: il bean [entityManagerFactory] è implementato utilizzando la classe [LocalContainerEntityManagerFactory], una classe Spring;
- riga 15: compare [Hibernate]. Si tratta dell'implementazione JPA che è stata scelta;
- riga 19: un dialetto Hibernate è la variante SQL da utilizzare con il DBMS. Qui, il dialetto [H2Dialect] indica che Hibernate funzionerà con il DBMS H2;
- righe 21–22: viene creato il database. Viene creata la tabella [CUSTOMER]. Ciò significa che Hibernate è stato configurato per generare tabelle dalle definizioni JPA, in questo caso la definizione JPA della classe [Customer];
- righe 27–31: vengono inseriti i cinque clienti;
- righe 33–35: risultato del metodo [findOne] dell'interfaccia;
- righe 37–40: risultati del metodo [findByLastName];
- Righe 41 e seguenti: log dalla chiusura del contesto Spring.
8.4.1.5. Configurazione manuale del progetto Spring Data
Duplichiamo il progetto precedente nel progetto [gs-accessing-data-jpa-2]:
![]() |
In questo nuovo progetto non faremo affidamento sulla configurazione automatica fornita da Spring Boot. Lo configureremo manualmente. Ciò può essere utile se le configurazioni predefinite non soddisfano le nostre esigenze.
Per prima cosa, specificheremo le dipendenze necessarie nel file [pom.xml]:
...
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring transactions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.1.10.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.178</version>
</dependency>
<!-- Commons DBCP -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
...
</project>
- righe 2–18: librerie principali di Spring;
- righe 19–29: librerie Spring per la gestione delle transazioni del database;
- righe 30–35: la libreria Spring per lavorare con un ORM (Object Relational Mapper);
- righe 36–41: Spring Data utilizzato per accedere al database;
- righe 42–47: Spring Boot per avviare l'applicazione;
- righe 54–59: il DBMS H2;
- righe 60–70: i database sono spesso utilizzati con pool di connessioni, che evitano di aprire e chiudere ripetutamente le connessioni. In questo caso, l'implementazione utilizzata è [commons-dbcp];
Sempre in [pom.xml], modifichiamo il nome della classe eseguibile:
<properties>
...
<start-class>demo.console.Main</start-class>
</properties>
Nel nuovo progetto, l'entità [Customer] e l'interfaccia [CustomerRepository] rimangono invariate. Modificheremo la classe [Application], che verrà suddivisa in due classi:
- [Config], che sarà la classe di configurazione:
- [Main], che sarà la classe eseguibile;
![]() |
La classe eseguibile [Main] è la stessa di prima, senza le annotazioni di configurazione:
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();
}
}
- riga 12: la classe [Main] non presenta più alcuna annotazione di configurazione;
- riga 16: l'applicazione viene avviata con Spring Boot. Il parametro [Config.class] è la nuova classe di configurazione del progetto;
La classe [Config] che configura il progetto è la seguente:
package demo.config;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
// h2 data source
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// the provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("demo.entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transaction manager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- riga 22: l'annotazione [@Configuration] rende la classe [Config] una classe di configurazione Spring;
- riga 21: l'annotazione [@EnableJpaRepositories] specifica le directory in cui si trovano le interfacce [CrudRepository] di Spring Data. Queste interfacce diventeranno componenti Spring e saranno disponibili nel suo contesto;
- riga 20: l'annotazione [@EnableTransactionManagement] indica che i metodi delle interfacce [CrudRepository] devono essere eseguiti all'interno di una transazione;
- riga 19: l'annotazione [@EntityScan] specifica le directory in cui devono essere cercate le entità JPA. Qui è stata commentata perché questa informazione è stata fornita esplicitamente alla riga 50. Questa annotazione dovrebbe essere presente se si utilizza la modalità [@EnableAutoConfiguration] e le entità JPA non si trovano nella stessa directory della classe di configurazione;
- riga 18: l'annotazione [@ComponentScan] consente di specificare le directory in cui cercare i componenti Spring. I componenti Spring sono classi contrassegnate da annotazioni Spring quali @Service, @Component, @Controller, ecc. In questo caso, non ce ne sono altre oltre a quelle definite all'interno della classe [Config], quindi l'annotazione è stata commentata;
- Righe 25–33: definiscono la fonte dati, il database H2. È l'annotazione @Bean alla riga 25 che rende l'oggetto creato da questo metodo un componente gestito da Spring. Il nome del metodo qui può essere qualsiasi cosa. Tuttavia, deve essere denominato [dataSource] se l'EntityManagerFactory alla riga 47 è assente e definita tramite configurazione automatica;
- riga 29: il database si chiamerà [demo] e verrà generato nella cartella del progetto;
- Righe 36–43: definiscono l'implementazione JPA utilizzata, in questo caso un'implementazione Hibernate. Il nome del metodo può essere qualsiasi cosa;
- riga 39: nessun log SQL;
- riga 30: il database verrà creato se non esiste;
- righe 46–54: definiscono l'EntityManagerFactory che gestirà la persistenza JPA. Il metodo deve essere denominato [entityManagerFactory];
- riga 47: il metodo riceve due parametri dei tipi dei due bean definiti in precedenza. Questi saranno poi costruiti e iniettati da Spring come parametri del metodo;
- riga 49: imposta l'implementazione JPA da utilizzare;
- riga 50: specifica le directory in cui si trovano le entità JPA;
- riga 51: imposta l'origine dati da gestire;
- righe 57–62: il gestore delle transazioni. Il metodo deve essere denominato [transactionManager]. Riceve il bean delle righe 46–54 come parametro;
- riga 60: il gestore delle transazioni è associato all'EntityManagerFactory;
I metodi precedenti possono essere definiti in qualsiasi ordine.
L'esecuzione del progetto produce gli stessi risultati. Nella cartella del progetto compare un nuovo file, il file del database H2:
![]() |
Finalmente possiamo fare a meno di Spring Boot. Creiamo una seconda classe eseguibile [Main2]:
![]() |
La classe [Main2] contiene il seguente codice:
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();
}
}
- Riga 15: La classe di configurazione [Config] viene ora utilizzata dalla classe Spring [AnnotationConfigApplicationContext]. Come si vede alla riga 5, non ci sono più dipendenze da Spring Boot.
L'esecuzione produce gli stessi risultati di prima.
8.4.1.6. Creazione di un archivio eseguibile
Per creare un archivio eseguibile del progetto, procedere come segue:
![]() |
- in [1]: creare una configurazione di runtime;
- in [2]: di tipo [Applicazione Java]
- in [3]: specificare il progetto da eseguire (utilizzare il pulsante Sfoglia);
- in [4]: specificare la classe da eseguire;
- in [5]: il nome della configurazione di esecuzione – può essere qualsiasi cosa;
![]() |
- in [6]: esporta il progetto;
- in [7]: come archivio JAR eseguibile;
- in [8]: specificare il percorso e il nome del file eseguibile da creare;
- in [9]: il nome della configurazione di esecuzione creata in [5];
Una volta fatto ciò, aprire una console nella cartella contenente l'archivio eseguibile:
L'archivio viene eseguito come segue:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
I risultati visualizzati nella console sono i seguenti:
8.4.1.7. Crea un nuovo progetto Spring Data
Per creare un modello di progetto Spring Data, segui questi passaggi:
![]() |
- In [1], creare un nuovo progetto;
- in [2]: selezionare [Spring Starter Project];
- Il progetto generato sarà un progetto Maven. In [3], specificare il nome del gruppo di progetto;
- In [4], specificare il nome dell'artefatto (un file JAR in questo caso) che verrà creato al momento della compilazione del progetto;
- in [5]: specificare il pacchetto della classe eseguibile che verrà creata nel progetto;
- in [6]: il nome Eclipse del progetto – può essere qualsiasi cosa (non deve necessariamente essere uguale a [4]);
- in [7]: specificare che si sta creando un progetto con un livello [JPA]. Le dipendenze richieste per un progetto di questo tipo saranno quindi incluse nel file [pom.xml];
![]() |
- in [8]: il progetto creato;
Il file [pom.xml] include le dipendenze necessarie per un progetto JPA:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- righe 9–12: dipendenze richieste per JPA — includeranno [Spring Data];
- righe 13–17: dipendenze necessarie per i test JUnit integrati con Spring;
La classe eseguibile [Application] non fa nulla ma è preconfigurata:
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);
}
}
La classe di test [ApplicationTests] non fa nulla, ma è preconfigurata:
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() {
}
}
- Riga 9: l'annotazione [@SpringApplicationConfiguration] consente di utilizzare il file di configurazione [Application]. La classe di test potrà così beneficiare di tutti i bean definiti in questo file;
- riga 8: l'annotazione [@RunWith] consente l'integrazione di Spring con JUnit: la classe potrà essere eseguita come test JUnit. [@RunWith] è un'annotazione JUnit (riga 4), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 6);
Ora che disponiamo di uno scheletro di applicazione JPA, possiamo completarlo per scrivere il livello di persistenza lato server della nostra applicazione di gestione degli appuntamenti.
8.4.2. Il progetto server di Eclipse
![]() |
![]() |
Le componenti principali del progetto sono le seguenti:
- [pom.xml]: il file di configurazione Maven del progetto;
- [rdvmedecins.entities]: le entità JPA;
- [rdvmedecins.repositories]: interfacce Spring Data per l'accesso alle entità JPA;
- [rdvmedecins.metier]: il livello [business];
- [rdvmedecins.domain]: le entità gestite dal livello [business];
- [rdvmdecins.config]: le classi di configurazione del livello di persistenza;
- [rdvmedecins.boot]: un'applicazione console di base;
8.4.3. La configurazione Maven
![]() | ![]() | ![]() |
Il file [pom.xml] del progetto è il seguente:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- driver JDBC / MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- mapper jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Googe Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- use UTF-8 for everything -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>rdvmedecins.boot.Boot</start-class>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
- righe 8–12: Il progetto si basa sul progetto padre [spring-boot-starter-parent]. Per le dipendenze già presenti nel progetto padre, non viene specificata alcuna versione. Verrà utilizzata la versione definita nel progetto padre. Le altre dipendenze sono dichiarate come di consueto;
- righe 15–18: per Spring Data;
- righe 20–24: per i test JUnit;
- righe 26–29: per la libreria Spring Security, il cui livello [DAO] utilizza una delle classi di crittografia delle password;
- righe 31–34: driver JDBC per il DBMS MySQL5;
- righe 36–39: pool di connessioni JDBC di Tomcat. Un pool di connessioni raccoglie le connessioni aperte a un database. Quando il codice desidera aprire una connessione, ne richiede una dal pool. Quando il codice chiude la connessione, questa non viene chiusa, ma restituita al pool. Tutto ciò avviene in modo trasparente a livello di codice. Le prestazioni risultano migliorate poiché l'apertura e la chiusura ripetute di una connessione richiedono tempo. In questo caso, il pool di connessioni stabilisce un certo numero di connessioni al database al momento dell'istanziazione. Successivamente, non vi è alcuna apertura o chiusura di connessioni, a meno che il numero di connessioni memorizzate nel pool non si riveli insufficiente. In tal caso, il pool crea automaticamente nuove connessioni;
- righe 41–44: libreria Jackson per la gestione di JSON;
- righe 46–50: libreria Google Collections;
8.4.4. Entità JPA
![]() |
Le entità JPA sono gli oggetti che incapsulano le righe delle tabelle del database.
![]() |
La classe [AbstractEntity] è la classe padre delle entità [Person, Slot, Appointment]. La sua definizione è la seguente:
package rdvmedecins.entities;
import java.io.Serializable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
@Version
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// initialization
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1) || entity==null) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id.longValue() == other.id.longValue();
}
// getters and setters
..
}
- riga 11: l'annotazione [@MappedSuperclass] indica che la classe annotata è un genitore delle entità JPA [@Entity];
- righe 15–17: definiscono la chiave primaria [id] per ciascuna entità. È l'annotazione [@Id] che rende il campo [id] una chiave primaria. L'annotazione [@GeneratedValue(strategy = GenerationType.IDENTITY)] indica che il valore di questa chiave primaria è generato dal DBMS e che viene applicata la modalità di generazione [IDENTITY]. Per il DBMS MySQL, ciò significa che le chiavi primarie saranno generate dal DBMS con l'attributo [AUTO_INCREMENT]
- Righe 18–19: definiscono la versione di ciascuna entità. L'implementazione JPA incrementerà questo numero di versione ogni volta che l'entità viene modificata. Questo numero viene utilizzato per impedire aggiornamenti simultanei dell'entità da parte di due utenti diversi: due utenti, U1 e U2, leggono l'entità E con un numero di versione pari a V1. U1 modifica E e salva questa modifica nel database: il numero di versione passa quindi a V1+1. A sua volta, U2 modifica E e salva questa modifica nel database: riceverà un'eccezione perché la sua versione (V1) differisce da quella nel database (V1+1);
- righe 29–33: il metodo [build] inizializza i due campi di [AbstractEntity]. Questo metodo restituisce un riferimento all'istanza di [AbstractEntity] così inizializzata;
- righe 36–44: il metodo [equals] della classe viene sovrascritto: due entità sono considerate uguali se hanno lo stesso nome di classe e lo stesso identificatore id;
- righe 21–26: quando si sovrascrive il metodo [equals] di una classe, deve essere sovrascritto anche il suo metodo [hashCode] (righe 21–26). La regola è che due entità ritenute uguali dal metodo [equals] devono avere anche lo stesso [hashCode]. Qui, l’[hashCode] di un’entità è uguale alla sua chiave primaria [id]. L'[hashCode] di una classe viene utilizzato, in particolare, nella gestione di dizionari i cui valori sono istanze della classe;
L'entità [Person] è la classe padre delle entità [Doctor] e [Client]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Personne extends AbstractEntity {
private static final long serialVersionUID = 1L;
// attributes of a person
@Column(length = 5)
private String titre;
@Column(length = 20)
private String nom;
@Column(length = 20)
private String prenom;
// default builder
public Personne() {
}
// builder with parameters
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// getters and setters
...
}
- riga 6: l'annotazione [@MappedSuperclass] indica che la classe annotata è una classe padre delle entità JPA [@Entity];
- righe 10–15: una persona ha un titolo (Ms.), un nome (Jacqueline) e un cognome (Tatou). Non vengono fornite informazioni sulle colonne della tabella. Per impostazione predefinita, avranno quindi gli stessi nomi dei campi;
L'entità [Medecin] è la seguente:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
private static final long serialVersionUID = 1L;
// default builder
public Medecin() {
}
// builder with parameters
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
- riga 6: la classe è un'entità JPA;
- riga 7: associata alla tabella [DOCTORS] nel database;
- riga 8: l'entità [Doctor] deriva dall'entità [Person];
Un medico può essere inizializzato come segue:
Se, inoltre, vogliamo assegnargli un ID e una versione, possiamo scrivere:
dove il metodo [build] è quello definito in [AbstractEntity].
L'entità [Client] è la seguente:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "clients")
public class Client extends Personne {
private static final long serialVersionUID = 1L;
// default builder
public Client() {
}
// builder with parameters
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// identity
public String toString() {
return String.format("Client[%s]", super.toString());
}
}
- riga 6: la classe è un'entità JPA;
- riga 7: associata alla tabella [CLIENTS] nel database;
- riga 8: l'entità [Client] deriva dall'entità [Person];
L'entità [TimeSlot] è la seguente:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// default builder
public Creneau() {
}
// builder with parameters
public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
this.medecin = medecin;
this.hdebut = hdebut;
this.mdebut = mdebut;
this.hfin = hfin;
this.mfin = mfin;
}
// toString
public String toString() {
return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
}
// foreign key
public long getIdMedecin() {
return idMedecin;
}
// setters - getters
...
}
- riga 10: la classe è un'entità JPA;
- riga 11: associata alla tabella [CRENEAUX] nel database;
- riga 12: l'entità [Creneau] deriva dall'entità [AbstractEntity] e quindi eredita i campi [id] e [version];
- riga 16: ora di inizio dello slot (14);
- riga 17: minuti di inizio dello slot (20);
- riga 18: ora di fine dello slot (14);
- riga 19: minuti di fine dello slot (40);
- righe 22–24: il medico a cui appartiene lo slot. La tabella [CRENEAUX] ha una chiave esterna sulla tabella [MEDECINS]. Questa relazione è rappresentata dalle righe 22–24;
- riga 22: l'annotazione [@ManyToOne] indica una relazione molti-a-uno (slot a medico). L'attributo [fetch=FetchType.LAZY] indica che quando viene richiesta un'entità [Slot] dal contesto di persistenza e questa deve essere recuperata dal database, l'entità [Doctor] non viene restituita insieme ad essa. Il vantaggio di questa modalità è che l'entità [Doctor] viene recuperata solo se lo sviluppatore la richiede. Ciò consente di risparmiare memoria e migliora le prestazioni;
- riga 23: specifica il nome della colonna della chiave esterna nella tabella [CRENEAUX];
- righe 27–28: la chiave esterna nella tabella [MEDECINS];
- riga 27: la colonna [ID_MEDECIN] è già stata utilizzata alla riga 23. Ciò significa che può essere modificata in due modi diversi, cosa non consentita dallo standard JPA. Aggiungiamo quindi gli attributi [insertable = false, updatable = false], assicurandoci che la colonna sia di sola lettura;
L'entità [Rv] è la seguente:
package rdvmedecins.entities;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of an Rv
@Temporal(TemporalType.DATE)
private Date jour;
// an appointment is linked to a customer
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// an appointment is linked to a time slot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// foreign keys
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
// default builder
public Rv() {
}
// with parameters
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// foreign keys
public long getIdCreneau() {
return idCreneau;
}
public long getIdClient() {
return idClient;
}
// getters and setters
...
}
- riga 14: la classe è un'entità JPA;
- riga 15: associata alla tabella [RV] nel database;
- riga 16: l'entità [Rv] deriva dall'entità [AbstractEntity] e quindi eredita i campi [id] e [version];
- riga 21: la data dell'appuntamento;
- riga 20: il tipo Java [Date] contiene sia una data che un'ora. Qui specifichiamo che viene utilizzata solo la data;
- righe 24–26: il cliente per il quale è stato fissato questo appuntamento. La tabella [RV] ha una chiave esterna sulla tabella [CLIENTS]. Questa relazione è rappresentata dalle righe 24–26;
- righe 29–31: la fascia oraria dell'appuntamento. La tabella [RV] ha una chiave esterna sulla tabella [CRENEAUX]. Questa relazione è rappresentata dalle righe 29–31;
- righe 34–35: la chiave esterna [idClient];
- righe 36–37: la chiave esterna [idCreneau];
8.4.5. Il livello [DAO]
![]() |
Implementeremo il livello [DAO] utilizzando Spring Data:
![]() |
Il livello [DAO] è implementato utilizzando quattro interfacce Spring Data:
- [ClientRepository]: fornisce l'accesso alle entità JPA [Client];
- [CreneauRepository]: fornisce l'accesso alle entità JPA [Creneau];
- [MedecinRepository]: fornisce l'accesso alle entità JPA [Medecin];
- [RvRepository]: fornisce l'accesso alle entità JPA [Rv];
L'interfaccia [MedecinRepository] è la seguente:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
- Riga 7: L'interfaccia [MedecinRepository] eredita semplicemente i metodi dall'interfaccia [CrudRepository] senza aggiungerne altri;
L'interfaccia [ClientRepository] è la seguente:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
- Riga 7: L'interfaccia [ClientRepository] eredita semplicemente i metodi dall'interfaccia [CrudRepository] senza aggiungerne altri;
L'interfaccia [CreneauRepository] è la seguente:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Creneau;
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
// liste des créneaux horaires d'un médecin
@Query("select c from Creneau c where c.medecin.id=?1")
Iterable<Creneau> getAllCreneaux(long idMedecin);
}
- riga 8: l'interfaccia [CreneauRepository] eredita i metodi dell'interfaccia [CrudRepository];
- righe 10-11: il metodo [getAllCreneaux] recupera le fasce orarie disponibili di un medico;
- riga 11: il parametro è l'ID del medico. Il risultato è un elenco di fasce orarie sotto forma di un oggetto [Iterable<Creneau>];
- riga 10: l'annotazione [@Query] viene utilizzata per specificare la query JPQL (Java Persistence Query Language) che implementa il metodo. Il parametro [?1] verrà sostituito dal parametro [idMedecin] del metodo;
L'interfaccia [RvRepository] è la seguente:
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);
}
- riga 10: l'interfaccia [RvRepository] eredita i metodi dell'interfaccia [CrudRepository];
- righe 12–13: il metodo [getRvMedecinJour] recupera gli appuntamenti di un medico per un determinato giorno;
- riga 13: i parametri sono l'ID del medico e il giorno. Il risultato è un elenco di appuntamenti sotto forma di oggetto [Iterable<Rv>];
- riga 12: l'annotazione [@Query] consente di specificare la query JPQL che implementa il metodo. Il parametro [?1] sarà sostituito dal parametro [idMedecin] del metodo, mentre il parametro [?2] sarà sostituito dal parametro [jour] del metodo. La seguente query JPQL non è sufficiente:
perché i campi della classe Rv, di tipo [Client] e [Creneau], vengono recuperati in modalità [FetchType.LAZY], il che significa che devono essere esplicitamente richiesti per essere ottenuti. Ciò avviene nella query JPQL utilizzando la sintassi [left join fetch entity], che richiede l'esecuzione di un join con la tabella a cui fa riferimento la chiave esterna per recuperare l'entità di riferimento;
8.4.6. Il livello [business]
![]() |
![]() |
- [IMetier] è l'interfaccia per il livello [business], mentre [Metier] ne costituisce l'implementazione;
- [Doctor'sDailySchedule] e [Doctor'sDailyTimeSlot] sono due entità di business;
8.4.6.1. Le entità
L'entità [CreneauMedecinJour] associa una fascia oraria a qualsiasi appuntamento prenotato all'interno di tale fascia:
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
// manufacturers
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau, rv);
}
// getters and setters
...
}
- riga 12: la fascia oraria;
- riga 13: l'appuntamento, se presente – null in caso contrario;
L'entità [AgendaMedecinJour] rappresenta l'agenda di un medico per un determinato giorno, ovvero l'elenco dei suoi appuntamenti:
package rdvmedecins.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.entities.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// manufacturers
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters and setters
...
}
- riga 13: il medico;
- riga 14: il giorno del calendario;
- riga 15: le loro fasce orarie disponibili, con o senza appuntamento;
8.4.6.2. Il servizio
L'interfaccia del livello [aziendale] è la seguente:
package rdvmedecins.metier;
import java.util.Date;
import java.util.List;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
public interface IMetier {
// customer list
public List<Client> getAllClients();
// list of doctors
public List<Medecin> getAllMedecins();
// list of physician slots
public List<Creneau> getAllCreneaux(long idMedecin);
// list of doctor's appointments on a given day
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// find a customer identified by its id
public Client getClientById(long id);
// find a customer identified by its id
public Medecin getMedecinById(long id);
// find an Rv identified by its id
public Rv getRvById(long id);
// find a time slot identified by its id
public Creneau getCreneauById(long id);
// add a RV
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// delete a RV
public void supprimerRv(Rv rv);
// job
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
I commenti spiegano il ruolo di ciascun metodo.
L'implementazione dell'interfaccia [IMetier] è la seguente classe [Metier]:
package rdvmedecins.metier;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("métier")
public class Metier implements IMetier {
// repositories
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// interface implementation
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
return rvRepository.save(new Rv(jour, client, créneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
}
- riga 24: l'annotazione [@Service] è un'annotazione Spring che rende la classe annotata un componente gestito da Spring. È possibile assegnare o meno un nome al componente. Questo è denominato [business];
- riga 25: la classe [Metier] implementa l'interfaccia [IMetier];
- riga 28: l'annotazione [@Autowired] è un'annotazione Spring. Il valore del campo annotato in questo modo verrà inizializzato (iniettato) da Spring con il riferimento a un componente Spring del tipo o del nome specificato. Qui, l'annotazione [@Autowired] non specifica un nome. Pertanto, verrà eseguita l'iniezione basata sul tipo;
- riga 29: il campo [medecinRepository] verrà inizializzato con il riferimento a un componente Spring di tipo [MedecinRepository]. Questo sarà il riferimento alla classe generata da Spring Data per implementare l'interfaccia [MedecinRepository] che abbiamo già presentato;
- righe 30–35: questo processo viene ripetuto per le altre tre interfacce discusse;
- righe 39–41: implementazione del metodo [getAllClients];
- riga 40: utilizziamo il metodo [findAll] dell'interfaccia [ClientRepository]. Questo metodo restituisce un tipo [Iterable<Client>], che convertiamo in un [List<Client>] utilizzando il metodo statico [Lists.newArrayList]. La classe [Lists] è definita nella libreria Google Guava. Nel file [pom.xml], questa dipendenza è stata importata:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- righe 38–86: i metodi dell'interfaccia [IMetier] sono implementati utilizzando classi del livello [DAO];
Solo il metodo alla riga 88 è specifico del livello [business]. È stato collocato qui perché esegue una logica di business che va oltre il semplice accesso ai dati. Senza questo metodo, non ci sarebbe motivo di creare un livello [business]. Il metodo [getAgendaMedecinJour] è il seguente:
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
// list of doctor's time slots
List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
// list of bookings for the same doctor on the same day
List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
// a dictionary is created from the Rvs taken
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// create the agenda for the requested day
AgendaMedecinJour agenda = new AgendaMedecinJour();
// the doctor
agenda.setMedecin(getMedecinById(idMedecin));
// the day
agenda.setJour(jour);
// reservation slots
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// filling reservation slots
for (int i = 0; i < creneauxHoraires.size(); i++) {
// line i agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// time slot
Creneau créneau = creneauxHoraires.get(i);
long idCreneau = créneau.getId();
creneauxMedecinJour[i].setCreneau(créneau);
// is the slot free or reserved?
if (hReservations.containsKey(idCreneau)) {
// the slot is occupied - we note the resa
Rv resa = hReservations.get(idCreneau);
creneauxMedecinJour[i].setRv(resa);
}
}
// we return the result
return agenda;
}
Si invitano i lettori a leggere i commenti. L'algoritmo è il seguente:
- recuperare tutte le fasce orarie per il medico specificato;
- recuperare tutti i suoi appuntamenti per il giorno specificato;
- con queste due informazioni, possiamo determinare se una fascia oraria è libera o prenotata;
8.4.7. La configurazione del progetto Spring
![]() |
La classe [DomainAndPersistenceConfig] configura l'intero progetto:
package rdvmedecins.config;
import javax.persistence.EntityManagerFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {
// JPA entity packages
public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
// the MySQL data source
@Bean
public DataSource dataSource() {
// data source TomcatJdbc
DataSource dataSource = new DataSource();
// configuration JDBC
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
// initially open connections
dataSource.setInitialSize(5);
// result
return dataSource;
}
// provider JPA is Hibernate
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transaction manager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- riga 17: la classe è una classe di configurazione Spring;
- riga 18: i pacchetti contenenti le interfacce [CrudRepository] di Spring Data. Questi saranno aggiunti al contesto Spring;
- riga 19: aggiunge al contesto Spring tutte le classi del pacchetto [rdvmedecins] e le sue sottoclassi che presentano un'annotazione Spring. Nel pacchetto [rdvmedecins.metier], verrà individuata la classe [Metier] con la sua annotazione [@Service] e aggiunta al contesto Spring;
- righe 26–39: configurano il pool di connessioni JDBC di Tomcat (riga 5);
- riga 36: il pool di connessioni avrà 5 connessioni aperte per impostazione predefinita. Questa riga è mostrata a scopo illustrativo. Nel nostro caso, sarebbe sufficiente 1 connessione. Se il livello [DAO] dovesse essere utilizzato da più thread, questa riga sarebbe necessaria. Questo sarà il caso in seguito, quando il livello [DAO] fungerà da base per un'applicazione web che, per sua natura, supporta più utenti serviti contemporaneamente;
- Righe 42–49: l'implementazione JPA utilizzata è un'implementazione Hibernate;
- riga 45: nessun log SQL;
- riga 46: nessuna rigenerazione delle tabelle;
- riga 47: il DBMS utilizzato è MySQL;
- Righe 53–61: definiscono l'EntityManagerFactory per il livello JPA. Da questo oggetto otteniamo l'oggetto [EntityManager], che viene utilizzato per eseguire operazioni JPA;
- Riga 57: specifica i pacchetti in cui si trovano le entità JPA;
- riga 58: specifica l'origine dati da collegare al livello JPA;
- righe 64–69: il gestore delle transazioni associato al precedente EntityManagerFactory. Per impostazione predefinita, i metodi delle interfacce [CrudRepository] di Spring Data vengono eseguiti all'interno di una transazione. La transazione viene avviata prima di entrare nel metodo e viene completata (tramite un commit o un rollback) dopo esserne usciti;
8.4.8. Test per il livello [business]
La classe [rdvmedecins.tests.Metier] è una classe di test Spring/JUnit 4:
package rdvmedecins.tests;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
@Autowired
private IMetier métier;
@Test
public void test1(){
// customer display
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
// physician display
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
// display doctor's slots
Medecin médecin = medecins.get(0);
List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
// list of doctor's appointments on a given day
Date jour = new Date();
display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// add a RV to the list
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// check
Rv rv2 = métier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// add a RV in the same slot on the same day
// must trigger an exception
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
Boolean erreur = false;
try {
rv = métier.ajouterRv(jour, créneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// we note the error
erreur = true;
}
// check for errors
Assert.assertTrue(erreur);
// RV list
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// calendar display
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// delete a RV
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// check
rv2 = métier.getRvById(rv.getId());
Assert.assertNull(rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
}
// utility method - displays items in a collection
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- riga 22: l'annotazione [@SpringApplicationConfiguration] consente di utilizzare il file di configurazione [DomainAndPersistenceConfig] descritto in precedenza. La classe di test beneficia così di tutti i bean definiti da questo file;
- riga 23: l'annotazione [@RunWith] consente l'integrazione di Spring con JUnit: la classe può essere eseguita come un test JUnit. [@RunWith] è un'annotazione JUnit (riga 9), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 12);
- Righe 26–27: Iniezione di un riferimento al livello [business] nella classe di test;
- molti test sono semplicemente test visivi:
- righe 32–33: elenco dei clienti;
- righe 35–36: elenco dei medici;
- righe 39-40: elenco delle fasce orarie di un medico;
- riga 43: elenco degli appuntamenti di un medico;
- riga 50: aggiunta di un nuovo appuntamento. Il metodo [addAppt] restituisce l'appuntamento con informazioni aggiuntive, ovvero il suo ID chiave primaria;
- riga 53: questa chiave primaria viene utilizzata per cercare l'appuntamento nel database;
- riga 54: verifichiamo che l'appuntamento cercato e quello trovato siano gli stessi. Ricordiamo che il metodo [equals] dell'entità [Rv] è stato ridefinito: due appuntamenti sono uguali se hanno lo stesso id. Qui, questo ci mostra che l'appuntamento aggiunto è stato effettivamente inserito nel database;
- Righe 61–73: proviamo ad aggiungere lo stesso appuntamento una seconda volta. Questo dovrebbe essere rifiutato dal DBMS perché esiste un vincolo di unicità:
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 ;
La riga 8 sopra specifica che la combinazione [DAY, SLOT_ID] deve essere univoca, il che impedisce che due appuntamenti vengano programmati nella stessa fascia oraria dello stesso giorno.
- riga 73: verifichiamo che si sia effettivamente verificata un'eccezione;
- riga 77: recuperiamo il calendario del medico per il quale abbiamo appena aggiunto un appuntamento;
- riga 79: verifichiamo che l'appuntamento aggiunto sia effettivamente presente nel suo calendario;
- riga 82: eliminiamo l'appuntamento aggiunto;
- riga 84: recuperiamo l'appuntamento cancellato dal database;
- riga 85: verifichiamo di aver recuperato un puntatore nullo, a indicare che l'appuntamento che stavamo cercando non esiste;
Il test viene eseguito con successo:
![]() |
8.4.9. Il programma da console
![]() |
Il programma da console è semplice. Mostra come recuperare una chiave esterna:
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
public class Boot {
// the boot
public static void main(String[] args) {
// prepare the configuration
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// launch it
ConfigurableApplicationContext context = app.run(args);
// business
IMetier métier = context.getBean(IMetier.class);
try {
// add a RV to the list
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// check
créneau = métier.getCreneauById(1L);
long idMedecin = créneau.getIdMedecin();
display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
} catch (Exception ex) {
System.out.println("Exception : " + ex.getCause());
}
// closing the Spring context
context.close();
}
// utility method - displays items in a collection
private static <T> void display(String message, Iterable<T> elements) {
System.out.println(message);
for (T element : elements) {
System.out.println(element);
}
}
}
Il programma aggiunge un appuntamento e poi verifica che sia stato aggiunto.
- riga 19: la classe [SpringApplication] utilizzerà la classe di configurazione [DomainAndPersistenceConfig];
- riga 20: soppressione dei log di avvio dell'applicazione;
- riga 22: viene eseguita la classe [SpringApplication]. Restituisce un contesto Spring, ovvero l'elenco dei bean registrati;
- riga 24: viene recuperato un riferimento al bean che implementa l'interfaccia [IMetier]. Si tratta quindi di un riferimento al livello [business];
- righe 27–31: aggiungi un nuovo appuntamento per oggi, per il cliente n. 1 nello slot n. 1. Il cliente e lo slot sono stati creati da zero per dimostrare che vengono utilizzati solo gli identificatori. Abbiamo inizializzato la versione qui, ma avremmo potuto utilizzare qualsiasi valore. Qui non viene utilizzata;
- riga 34: vogliamo sapere quale medico ha lo slot n. 1. Per farlo, dobbiamo interrogare il database per lo slot n. 1. Poiché siamo in modalità [FetchType.LAZY], il medico non viene restituito con lo slot. Tuttavia, ci siamo assicurati di includere un campo [idMedecin] nell'entità [Creneau] per recuperare la chiave primaria del medico;
- riga 35: recuperiamo la chiave primaria del medico;
- riga 36: visualizziamo l'elenco degli appuntamenti del medico;
L'output della console è il seguente:
8.4.10. Gestione dei log
I log della console sono configurati tramite due file: [application.properties] e [logback.xml] [1]:
![]() |
Il file [application.properties] viene utilizzato dal framework Spring Boot. Consente di definire un'ampia gamma di impostazioni per sovrascrivere i valori predefiniti utilizzati da Spring Boot (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Ecco il suo contenuto:
logging.level.org.hibernate=OFF
spring.main.show-banner=false
- Riga 1: controlla il livello di registrazione di Hibernate; in questo caso, nessuna registrazione
- riga 2: controlla la visualizzazione del banner di Spring Boot — in questo caso, nessun banner
Il file [logback.xml] è il file di configurazione del framework di log [logback] [2]:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="info"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- Il livello di log generale è controllato dalla riga 9: in questo caso, i log di livello [info];
Questo produce il seguente risultato:
Se impostiamo il livello di log di Hibernate su [info] (senza modificare nient'altro):
logging.level.org.hibernate=INFO
spring.main.show-banner=false
questo produce il seguente risultato:
Se impostiamo il livello di log su [debug] (senza modificare nient'altro):
logging.level.org.hibernate=DEBUG
spring.main.show-banner=false
Questo produce il seguente risultato:
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. Il livello [web / JSON]
![]() |
![]() |
Realizzeremo il livello [web / JSON] in diverse fasi:
- Fase 1: Un livello web operativo senza autenticazione;
- Fase 2: Implementazione dell'autenticazione con Spring Security;
- Passaggio 3: Implementazione del CORS [Il Cross-Origin Resource Sharing (CORS) è un meccanismo che consente di richiedere molte risorse (ad esempio, font, JavaScript, ecc.) presenti in una pagina web da un dominio diverso da quello di origine della risorsa. (Wikipedia)]. Il client per il nostro servizio web sarà un client web Angular che non appartiene necessariamente allo stesso dominio del nostro servizio web. Per impostazione predefinita, non può accedervi a meno che il servizio web non lo autorizzi a farlo. Vedremo come;
8.4.11.1. Configurazione di Maven
Il file [pom.xml] del progetto è il seguente:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webjson-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- spring mvc web layer -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- test layer -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- layer DAO -->
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
...
</project>
- righe 12–15: il progetto Maven principale;
- righe 19–22: dipendenze per un progetto Spring MVC;
- righe 24–28: dipendenze per i test JUnit/Spring;
- righe 30–34: dipendenze relative ai livelli del progetto [logica di business, DAO, JPA];
8.4.11.2. L'interfaccia del servizio web
![]() |
- in [1], sopra, il browser può richiedere solo un numero limitato di URL con una sintassi specifica;
- in [4], riceve una risposta JSON;
Le risposte del nostro servizio web avranno tutte lo stesso formato, corrispondente alla rappresentazione JSON di un oggetto di tipo [Response] come segue:
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- riga 7: codice di errore della risposta 0: OK, in caso contrario: KO;
- riga 11: un elenco di messaggi di errore, se c'è un errore;
- riga 13: il corpo della risposta;
Presentiamo ora le schermate che illustrano l'interfaccia del servizio web / JSON:
Elenco di tutti i pazienti dello studio medico [/getAllClients]
![]() |
Elenco di tutti i medici dello studio medico [/getAllMedecins]
![]() |
Elenco degli orari disponibili di un medico [/getAllCreneaux/{idMedecin}]
![]() |
Elenco degli appuntamenti di un medico [/getRvMedecinJour/{idMedecin}/{yyyy-mm-dd}
![]() |
Agenda giornaliera del medico [/getAgendaMedecinJour/{idMedecin}/{yyyy-mm-dd}]
![]() |
Per aggiungere o eliminare un appuntamento, utilizziamo l'estensione di Chrome [Advanced Rest Client] poiché queste operazioni vengono eseguite tramite una richiesta POST.
Aggiungi un appuntamento [/addAppointment]
![]() |
- in [0], l'URL del servizio web;
- in [1], viene utilizzato il metodo POST;
- in [2], il testo JSON delle informazioni inviate al servizio web nel formato {day, clientId, slotId};
- in [3], il client specifica al servizio web che sta inviando informazioni in formato JSON;
La risposta è quindi la seguente:
![]() |
- in [4]: il client invia l'intestazione indicando che i dati che sta inviando sono in formato JSON;
- in [5]: il servizio web risponde che sta inviando anch'esso JSON;
- in [6]: la risposta JSON del servizio web. Il campo [body] contiene la rappresentazione JSON dell'appuntamento aggiunto;
È possibile verificare la presenza del nuovo appuntamento:
![]() |
Prendere nota dell'ID dell'appuntamento [50]. Questo verrà eliminato.
Elimina un appuntamento [/deleteApp]
![]() |
- in [1], l'URL del servizio web;
- in [2], viene utilizzato il metodo POST;
- in [3], il testo JSON delle informazioni inviate al servizio web nella forma {idRv};
- in [4], il client specifica al servizio web che sta inviando dati JSON;
La risposta è quindi la seguente:
![]() |
- in [5]: il campo [status] è impostato su 0, a indicare che l'operazione è andata a buon fine;
È possibile verificare l'eliminazione dell'appuntamento:
![]() |
Come si può vedere sopra, l'appuntamento della paziente [sig.ra GERMAIN] non è più presente.
Il servizio web consente inoltre di recuperare le entità in base al loro ID:
![]() |
![]() |
![]() |
![]() |
Tutti questi URL sono gestiti dal controller [RdvMedecinsController], che presenteremo tra poco.
8.4.11.3. Configurazione del servizio web
![]() |
La classe di configurazione [AppConfig] è la seguente:
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 {
}
- Riga 12: la classe [AppConfig] configura l'intera applicazione;
- riga 9: la classe [AppConfig] è una classe di configurazione Spring;
- riga 10: specifichiamo che i componenti Spring devono essere cercati nel pacchetto [rdvmedecins.web] e nei suoi sottopacchetti. In questo modo verranno individuati i seguenti componenti:
- [@RestController RdvMedecinsController] nel pacchetto [rdvmedecins.web.controllers];
- [@Component ApplicationModel] nel pacchetto [rdvmedecins.web.models];
- Riga 11: importiamo la classe [DomainAndPersistenceConfig], che configura il progetto [rdvmedecins-metier-dao] per fornire l'accesso ai bean di quel progetto;
- riga 11: la classe [SecurityConfig] configura la sicurezza dell'applicazione web. Per ora la ignoreremo;
- riga 11: la classe [WebConfig] configura il livello [web / JSON];
La classe [WebConfig] è la seguente:
package rdvmedecins.web.config;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@EnableWebMvc
public class WebConfig {
// dispatcherservlet configuration for CORS headers
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mappers jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- righe 20–25: definiscono il bean [dispatcherServlet]. La classe [DispatcherServlet] è il servlet del framework Spring MVC. Agisce come un [FrontController]: intercetta le richieste inviate al sito Spring MVC e le instrada verso uno dei controller del sito;
- riga 22: istanziazione della classe;
- riga 23: questa riga può essere ignorata per ora;
- righe 27–30: il servlet [dispatcherServlet] gestisce tutti gli URL;
- righe 27–30: attivazione del server Tomcat incorporato nelle dipendenze del progetto. Verrà eseguito sulla porta 8080;
- righe 38–67: quattro mappatori JSON configurati con diversi filtri JSON;
- righe 38–41: un mappatore JSON senza filtri;
- righe 43–49: il mappatore JSON [jsonMapperShortCreneau] serializza/deserializza un oggetto [Creneau] ignorando il campo [Creneau.medecin];
- righe 51–59: il mappatore JSON [jsonMapperLongRv] serializza/deserializza un oggetto [Rv] ignorando il campo [Rv.creneau.medecin];
- righe 61-67: il mappatore JSON [jsonMapperShortRv] serializza/deserializza un oggetto [Rv] ignorando i campi [Rv.creneau] e [Rv.client];
8.4.11.4. La classe [ApplicationModel]
![]() |
La classe [ApplicationModel] avrà due scopi:
- come cache per memorizzare gli elenchi di medici e pazienti (clienti);
- come interfaccia unica per i controller;
package rdvmedecins.web.models;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;
@Component
public class ApplicationModel implements IMetier {
// the [business] layer
@Autowired
private IMetier métier;
// data from the [business] layer
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// configuration data
private boolean CORSneeded = false;
private boolean secured = false;
@PostConstruct
public void init() {
// we get the doctors and the customers
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// getter
public List<String> getMessages() {
return messages;
}
// ------------------------- [business] layer interface
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return médecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return métier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return métier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return métier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return métier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return métier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return métier.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return métier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(long idRv) {
métier.supprimerRv(idRv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
// getters and setters
public boolean isCORSneeded() {
return CORSneeded;
}
public boolean isSecured() {
return secured;
}
}
- riga 19: l'annotazione [@Component] rende la classe [ApplicationModel] un componente Spring. Come tutti i componenti Spring visti finora (ad eccezione di @Controller), verrà istanziato un solo oggetto di questo tipo (singleton);
- riga 20: la classe [ApplicationModel] implementa l'interfaccia [IMetier];
- righe 23–24: Spring inietta un riferimento al livello [business];
- riga 34: l'annotazione [@PostConstruct] garantisce che il metodo [init] venga eseguito immediatamente dopo l'istanziazione della classe [ApplicationModel];
- righe 38–39: recupero degli elenchi di medici e clienti dal livello [business];
- Riga 41: se si verifica un'eccezione, i messaggi dello stack di eccezioni vengono memorizzati nel campo alla riga 17;
L'architettura del livello web si evolve come segue:
![]() |
- In [2b], i metodi del/i controller comunicano con il singleton [ApplicationModel];
Questa strategia offre flessibilità nella gestione della cache. Attualmente, gli slot degli appuntamenti dei medici non vengono memorizzati nella cache. Per memorizzarli, è sufficiente modificare la classe [ApplicationModel]. Ciò non ha alcun impatto sul controller, che continuerà a utilizzare il metodo [List<Creneau> getAllCreneaux(long idMedecin)] come prima. È l’implementazione di questo metodo in [ApplicationModel] che verrà modificata.
8.4.11.5. La classe Static
La classe [Static] contiene un insieme di metodi di utilità statici che non hanno aspetti "aziendali" o "web":
![]() |
Il codice è il seguente:
package rdvmedecins.web.helpers;
import java.util.ArrayList;
import java.util.List;
public class Static {
public Static() {
}
// list of exception error messages
public static List<String> getErreursForException(Exception exception) {
// retrieve the list of exception error messages
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
erreurs.add(cause.getMessage());
cause = cause.getCause();
}
return erreurs;
}
}
- riga 12: il metodo [Static.getErrorsForException] utilizzato (riga 8 sotto) nel metodo [init] della classe [ApplicationModel]:
@PostConstruct
public void init() {
// we get the doctors and the customers
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
Il metodo costruisce un oggetto [List<String>] contenente i messaggi di errore [exception.getMessage()] di un'eccezione [exception] e quelli della sua eccezione interna [exception.getCause()].
8.4.11.6. Lo scheletro del controller [RdvMedecinsController]
![]() |
Descriveremo ora in dettaglio la gestione degli URL del servizio web. In questo processo sono coinvolte tre classi principali:
- il controller [RdvMedecinsController];
- la classe dei metodi di utilità [Static];
- la classe della cache [ApplicationModel];
![]() |
Il controller [RdvMedecinsController] è il seguente:
package rdvmedecins.web.controllers;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;
@Controller
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
// message list
private List<String> messages;
// mappers jSON
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
@PostConstruct
public void init() {
// application error messages
messages = application.getMessages();
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {...}
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {...}
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String authenticate() throws JsonProcessingException {...}
}
- riga 35: l'annotazione [@Controller] rende la classe [RdvMedecinsController] un controller Spring, la C in MVC;
- righe 38–39: un oggetto di tipo [ApplicationModel] verrà iniettato qui da Spring. Lo abbiamo già presentato;
- righe 41-42: un oggetto di tipo [RdvMedecinsCorsController] verrà iniettato qui da Spring. Introdurremo questo oggetto più avanti;
- righe 48–58: i mappatori JSON definiti nella classe di configurazione [WebConfig];
- riga 60: l'annotazione [@PostConstruct] contrassegna un metodo da eseguire immediatamente dopo l'istanziazione della classe. Quando questo metodo viene eseguito, gli oggetti iniettati da Spring sono disponibili;
- riga 63: recuperiamo eventuali messaggi di errore dall'oggetto [ApplicationModel]. Questo oggetto è stato istanziato all'avvio dell'applicazione e ha tentato di memorizzare nella cache i medici e i clienti. Se l'operazione non è andata a buon fine, allora [messages!=null]. Ciò consentirà ai metodi del controller di determinare se l'applicazione si è inizializzata correttamente;
- righe 67–118: gli URL esposti dal servizio [web/jSON]. Tutti i metodi restituiscono una stringa JSON del seguente tipo [Response<T>]:
![]() |
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- riga 9: un codice di errore: 0 significa nessun errore;
- riga 11: se [status!=0], allora [messages] è un elenco di messaggi di errore;
- riga 13: un oggetto T incapsulato nella risposta. T è nullo in caso di errore;
Questo oggetto viene serializzato in JSON prima di essere inviato al browser del client;
- riga 67: l'URL esposto è [/getAllDoctors]. Il client deve utilizzare un metodo [GET] per effettuare la richiesta (method = RequestMethod.GET). Se questo URL fosse richiesto tramite un POST, verrebbe rifiutato e Spring MVC invierebbe un codice di errore HTTP al client web. Il metodo stesso restituisce la risposta al client (riga 68). Si tratterà di una stringa (riga 67). L'intestazione HTTP [Content-type: application/json; charset=UTF-8] verrà inviata al client per indicare che riceverà una stringa JSON (riga 67);
- riga 77: l'URL è configurato con {idMedecin}. Questo parametro viene recuperato utilizzando l'annotazione [@PathVariable] alla riga 79;
- Riga 79: il parametro [long idMedecin] ottiene il suo valore dal parametro {idMedecin} nell'URL [@PathVariable("idMedecin")]. Il parametro nell'URL e quello nel metodo possono avere nomi diversi . Si noti che [@PathVariable("idMedecin")] è di tipo String (l'intero URL è una String), mentre il parametro [long idMedecin] è di tipo [long]. La conversione di tipo viene eseguita automaticamente. Se questa conversione di tipo fallisce, viene restituito un codice di errore HTTP;
- riga 105: l'annotazione [@RequestBody] si riferisce al corpo della richiesta. In una richiesta GET, non c'è quasi mai un corpo (ma è possibile includerne uno). In una richiesta POST, di solito ce n'è uno (ma è possibile ometterlo). Per l'URL [ajouterRv], il client web invia la seguente stringa JSON nel suo POST:
La sintassi [@RequestBody PostAjouterRv post] (riga 105), combinata con il fatto che il metodo si aspetta JSON [consumes = "application/json; charset=UTF-8"] (riga 103), significa che la stringa JSON inviata dal client web verrà deserializzata in un oggetto di tipo [PostAjouterRv]. Questo avviene come segue:
package rdvmedecins.web.models;
public class PostAjouterRv {
// pOST DATA
private String jour;
private long idClient;
private long idCreneau;
// getters and setters
...
}
Anche in questo caso, le conversioni di tipo necessarie avverranno automaticamente;
- Le righe 107–109 contengono un meccanismo simile per l'URL [/supprimerRv]. La stringa JSON inviata è la seguente:
e il tipo [PostSupprimerRv] è il seguente:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// pOST DATA
private long idRv;
// getters and setters
...
}
8.4.11.7. L'URL [/getAllDoctors]
L'URL [/getAllMedecins] è gestito dal seguente metodo nel controller [RdvMedecinsController]:
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {
// the answer
Response<List<Medecin>> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// list of doctors
try {
response = new Response<>(0, null, application.getAllMedecins());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
}
// answer
return jsonMapper.writeValueAsString(response);
}
- righe 9-10: verifichiamo se l'applicazione si è inizializzata correttamente (messages==null). In caso contrario, restituiamo una risposta con status=-1 e body=messages;
- riga 13: altrimenti, richiediamo l'elenco dei medici dalla classe [ApplicationModel];
- riga 19: inviamo la stringa JSON della risposta utilizzando il mappatore JSON [jsonMapper] poiché la classe [Medecin] non dispone di un filtro JSON. La risposta può essere priva di errori (riga 14) o contenere un errore (riga 16). Il metodo [application.getAllMedecins()] non genera un'eccezione perché restituisce semplicemente un elenco memorizzato nella cache. Tuttavia, manterremo questa gestione delle eccezioni nel caso in cui i medici non siano più memorizzati nella cache;
Non abbiamo ancora illustrato il caso in cui l'applicazione si sia inizializzata in modo errato. Arrestiamo il DBMS MySQL5, avviamo il servizio web e quindi richiediamo l'URL [/getAllMedecins]:

Otteniamo effettivamente un errore. In circostanze normali, otteniamo la seguente vista:
![]() |
8.4.11.8. L'URL [/getAllClients]
L'URL [/getAllClients] è gestito dal seguente metodo nel [RdvMedecinsController]:
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {
// the answer
Response<List<Client>> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// customer list
try {
response = new Response<>(0, null, application.getAllClients());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
// answer
return jsonMapper.writeValueAsString(response);
}
È simile al metodo [getAllMedecins] che abbiamo già visto. I risultati ottenuti sono i seguenti:
![]() |
8.4.11.9. L'URL [/getAllSlots/{doctorId}]
L'URL [/getAllSlots/{doctorId}] è gestito dal seguente metodo del controller [RdvMedecinsController]:
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
// the answer
Response<List<Creneau>> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// we get the doctor back
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
Medecin médecin = responseMedecin.getBody();
// doctor's slots
try {
response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapperShortCreneau.writeValueAsString(response);
}
- riga 12: il medico identificato dal parametro [id] viene richiesto da un metodo locale:
private Response<Medecin> getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// existing doctor?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// ok
return new Response<Medecin>(0, null, médecin);
}
Questo metodo restituisce un valore di stato compreso nell'intervallo [0,1,2]. Torniamo al codice del metodo [getAllCreneaux]:
- righe 13-14: se status!=0, costruiamo una risposta con un errore;
- riga 16: recuperiamo il medico;
- riga 19: recuperiamo le fasce orarie di questo medico;
- riga 25: inviamo un oggetto [List<Creneau>] come risposta. Ricordiamo la definizione della classe [Creneau]:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- riga 13: il medico viene recuperato in modalità [FetchType.LAZY];
Ricordiamo la query JPQL che implementa il metodo [getAllCreneaux] nel livello [DAO]:
@Query("select c from Creneau c where c.medecin.id=?1")
La notazione [c.medecin.id] impone un join tra le tabelle [CRENEAUX] e [MEDECINS]. Di conseguenza, la query restituisce tutti gli slot del medico, con il medico incluso in ciascuno di essi. Quando serializziamo questi slot in JSON, la stringa JSON del medico compare in ciascuno di essi. Ciò non è necessario. Per controllare la serializzazione, abbiamo bisogno di due cose:
- l'accesso all'oggetto da serializzare;
- configurare l'oggetto da serializzare;
Il punto 1 viene gestito iniettando nel controller il convertitore JSON appropriato per l'oggetto:
@Autowired
private ObjectMapper jsonMapperShortCreneau;
Il punto 2 si ottiene aggiungendo un'annotazione alla classe [Creneau] definita nel progetto [rdvmedecins-metier-dao]:
![]() |
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
- Riga 3: un'annotazione della libreria JSON Jackson. Crea un filtro chiamato [creneauFilter]. Utilizzando questo filtro, saremo in grado di definire a livello di programmazione quali campi debbano o non debbano essere serializzati;
La serializzazione dell'oggetto [Creneau] avviene nella seguente riga del metodo [getAllCreneaux]:
// réponse
return jsonMapperShortCreneau.writeValueAsString(response);
Il mappatore JSON [jsonMapperShortCreneau] è stato definito nella classe [WebConfig] come segue:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
- Riga 5: Il filtro denominato [creneauFilter] è associato al filtro [creneauFilter] della riga 4. Questo filtro serializza l'oggetto [Creneau] senza il suo campo [medecin];
Il risultato restituito dal metodo [getAllCreneaux] è una stringa JSON di tipo [Response<List<Creneau>].
I risultati ottenuti sono i seguenti:
![]() |
oppure questi se lo slot non esiste:
![]() |
Da questo esempio possiamo ricavare la seguente regola:
- I metodi del server Web / JSON restituiscono un oggetto di tipo [Response<T>] serializzato in JSON;
- se il tipo T ha uno o più filtri JSON, per serializzarlo verrà utilizzato un mappatore con gli stessi filtri;
8.4.11.10. L'URL [/getRvMedecinJour/{idMedecin}/{jour}]
L'URL [/getRvMedecinJour/{idMedecin}/{jour}] è gestito dal seguente metodo del controller [RdvMedecinsController]:
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// the answer
Response<List<Rv>> response=null;
boolean erreur = false;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// check the date
Date jourAgenda = null;
if (!erreur) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<List<Rv>>(3, messages, null);
erreur = true;
}
}
Response<Medecin> responseMedecin = null;
if (!erreur) {
// we get the doctor back
responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
Medecin médecin = responseMedecin.getBody();
// list of appointments
try {
response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapperLongRv.writeValueAsString(response);
}
- Dobbiamo restituire la stringa JSON di tipo [Response<List<Rv>>]. La classe [Rv] ha un campo [Rv.creneau]. Se questo campo viene serializzato, incontreremo il filtro JSON [creneauFilter];
- riga 47: l'oggetto di tipo [Response<List<Rv>>] della riga 7 viene serializzato in JSON;
Esaminiamo il caso in cui l'elenco degli appuntamenti è stato ottenuto alla riga 42. La classe [Rv] nel progetto [rdvmedecins-metier-dao] è definita come segue:
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of an Rv
@Temporal(TemporalType.DATE)
private Date jour;
// an appointment is linked to a customer
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// an appointment is linked to a time slot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// foreign keys
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
}
- riga 11: il client viene recuperato utilizzando la modalità [FetchType.LAZY];
- riga 18: lo slot viene recuperato utilizzando la modalità [FetchType.LAZY];
Ricordiamo la query JPQL che recupera gli appuntamenti:
@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")
I join vengono eseguiti in modo esplicito per recuperare i campi [client] e [slot]. Inoltre, grazie al join [cr.doctor.id=?1], otterremo anche il medico. Il medico apparirà quindi nella stringa JSON per ogni appuntamento. Tuttavia, queste informazioni duplicate non sono necessarie. Abbiamo visto come risolvere questo problema utilizzando un filtro JSON sull'oggetto [Creneau]. A causa delle modalità [FetchType.LAZY] dei campi [client] e [slot] nella classe [Rv], scopriremo presto la necessità di applicare un filtro JSON alla classe [RV] nel progetto [rdvmedecins-metier-dao]:
@Entity
@Table(name = "rv")
@JsonFilter("rvFilter")
public class Rv extends AbstractEntity {
...
Controlleremo la serializzazione dell'oggetto [Rv] utilizzando il filtro [rvFilter]. Apparentemente, in questo caso, non abbiamo bisogno di filtrare perché ci servono tutti i campi dell'oggetto [Rv]. Tuttavia, poiché abbiamo specificato che la classe ha un filtro JSON, dobbiamo definirlo per qualsiasi serializzazione di un oggetto di tipo [Rv]; altrimenti, otterremo un'eccezione. Per farlo, usiamo il seguente mappatore JSON definito nella classe [rdvMedecinsController]:
@Autowired
private ObjectMapper jsonMapperLongRv;
Questo mapper è definito come segue nella classe di configurazione [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;
}
- Riga 4: specifichiamo che tutti i campi dell'oggetto [Rv] devono essere serializzati;
- riga 5: specifichiamo che nell'oggetto [Creneau], il campo [medecin] non deve essere serializzato;
- riga 6: aggiungiamo i due filtri [rvFilter] e [creneauFilter] ai filtri JSON dell'oggetto [jsonMapperLongRv];
I risultati ottenuti sono i seguenti:
![]() |
oppure questi con una giornata senza appuntamenti:
![]() |
oppure questi con un giorno errato:
![]() |
oppure questi con un medico errato:
![]() |
8.4.11.11. L'URL [/getAgendaMedecinJour/{idMedecin}/{jour}]
L'URL [/getAgendaMedecinJour/{idMedecin}/{jour}] è gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// the answer
Response<AgendaMedecinJour> response = null;
boolean erreur = false;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// check the date
Date jourAgenda = null;
if (!erreur) {
// check the date
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
erreur = true;
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(3, messages, null);
}
}
// we get the doctor back
Medecin médecin = null;
if (!erreur) {
// we get the doctor back
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
médecin = responseMedecin.getBody();
}
}
// get your diary back
if (!erreur) {
try {
response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapperLongRv.writeValueAsString(response);
}
- righe 6, 49: restituiamo la stringa JSON di tipo [AgendaMedecinJour] incapsulata in un oggetto [Response];
Il tipo [AgendaMedecinJour] è il seguente:
public class AgendaMedecinJour implements Serializable {
// fields
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
Il tipo [CreneauMedecinJour] è il seguente:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
I campi [creneau] e [rv] hanno filtri JSON che devono essere configurati. Questo è ciò che fa la riga 49 del metodo [getAgendaMedecinJour], utilizzando il mappatore JSON [jsonMapperLongRv] che abbiamo incontrato in precedenza:
@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;
}
I risultati ottenuti sono i seguenti:
![]() |
Come si vede sopra, il 28/01/2015 il dottor PELISSIER ha un appuntamento con la signora Brigitte BISTROU alle 8:20;
oppure questi, se la data non è corretta:
![]() |
oppure questi se l'ID del medico non è valido:
![]() |
8.4.11.12. L'URL [/getMedecinById/{id}]
L'URL [/getMedecinById/{id}] è gestito dal seguente metodo nel [RdvMedecinsController]:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
// the answer
Response<Medecin> response;
// application status
if (messages != null) {
response = new Response<Medecin>(-1, messages, null);
} else {
response = getMedecin(id);
}
// answer
return jsonMapper.writeValueAsString(response);
}
- Righe 5, 13: Il metodo restituisce una stringa JSON di tipo [Doctor]. Questo tipo non ha alcuna annotazione di filtro JSON. Pertanto, alla riga 14, il mappatore JSON viene utilizzato senza filtri;
Riga 10: il metodo [getMedecin] è il seguente:
private Response<Medecin> getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// existing doctor?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// ok
return new Response<Medecin>(0, null, médecin);
}
I risultati sono i seguenti:
![]() |
oppure questi se l'ID del medico non è corretto:
![]() |
8.4.11.13. L'URL [/getClientById/{id}]
L'URL [/getClientById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
// the answer
Response<Client> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
response = getClient(id);
}
// answer
return jsonMapper.writeValueAsString(response);
}
- Righe 5, 13: Il metodo restituisce una stringa JSON di tipo [Client]. Questo tipo non ha annotazioni di filtro JSON. Pertanto, alla riga 13, il mappatore JSON viene utilizzato senza filtri;
Riga 11: il metodo [getClient] è il seguente:
private Response<Client> getClient(long id) {
// we get the customer back
Client client = null;
try {
client = application.getClientById(id);
} catch (RuntimeException e1) {
return new Response<Client>(1, Static.getErreursForException(e1), null);
}
// existing customer?
if (client == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le client d'id [%s] n'existe pas", id));
return new Response<Client>(2, messages, null);
}
// ok
return new Response<Client>(0, null, client);
}
I risultati sono i seguenti:
![]() |
oppure questi se l'ID cliente non è corretto:
![]() |
8.4.11.14. L'URL [/getCreneauById/{id}]
L'URL [/getCreneauById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
// the answer
Response<Creneau> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// we give back the slot
response = getCreneau(id);
}
// answer
return jsonMapperShortCreneau.writeValueAsString(response);
}
- Righe 5, 14: il metodo restituisce una stringa JSON di tipo [Response<Creneau>];
Riga 8: il metodo [getCreneau] è il seguente:
private Response<Creneau> getCreneau(long id) {
// we get the slot back
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (RuntimeException e1) {
return new Response<Creneau>(1, Static.getErreursForException(e1), null);
}
// existing niche?
if (créneau == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le créneau d'id [%s] n'existe pas", id));
return new Response<Creneau>(2, messages, null);
}
// ok
return new Response<Creneau>(0, null, créneau);
}
Rivediamo il codice dell'entità [Creneau]:
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
- righe 14-16: poiché il campo [doctor] è in modalità [fetch = FetchType.LAZY], non viene recuperato quando si recupera uno slot tramite il suo [id]. È quindi necessario escluderlo dalla serializzazione. Senza questa esclusione, si verifica un'eccezione. Ciò è dovuto al fatto che l'oggetto di serializzazione [mapper] chiamerà il metodo [getMedecin] per recuperare il campo [medecin]. Tuttavia, con un'implementazione JPA/Hibernate, la modalità [fetch = FetchType.LAZY] del campo [medecin] restituisce un oggetto [Creneau] il cui metodo [getMedecin] è programmato per recuperare il medico dal contesto JPA. Questo è chiamato oggetto [proxy]. Ora, ricordiamo l'architettura dell'applicazione web:
![]() |
Il controller si trova nel blocco [Controllers / Actions]. Una volta all'interno di questo blocco, il concetto di contesto JPA non è più applicabile. Il contesto JPA viene creato durante le operazioni nel livello [DAO] e non persiste oltre tale livello. Pertanto, quando il controller tenta di accedere al contesto JPA, si verifica un'eccezione che indica che è chiuso. Per evitare questa eccezione, è necessario impedire la serializzazione del campo [medecin] della classe [Rv]. Questo è ciò che fa il mappatore JSON [jsonMapperShortCreneau]:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
I risultati ottenuti sono i seguenti:
![]() |
oppure questi se il numero dello slot non è corretto:
![]() |
8.4.11.15. L'URL [/getRvById/{id}]
L'URL [/getRvById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
// the answer
Response<Rv> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// we recover rv
response = getRv(id);
}
// answer
return jsonMapperShortRv.writeValueAsString(response);
}
- Righe 5, 14: Il metodo restituisce una stringa JSON di tipo [Response<Rv>];
Riga 11: il metodo [getRv] è il seguente:
private Response<Rv> getRv(long id) {
// we recover the Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (RuntimeException e1) {
return new Response<Rv>(1, Static.getErreursForException(e1), null);
}
// Existing Rv?
if (rv == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le rendez-vous d'id [%s] n'existe pas", id));
return new Response<Rv>(2, messages, null);
}
// ok
return new Response<Rv>(0, null, rv);
}
La classe [Rv] presenta due campi annotati con [fetch = FetchType.LAZY]: i campi [creneau] e [client]. Questi campi non vengono quindi recuperati quando si recupera un [Rv] tramite la sua chiave primaria. Per gli stessi motivi di prima, devono quindi essere esclusi dalla serializzazione. Questo è ciò che fa il seguente mappatore [jsonMapperShortRv], definito nella 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;
}
I risultati ottenuti sono i seguenti:
![]() |
oppure questi, se il numero dell'appuntamento non è corretto:
![]() |
8.4.11.16. L'URL [/ajouterRv]
L'URL [/addAppt] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
// the answer
Response<Rv> response = null;
boolean erreur = false;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// retrieve posted values
String jour;
long idCreneau = -1;
long idClient = -1;
Date jourAgenda = null;
if (!erreur) {
// retrieve posted values
jour = post.getJour();
idCreneau = post.getIdCreneau();
idClient = post.getIdClient();
// check the date
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(6, messages, null);
erreur = true;
}
}
// we get the slot back
Response<Creneau> responseCréneau = null;
if (!erreur) {
// we get the slot back
responseCréneau = getCreneau(idCreneau);
if (responseCréneau.getStatus() != 0) {
erreur = true;
response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
}
}
// we get the customer back
Response<Client> responseClient = null;
Creneau créneau = null;
if (!erreur) {
créneau = (Creneau) responseCréneau.getBody();
// we get the customer back
responseClient = getClient(idClient);
if (responseClient.getStatus() != 0) {
erreur = true;
response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
}
}
if (!erreur) {
Client client = responseClient.getBody();
// we add the Rv
try {
response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(5, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapperLongRv.writeValueAsString(response);
}
- righe 5, 67: il metodo deve restituire una stringa JSON di tipo [Response<Rv>];
- riga 3: l'annotazione [@RequestBody PostAjouterRv post] recupera il corpo POST e lo inserisce nel parametro [PostAjouterRv post]. Questo corpo è JSON [consumes = "application/json; charset=UTF-8"] che verrà automaticamente deserializzato nel seguente tipo [PostAjouterRv]:
public class PostAjouterRv {
// pOST DATA
private String jour;
private long idClient;
private long idCreneau;
...
- poi c'è del codice che è già stato incontrato in una forma o nell'altra;
- riga 67: configurazione dei filtri JSON [creneauFilter] e [rvFilter]. Il metodo restituisce una stringa JSON di tipo [Response<Rv>], dove Rv è stato ottenuto alla riga 61. L'oggetto [Rv] incapsula un oggetto [Creneau] e un oggetto [Client]. L'oggetto [Creneau] ha una dipendenza [FetchType.LAZY] su un oggetto [Medecin] ed è stato recuperato nelle righe 36–44. È stato prelevato dal contesto JPA tramite la sua chiave primaria ed è stato recuperato senza la sua dipendenza [FetchType.LAZY]. In definitiva,
- l'oggetto [Rv] ha tutte le sue dipendenze. Queste possono essere serializzate;
- l'oggetto [Creneau] non ha la sua dipendenza [medecin]. Pertanto, questa dipendenza non deve essere serializzata;
Il mappatore JSON [jsonMapperLongRv] definito nella classe [WebConfig] soddisfa questi vincoli:
@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;
}
I risultati ottenuti con il client [Advanced Rest Client] sono i seguenti:
![]() |
- in [1], l'URL POST;
- in [2], la richiesta POST;
- in [3], il valore inviato;
- in [4a], questo valore inviato è in formato JSON;
![]() |
- in [4b], il client indica che sta inviando JSON;
- in [5], il server indica che sta restituendo JSON;
![]() |
- in [6], la risposta JSON del server che rappresenta l'appuntamento aggiunto. Mostra l'ID [id] dell'appuntamento aggiunto;
Otteniamo quanto segue con un numero di slot inesistente:
![]() |
8.4.11.17. L'URL [/deleteAppointment]
L'URL [/deleteAppointment] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
// the answer
Response<Void> response = null;
boolean erreur = false;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// retrieve posted values
long idRv = post.getIdRv();
// recovering the rv
if (!erreur) {
Response<Rv> responseRv = getRv(idRv);
if (responseRv.getStatus() != 0) {
response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
// rv deletion
try {
application.supprimerRv(idRv);
response = new Response<Void>(0, null, null);
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapper.writeValueAsString(response);
}
- riga 5: il tipo [Void] è la classe corrispondente al tipo primitivo [void];
- righe 5, 34: il metodo restituisce una stringa JSON di tipo [Response<Void>] che non ha filtri JSON. Pertanto, alla riga 34, utilizziamo il mappatore JSON senza filtri;
- riga 3: il metodo accetta il corpo POST come parametro, ovvero il valore inviato. Questo viene ricevuto in formato JSON [content-type="application/json; charset=UTF-8"] e automaticamente deserializzato nel seguente tipo [PostSupprimerRv]:
public class PostSupprimerRv {
// pOST DATA
private long idRv;
- riga 28: quando l'eliminazione va a buon fine, viene inviata una risposta con [status=0];
I risultati ottenuti sono i seguenti:
![]() |
![]() |
- in [5], il campo [status=0] indica che l'eliminazione è andata a buon fine;
Con un ID appuntamento inesistente, otteniamo quanto segue:
![]() |
Abbiamo finito con il controller. Ora vediamo come eseguire il progetto.
8.4.11.18. La classe eseguibile del servizio web
![]() |
La classe [Boot] [1] è la seguente:
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);
}
}
Riga 10: Il metodo statico [SpringApplication.run] viene eseguito con la classe di configurazione del progetto [AppConfig] come primo parametro. Questo metodo configurerà automaticamente il progetto, avvierà il server Tomcat incorporato nelle dipendenze e distribuirà il controller [RdvMedecinsController] su di esso.
I log sono gestiti dai seguenti file [2]:
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="info"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- riga 9: il livello di log generale è impostato su [info];
[application.properties]
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=OFF
spring.main.show-banner=false
Le righe 1-2 impostano un livello di registrazione specifico per alcune parti dell'applicazione:
- riga 1: vogliamo i log dal livello [web];
- riga 2: non vogliamo i log dal livello [JPA];
- riga 3: nessun banner di Spring Boot;
I log durante l'esecuzione sono i seguenti:
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)
- riga 18: il server Tomcat è attivo;
- riga 21: il contesto Spring è in fase di inizializzazione;
- righe 27–38: gli URL esposti dal servizio web vengono individuati;
- riga 44: il server Tomcat è pronto e in attesa di richieste sulla porta 8080;
Se modifichiamo il file [application.properties] come segue:
logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false
otteniamo i seguenti log:
Inoltre, se modifichiamo il file [logback.xml] come segue:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="off"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
Si ottengono i seguenti log:
Possiamo quindi vedere che abbiamo un certo controllo sui log che appaiono nella console. Il livello [info] è spesso il livello di log appropriato.
Ora disponiamo di un servizio web operativo che può essere interrogato utilizzando un client web. Ci occuperemo ora della sicurezza di questo servizio: vogliamo che solo determinate persone possano gestire gli appuntamenti dei medici. Per farlo, utilizzeremo il framework Spring Security, un componente dell'ecosistema Spring.
8.4.12. Introduzione a Spring Security
Importeremo nuovamente una guida Spring seguendo i passaggi da 1 a 3 riportati di seguito:
![]() |
![]() |
Il progetto comprende i seguenti elementi:
- Nella cartella [templates] troverete le pagine HTML del progetto;
- [Application]: è la classe eseguibile del progetto;
- [MvcConfig]: è la classe di configurazione Spring MVC;
- [WebSecurityConfig]: è la classe di configurazione di Spring Security;
8.4.12.1. Configurazione Maven
Il progetto [3] è un progetto Maven. Esaminiamo il suo file [pom.xml] per vedere le sue dipendenze:
<?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>
- righe 10–14: il progetto è un progetto Spring Boot;
- righe 17–20: dipendenza dal framework [Thymeleaf];
- righe 22–25: dipendenza dal framework Spring Security;
8.4.12.2. viste Thymeleaf
![]() |
La vista [home.html] è la seguente:
![]() |
<!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>
- riga 12: l'attributo [th:href="@{/hello}"] genererà l'attributo [href] del tag <a>. Il valore [@{/hello}] genererà il percorso [<context>/hello], dove [context] è il contesto dell'applicazione web;
Il codice HTML generato è il seguente:
<!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>
La vista [hello.html] è la seguente:
![]() |
<!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>
- riga 9: L'attributo [th:inline="text"] genererà il testo del tag <h1>. Questo testo contiene un'espressione $ che deve essere valutata. L'elemento [[${#httpServletRequest.remoteUser}]] è il valore dell'attributo [RemoteUser] della richiesta HTTP corrente. Questo è il nome dell'utente che ha effettuato l'accesso;
- riga 10: un modulo HTML. L'attributo [th:action="@{/logout}"] genererà l'attributo [action] del tag [form]. Il valore [@{/logout}] genererà il percorso [<context>/logout], dove [context] è il contesto dell'applicazione web;
Il codice HTML generato è il seguente:
<!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>
- riga 8: la traduzione di Hello [[${#httpServletRequest.remoteUser}]]!;
- riga 9: la traduzione di @{/logout};
- riga 11: un campo nascosto denominato (attributo name) _csrf;
La vista finale [login.html] è la seguente:
![]() |
<!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>
- riga 9: l'attributo [th:if="${param.error}"] garantisce che il tag <div> venga generato solo se l'URL che visualizza la pagina di login contiene il parametro [error] (http://context/login?error);
- riga 10: l'attributo [th:if="${param.logout}"] garantisce che il tag <div> venga generato solo se l'URL che visualizza la pagina di accesso contiene il parametro [logout] (http://context/login?logout);
- righe 11–23: un modulo HTML;
- riga 11: il modulo verrà inviato all'URL [<context>/login], dove <context> è il contesto dell'applicazione web;
- riga 13: un campo di immissione denominato [username];
- riga 17: un campo di immissione denominato [password];
Il codice HTML generato è il seguente:
<!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>
Si noti alla riga 28 che Thymeleaf ha aggiunto un campo nascosto denominato [_csrf].
8.4.12.3. Configurazione Spring MVC
![]() |
La classe [MvcConfig] configura il 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");
}
}
- riga 7: l'annotazione [@Configuration] rende la classe [MvcConfig] una classe di configurazione;
- riga 8: la classe [MvcConfig] estende la classe [WebMvcConfigurerAdapter] per sovrascrivere determinati metodi;
- riga 10: ridefinizione di un metodo della classe padre;
- righe 11–16: il metodo [addViewControllers] consente di associare gli URL alle viste HTML. Qui vengono effettuate le seguenti associazioni:
URL | vista |
/templates/home.html | |
/templates/hello.html | |
/modelli/login.html |
Il suffisso [html] e la cartella [templates] sono i valori predefiniti utilizzati da Thymeleaf. Possono essere modificati tramite la configurazione. La cartella [templates] deve trovarsi nella radice del classpath del progetto:
![]() |
Nel punto [1] sopra, le cartelle [java] e [resources] sono entrambe cartelle sorgente. Ciò significa che il loro contenuto si troverà nella radice del classpath del progetto. Pertanto, nel punto [2], le cartelle [hello] e [templates] si troveranno nella radice del classpath.
8.4.12.4. Configurazione di Spring Security
![]() |
La classe [WebSecurityConfig] configura il 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");
}
}
- riga 9: l'annotazione [@Configuration] rende la classe [WebSecurityConfig] una classe di configurazione;
- riga 10: l'annotazione [@EnableWebSecurity] rende la classe [WebSecurityConfig] una classe di configurazione di Spring Security;
- riga 11: la classe [WebSecurity] estende la classe [WebSecurityConfigurerAdapter] per sovrascrivere determinati metodi;
- riga 12: ridefinizione di un metodo della classe padre;
- righe 13–16: il metodo [configure(HttpSecurity http)] viene sovrascritto per definire i diritti di accesso per i vari URL dell'applicazione;
- riga 14: il metodo [http.authorizeRequests()] consente di associare gli URL ai diritti di accesso. Qui vengono effettuate le seguenti associazioni:
URL | regola | codice |
accesso senza autenticazione | | |
solo accesso autenticato |
- riga 15: definisce il metodo di autenticazione. L'autenticazione viene eseguita tramite un modulo URL [/login] accessibile a tutti [http.formLogin().loginPage("/login").permitAll()]. Anche il logout è accessibile a tutti;
- righe 19–21: ridefiniscono il metodo [configure(AuthenticationManagerBuilder auth)] che gestisce gli utenti;
- riga 20: l'autenticazione viene eseguita utilizzando utenti hardcoded [auth.inMemoryAuthentication()]. Qui viene definito un utente con il login [user], la password [password] e il ruolo [USER]. Agli utenti con lo stesso ruolo possono essere concesse le stesse autorizzazioni;
8.4.12.5. Classe eseguibile
![]() |
La classe [Application] è la seguente:
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);
}
}
- Riga 8: l'annotazione [@EnableAutoConfiguration] indica a Spring Boot (riga 3) di eseguire la configurazione che lo sviluppatore non ha impostato esplicitamente;
- riga 9: rende la classe [Application] una classe di configurazione Spring;
- riga 10: indica al sistema di scansionare la directory contenente la classe [Application] per cercare i componenti Spring. Le due classi [MvcConfig] e [WebSecurityConfig] verranno quindi individuate poiché presentano l'annotazione [@Configuration];
- riga 13: il metodo [main] della classe eseguibile;
- riga 14: il metodo statico [SpringApplication.run] viene eseguito con la classe di configurazione [Application] come parametro. Abbiamo già incontrato questo processo e sappiamo che il server Tomcat incorporato nelle dipendenze Maven del progetto verrà avviato e il progetto distribuito su di esso. Abbiamo visto che sono stati gestiti quattro URL [/, /home, /login, /hello] e che alcuni erano protetti da diritti di accesso.
8.4.12.6. Test dell'applicazione
Iniziamo richiedendo l'URL [/], che è uno dei quattro URL accettati. È associato alla vista [/templates/home.html]:
![]() |
L'URL richiesto [/] è accessibile a tutti. Ecco perché siamo riusciti a recuperarlo. Il link [qui] è il seguente:
L'URL [/hello] verrà richiesto quando clicchiamo sul link. Questo è protetto:
URL | regola | codice |
accesso senza autenticazione | | |
solo accesso autenticato |
Per accedervi è necessario essere autenticati. Spring Security reindirizzerà quindi il browser del client alla pagina di autenticazione. In base alla configurazione mostrata, si tratta della pagina all'URL [/login]. Questa pagina è accessibile a tutti:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Quindi otteniamo [1]:
![]() |
Il codice sorgente della pagina risultante è il seguente:
- alla riga 7, compare un campo nascosto che non è presente nella pagina originale [login.html]. È stato aggiunto da Thymeleaf. Questo codice, noto come CSRF (Cross-Site Request Forgery), è progettato per eliminare una vulnerabilità di sicurezza. Questo token deve essere rinviato a Spring Security insieme all'autenticazione affinché venga accettato;
Ricordiamo che solo la coppia utente/password viene riconosciuta da Spring Security. Se inseriamo qualcos'altro in [2], otteniamo la stessa pagina con un messaggio di errore in [3]. Spring Security ha reindirizzato il browser all'URL [http://localhost:8080/login?error]. La presenza del parametro [error] ha attivato la visualizzazione del tag:
<div th:if="${param.error}">Invalid username and password.</div>
Ora, inseriamo i valori previsti per utente/password [4]:
![]() |
- in [4], effettuiamo l'accesso;
- In [5], Spring Security ci reindirizza all'URL [/hello] poiché si tratta dell'URL che avevamo richiesto quando siamo stati reindirizzati alla pagina di accesso. L'identità dell'utente è stata visualizzata dalla seguente riga del file [hello.html]:
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
La pagina [5] mostra il seguente modulo:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
Quando si fa clic sul pulsante [Esci], viene inviata una richiesta POST all'URL [/logout]. Come l'URL [/login], questo URL è accessibile a tutti:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Nella nostra mappatura URL/vista, non abbiamo definito nulla per l'URL [/logout]. Cosa succederà? Proviamo:
![]() |
- In [6], clicchiamo sul pulsante [Sign Out];
- in [7], vediamo che siamo stati reindirizzati all'URL [http://localhost:8080/login?logout]. Spring Security ha richiesto questo reindirizzamento. La presenza del parametro [logout] nell'URL ha fatto sì che nella vista venisse visualizzata la seguente riga:
<div th:if="${param.logout}">You have been logged out.</div>
8.4.12.7. Conclusione
Nell'esempio precedente, avremmo potuto scrivere prima l'applicazione web e poi proteggerla in un secondo momento. Spring Security è non intrusivo. È possibile implementare la sicurezza per un'applicazione web che è già stata scritta. Inoltre, abbiamo scoperto i seguenti punti:
- è possibile definire una pagina di autenticazione;
- l'autenticazione deve essere accompagnata dal token CSRF emesso da Spring Security;
- se l'autenticazione fallisce, si viene reindirizzati alla pagina di autenticazione con un parametro di errore aggiuntivo nell'URL;
- se l'autenticazione ha esito positivo, si viene reindirizzati alla pagina richiesta al momento dell'autenticazione. Se si richiede la pagina di autenticazione direttamente senza passare attraverso una pagina intermedia, Spring Security reindirizza all'URL [/] (questo caso non è stato dimostrato);
- Si effettua il logout richiedendo l'URL [/logout] con una richiesta POST. Spring Security reindirizza quindi alla pagina di autenticazione con il parametro "logout" nell'URL;
Tutte queste conclusioni si basano sul comportamento predefinito di Spring Security. Questo comportamento può essere modificato tramite configurazione sovrascrivendo determinati metodi della classe [WebSecurityConfigurerAdapter].
Il tutorial precedente ci sarà di scarso aiuto per il prosieguo. Utilizzeremo infatti:
- un database per memorizzare gli utenti, le loro password e i loro ruoli;
- l'autenticazione basata su header HTTP;
Esistono relativamente pochi tutorial su ciò che vogliamo fare qui. La soluzione che proporremo è una combinazione di frammenti di codice trovati qua e là.
8.4.13. Implementazione della sicurezza sul servizio web di appuntamenti
8.4.13.1. Il database
Il database [rdvmedecins] è in fase di aggiornamento per includere gli utenti, le loro password e i loro ruoli. Sono state aggiunte tre nuove tabelle:

Tabella [USERS]: users
- ID: chiave primaria;
- VERSION: colonna di versioning delle righe;
- IDENTITY: identificatore descrittivo dell'utente;
- LOGIN: nome utente;
- PASSWORD: la sua password;
Nella tabella USERS, le password non sono memorizzate in chiaro:
![]() |
L'algoritmo utilizzato per crittografare le password è l'algoritmo BCRYPT.
Tabella [ROLES]: ruoli
- ID: chiave primaria;
- VERSION: colonna di versioning per la riga;
- NAME: nome del ruolo. Per impostazione predefinita, Spring Security si aspetta nomi nel formato ROLE_XX, ad esempio ROLE_ADMIN o ROLE_GUEST;
![]() |
Tabella [USERS_ROLES]: tabella di join USERS/ROLES
Un utente può avere più ruoli e un ruolo può includere più utenti. Si tratta di una relazione molti-a-molti rappresentata dalla tabella [USERS_ROLES].
- ID: chiave primaria;
- VERSION: colonna di versioning delle righe;
- USER_ID: identificatore utente;
- ROLE_ID: identificatore di un ruolo;
![]() |
Poiché stiamo modificando il database, è necessario modificare tutti i livelli del progetto [logica di business, DAO, JPA]:
![]() |
8.4.13.2. Il nuovo progetto STS per [logica di business, DAO, JPA]
Il progetto [rdvmedecins-business-dao] si evolve come segue:
![]() |
- in [1]: il nuovo progetto;
- in [2]: le modifiche introdotte dall'implementazione della sicurezza sono state raggruppate in un unico pacchetto [rdvmedecins.security]. Questi nuovi elementi appartengono ai livelli [JPA] e [DAO], ma per semplicità sono stati raggruppati nello stesso pacchetto.
8.4.13.3. Le nuove entità [JPA]
![]() |
Il livello JPA definisce tre nuove entità:
![]() |
La classe [User] rappresenta la tabella [USERS]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
private static final long serialVersionUID = 1L;
// properties
private String identity;
private String login;
private String password;
// manufacturer
public User() {
}
public User(String identity, String login, String password) {
this.identity = identity;
this.login = login;
this.password = password;
}
// identity
@Override
public String toString() {
return String.format("User[%s,%s,%s]", identity, login, password);
}
// getters and setters
....
}
- riga 9: la classe estende la classe [AbstractEntity] già utilizzata per le altre entità;
- righe 13–15: non vengono specificati nomi di colonne perché hanno gli stessi nomi dei campi associati;
La classe [Role] rispecchia la tabella [ROLES]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
private static final long serialVersionUID = 1L;
// properties
private String name;
// manufacturers
public Role() {
}
public Role(String name) {
this.name = name;
}
// identity
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// getters and setters
...
}
La classe [UserRole] rappresenta la tabella [USERS_ROLES]:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
private static final long serialVersionUID = 1L;
// a UserRole refers to a User
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// a UserRole refers to a Role
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// getters and setters
...
}
- righe 15-17: definisci la chiave esterna dalla tabella [USERS_ROLES] alla tabella [USERS];
- righe 19-21: implementano la chiave esterna dalla tabella [USERS_ROLES] alla tabella [ROLES];
8.4.13.4. Modifiche al livello [DAO]
![]() |
Il livello [DAO] è stato potenziato con tre nuovi [Repository]:
![]() |
L'interfaccia [UserRepository] gestisce l'accesso alle entità [User]:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
// liste des rôles d'un utilisateur identifié par son id
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// liste des rôles d'un utilisateur identifié par son login et son mot de passe
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// recherche d'un utilisateur via son login
User findUserByLogin(String login);
}
- riga 9: l'interfaccia [UserRepository] estende l'interfaccia [CrudRepository] di Spring Data (riga 4);
- righe 12-13: il metodo [getRoles(User user)] recupera tutti i ruoli per un utente identificato dal proprio [id]
- righe 16-17: come sopra, ma per un utente identificato dal proprio login e password;
- Riga 20: per trovare un utente tramite il suo nome utente;
L'interfaccia [RoleRepository] gestisce l'accesso alle entità [Role]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// search for a role by name
Role findRoleByName(String name);
}
- riga 5: l'interfaccia [RoleRepository] estende l'interfaccia [CrudRepository];
- riga 8: è possibile cercare un ruolo in base al suo nome;
L'interfaccia [userRoleRepository] gestisce l'accesso alle entità [UserRole]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- riga 5: l'interfaccia [UserRoleRepository] estende semplicemente l'interfaccia [CrudRepository] senza aggiungere alcun nuovo metodo;
8.4.13.5. Classi di gestione utenti e ruoli
![]() |
Spring Security richiede la creazione di una classe che implementi la seguente interfaccia [UsersDetail]:
![]() |
Questa interfaccia è implementata qui dalla classe [AppUserDetails]:
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// properties
private User user;
private UserRepository userRepository;
// manufacturers
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interface
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// getters and setters
...
}
- riga 10: la classe [AppUserDetails] implementa l'interfaccia [UserDetails];
- righe 15–16: la classe incapsula un utente (riga 15) e il repository che fornisce dettagli su quell'utente (riga 16);
- righe 22–25: il costruttore che istanzia la classe con un utente e il relativo repository;
- righe 28–35: implementazione del metodo [getAuthorities] dell'interfaccia [UserDetails]. Deve costruire una collezione di elementi di tipo [GrantedAuthority] o di un tipo derivato. Qui utilizziamo il tipo derivato [SimpleGrantedAuthority] (riga 32), che incapsula il nome di uno dei ruoli dell'utente della riga 15;
- righe 31–33: iteriamo attraverso l'elenco dei ruoli dell'utente della riga 15 per costruire un elenco di elementi di tipo [SimpleGrantedAuthority];
- righe 38–40: implementiamo il metodo [getPassword] dell'interfaccia [UserDetails]. Restituiamo la password dell'utente della riga 15;
- righe 38–40: implementiamo il metodo [getUserName] dell'interfaccia [UserDetails]. Restituiamo il nome utente dell'utente della riga 15;
- righe 47–50: l'account dell'utente non scade mai;
- righe 52–55: l'account dell'utente non viene mai bloccato;
- righe 57–60: le credenziali dell'utente non scadono mai;
- righe 62–65: l'account dell'utente è sempre attivo;
Spring Security richiede inoltre l'esistenza di una classe che implementi l'interfaccia [AppUserDetailsService]:
![]() |
Questa interfaccia è implementata dalla seguente classe [AppUserDetailsService]:
package rdvmedecins.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// search for user via login
User user = userRepository.findUserByLogin(login);
// found?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// render user details
return new AppUserDetails(user, userRepository);
}
}
- riga 9: la classe sarà un componente Spring, quindi sarà disponibile nel suo contesto;
- righe 12–13: il componente [UserRepository] verrà iniettato qui;
- righe 16–25: implementazione del metodo [loadUserByUsername] dell'interfaccia [UserDetailsService] (riga 10). Il parametro è il login dell'utente;
- riga 18: l'utente viene cercato utilizzando il suo nome utente;
- righe 20–22: se l'utente non viene trovato, viene generata un'eccezione;
- riga 24: viene costruito e restituito un oggetto [AppUserDetails]. È infatti di tipo [UserDetails] (riga 16);
8.4.13.6. Test del livello [DAO]
![]() |
Per prima cosa, creiamo una classe eseguibile [CreateUser] in grado di creare un utente con un ruolo:
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// syntax: login password roleName
// three parameters are required
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// parameters are retrieved
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// spring context
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// does the role already exist?
Role role = roleRepository.findRoleByName(roleName);
// if it doesn't exist, we create it
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// does the user already exist?
User user = userRepository.findUserByLogin(login);
// if it doesn't exist, we create it
if (user == null) {
// hash the password with bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// save user
user = userRepository.save(new User(login, login, crypt));
// we create the relationship with the role
userRoleRepository.save(new UserRole(user, role));
} else {
// the user already exists - does he/she have the required role?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// if not found, we create the relationship with the role
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// closing Spring context
context.close();
}
}
- riga 17: la classe richiede tre argomenti che definiscono un utente: login, password e ruolo;
- righe 25–27: i tre parametri vengono recuperati;
- riga 29: il contesto Spring viene creato dalla classe di configurazione [DomainAndPersistenceConfig]. Questa classe era già presente nel progetto iniziale. Deve essere aggiornata come segue:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- Riga 1: è necessario specificare che ora sono presenti componenti [Repository] nel pacchetto [rdvmedecins.security];
- riga 4: è necessario specificare che ora nel pacchetto [rdvmedecins.security] sono presenti entità JPA;
Torniamo al codice per la creazione di un utente:
- righe 30–32: recuperiamo i riferimenti dei tre oggetti [Repository] che potrebbero essere utili per la creazione dell'utente;
- riga 34: verifichiamo se il ruolo esiste già;
- righe 36–38: in caso contrario, lo creiamo nel database. Avrà un nome del tipo [ROLE_XX];
- riga 40: controlliamo se il login esiste già;
- righe 42-49: se il nome utente non esiste, lo creiamo nel database;
- riga 44: crittografiamo la password. Qui utilizziamo la classe [BCrypt] di Spring Security (riga 4). Abbiamo quindi bisogno degli archivi per questo framework. Il file [pom.xml] include una nuova dipendenza:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- Riga 46: l'utente viene salvato nel database;
- riga 48: così come la relazione che lo collega al suo ruolo;
- righe 51–57: se l'account esiste già, verifichiamo se il ruolo che vogliamo assegnargli è già tra i suoi ruoli;
- righe 59–61: se il ruolo desiderato non viene trovato, viene creata una riga nella tabella [USERS_ROLES] per collegare l'utente al proprio ruolo;
- Non abbiamo previsto alcuna protezione contro potenziali eccezioni. Si tratta di una classe di supporto per la creazione rapida di un utente con un ruolo.
Quando la classe viene eseguita con gli argomenti [x x guest], si ottengono i seguenti risultati nel database:
Tabella [USERS]
![]() |
Tabella [RUOLI]
![]() |
Tabella [USERS_ROLES]
![]() |
Consideriamo ora la seconda classe [UsersTest], che è un test JUnit:
![]() |
package rdvmedecins.security;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
@Test
public void findAllUsersWithTheirRoles() {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user);
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// user [admin] is retrieved
User user = userRepository.findUserByLogin("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// check admin / admin role
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// user [admin] is retrieved
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// check admin / admin role
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// utility method - displays items in a collection
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- righe 27–34: test visivo. Visualizziamo tutti gli utenti insieme ai loro ruoli;
- righe 36–46: verifichiamo che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando [UserRepository];
- riga 41: [admin] è la password in chiaro. Nel database, è crittografata utilizzando l'algoritmo BCrypt. Il metodo [BCrypt.checkpw] verifica che la password in chiaro crittografata corrisponda a quella nel database;
- righe 48–59: verifichiamo che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando [appUserDetailsService];
I test vengono eseguiti con successo con i seguenti log:
8.4.13.7. Conclusione provvisoria
Le classi necessarie per Spring Security sono state aggiunte con modifiche minime al progetto originale. Riassumendo:
- aggiunta di una dipendenza da Spring Security nel file [pom.xml];
- creazione di tre tabelle aggiuntive nel database;
- creazione di entità JPA e componenti Spring nel pacchetto [rdvmedecins.security];
Questo scenario molto favorevole deriva dal fatto che le tre tabelle aggiunte al database sono indipendenti da quelle esistenti. Avremmo potuto persino collocarle in un database separato. Ciò è stato possibile perché abbiamo deciso che un utente esistesse indipendentemente dai medici e dai clienti. Se questi ultimi fossero stati potenziali utenti, avremmo dovuto creare collegamenti tra la tabella [USERS] e le tabelle [MEDECINS] e [CLIENTS]. Ciò avrebbe avuto un impatto significativo sul progetto esistente.
8.4.13.8. Il progetto STS per il livello [web]
![]() |
Il progetto [rdvmedecins-webjson] si sta evolvendo come segue[1]:
![]() |
Le modifiche principali devono essere apportate nel file [rdvmedecins.web.config], dove è necessario configurare Spring Security. Ci sono altre modifiche minori nelle classi [AppConfig] e [ApplicationModel]. Abbiamo già incontrato una classe di configurazione di 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");
}
}
Seguiremo la stessa procedura:
- riga 11: definire una classe che estende la classe [WebSecurityConfigurerAdapter];
- riga 13: definire un metodo [configure(HttpSecurity http)] che definisca i diritti di accesso ai vari URL del servizio web;
- riga 19: definire un metodo [configure(AuthenticationManagerBuilder auth)] che definisce gli utenti e i loro ruoli;
La configurazione di Spring Security è gestita dalla classe [SecurityConfig]:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Autowired
private ApplicationModel application;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// authentication is performed by bean [appUserDetailsService]
// the password is encrypted using the BCrypt hash algorithm
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// secure application?
if (application.isSecured()) {
// the password is transmitted by the header Authorization: Basic xxxx
http.httpBasic();
// the HTTP OPTIONS method must be authorized for all
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// only the ADMIN role can use the application
http.authorizeRequests() //
.antMatchers("/", "/**") // all URL
.hasRole("ADMIN");
// no session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
- riga 15: la classe [SecurityConfig] è una classe di configurazione Spring;
- riga 16: per configurare la sicurezza del progetto;
- righe 19–20: viene iniettata la classe [AppUserDetails], che fornisce l'accesso agli utenti dell'applicazione;
- righe 21–22: viene iniettata la classe [ApplicationModel], che funge da cache per l'applicazione web. Abbiamo scelto di utilizzarla anche qui per configurare l'applicazione web in un unico punto. Essa definisce il valore booleano [isSecured] alla riga 36. Questo valore booleano protegge (true) o non protegge (false) l'applicazione web;
- righe 25–29: il metodo [configure(HttpSecurity http)] definisce gli utenti e i loro ruoli. Accetta un tipo [AuthenticationManagerBuilder] come parametro. Questo parametro è arricchito con due informazioni (riga 28):
- un riferimento all'[appUserDetailsService] della riga 20, che fornisce l'accesso agli utenti registrati. Si noti qui che il fatto che siano memorizzati in un database non è esplicitamente dichiarato. Potrebbero quindi trovarsi in una cache, essere forniti da un servizio web, ecc.
- il tipo di crittografia utilizzato per la password. Ricordiamo che abbiamo utilizzato l'algoritmo BCrypt;
- righe 38–47: il metodo [configure(HttpSecurity http)] definisce i diritti di accesso agli URL del servizio web;
- riga 34: abbiamo visto nel progetto introduttivo che, per impostazione predefinita, Spring Security gestisce un token CSRF (Cross-Site Request Forgery) che l'utente che tenta di autenticarsi deve inviare al server. Qui, questo meccanismo è disabilitato. In combinazione con il valore booleano (isSecured=false), ciò consente di utilizzare l'applicazione web senza sicurezza;
- riga 38: abilitiamo l'autenticazione tramite header HTTP. Il client deve inviare il seguente header HTTP:
dove code è la codifica Base64 della stringa login:password. Ad esempio, la codifica Base64 della stringa admin:admin è YWRtaW46YWRtaW4=. Pertanto, un utente con login [admin] e password [admin] invierà la seguente intestazione HTTP per autenticarsi:
- Righe 40–42: indicano che tutti gli URL del servizio web sono accessibili agli utenti con il ruolo [ROLE_ADMIN]. Ciò significa che un utente senza questo ruolo non può accedere al servizio web;
- Riga 47: la password dell'utente può essere memorizzata o meno in una sessione. Se viene memorizzata, l'utente deve autenticarsi solo la prima volta. Nelle richieste successive, le credenziali non vengono richieste. In questo caso, abbiamo scelto una modalità senza sessione. Ogni richiesta deve essere accompagnata da credenziali di sicurezza;
La classe [AppConfig], che configura l'intera applicazione, viene aggiornata come segue:
![]() |
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 {
}
- La modifica avviene alla riga 11: viene aggiunta la classe di configurazione [SecurityConfig];
Infine, la classe [ApplicationModel] viene potenziata con un valore booleano:
@Component
public class ApplicationModel implements IMetier {
...
// configuration data
private boolean secured = false;
public boolean isSecured() {
return secured;
}
- Riga 6: Imposta il valore booleano [secured] su [true / false] a seconda che tu voglia abilitare la sicurezza.
8.4.13.9. Test del servizio web
Testeremo il servizio web utilizzando il client Chrome [Advanced Rest Client]. Dovremo specificare l'intestazione di autenticazione HTTP:
dove [code] è la stringa [login:password] codificata in Base64. Per generare questo codice, è possibile utilizzare il seguente programma:
![]() |
package rdvmedecins.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// we expect two arguments: login password
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// we retrieve the two arguments
String chaîne = String.format("%s:%s", args[0], args[1]);
// encode the string
byte[] data = Base64.encode(chaîne.getBytes());
// displays its Base64 encoding
System.out.println(new String(data));
}
}
Se eseguiamo questo programma con i due argomenti [admin admin]:
![]() |
otteniamo il seguente risultato:
Ora che sappiamo come generare l'intestazione di autenticazione HTTP, avviamo il servizio web, ora sicuro:
@Component
public class ApplicationModel implements IMetier {
...
private boolean secured = true;
Quindi, utilizzando il client Chrome [Advanced Rest Client], richiediamo l'elenco di tutti i medici:
![]() |
- In [1], richiediamo l'URL dei medici;
- in [2], utilizzando un metodo GET;
- in [3], forniamo l'intestazione di autenticazione HTTP. Il codice [YWRtaW46YWRtaW4=] è la codifica Base64 della stringa [admin:admin];
- in [4], inviamo la richiesta HTTP;
La risposta del server è la seguente:
![]() |
- in [1], l'intestazione di autenticazione HTTP;
- in [2], il server restituisce una risposta JSON;
- in [3], un elenco di intestazioni HTTP relative alla sicurezza dell'applicazione web;
Otteniamo con successo l'elenco dei medici:
![]() |
Ora proviamo a inviare una richiesta HTTP con un'intestazione di autenticazione errata. La risposta è quindi la seguente:
![]() |
- in [1] e [3]: l'intestazione di autenticazione HTTP;
- in [2]: la risposta del servizio web;
Ora proviamo con l'utente "user / user". Esiste ma non ha accesso al servizio web. Se eseguiamo il programma di codifica Base64 con i due argomenti [user user]:
![]() |
otteniamo il seguente risultato:
![]() |
- in [1] e [3]: l'intestazione di autenticazione HTTP;
- in [2]: la risposta del servizio web. È diversa dalla precedente, che era [401 Non autorizzato]. In questo caso, l'utente si è autenticato con successo ma non dispone delle autorizzazioni sufficienti per accedere all'URL;
Un servizio web sicuro è ora operativo. Lo estenderemo per consentire le richieste cross-domain. Questo requisito è stato menzionato nel documento [Tutorial AngularJS / Spring 4] e, sebbene non sia applicabile in questo caso, lo affronteremo comunque.
8.4.14. Implementazione delle richieste cross-domain
Esaminiamo la questione delle richieste cross-domain. Nel documento [AngularJS / Spring 4 Tutorial], stiamo sviluppando un'applicazione client/server in cui il client è un'applicazione AngularJS:
![]() |
- le pagine HTML/CSS/JS dell'applicazione Angular provengono dal server [1];
- in [2], il servizio [dao] effettua una richiesta a un altro server, il server [2]. Tuttavia, ciò è vietato dal browser che esegue l'applicazione Angular poiché costituisce una vulnerabilità di sicurezza. L'applicazione può interrogare solo il server da cui ha origine, ovvero il server [1];
In realtà, non è corretto dire che il browser impedisca all'applicazione Angular di interrogare il server [2]. In realtà lo interroga per chiedere se consente a un client che non proviene da esso di interrogarlo. Questa tecnica di condivisione è chiamata CORS (Cross-Origin Resource Sharing). Il server [2] concede l'autorizzazione inviando specifici header HTTP.
Per dimostrare i problemi che possono sorgere, creeremo un'applicazione client/server in cui:
- il server sarà il nostro server web/JSON;
- il client sarà una semplice pagina HTML dotata di codice JavaScript che effettuerà richieste al server web/JSON;
8.4.14.1. Il progetto client
![]() |
Il progetto è un progetto Maven con il seguente file [pom.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-cors</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-cors</name>
<description>Client for webjson server</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.Client</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
- righe 14–19: questo è un progetto Spring Boot;
- righe 29–32: utilizziamo la dipendenza [spring-boot-starter-web], che include un server Tomcat e Spring MVC;
La pagina HTML è la seguente:
![]() |
È generato dal seguente codice:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
<h2>Client du service web / jSON</h2>
<form id="formulaire">
<!-- method HTTP -->
Méthode HTTP :
<!-- -->
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<!-- -->
<input type="radio" id="post" name="method" value="post" />POST
<!-- URL -->
<br /> <br />URL cible : <input type="text" id="url" size="30"><br />
<!-- posted value -->
<br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
<!-- validation button -->
<br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
</form>
<hr />
<h2>Réponse du serveur</h2>
<div id="response"></div>
</body>
</html>
- riga 6: importiamo la libreria jQuery;
- riga 7: importiamo il codice che scriveremo;
Il codice [client.js] è il seguente:
// global data
var url;
var posted;
var response;
var method;
function requestServer() {
// retrieve information from the form
var urlValue = url.val();
var postedValue = posted.val();
method = document.forms[0].elements['method'].value;
// make a manual Ajax call
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// make a manual Ajax call
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + url,
type : 'GET',
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// text result
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// system error
response.text(jqXHR.responseText);
}
})
}
function doPost(url, posted) {
// make a manual Ajax call
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + url,
type : 'POST',
contentType : 'application/json',
data : posted,
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// text result
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// system error
response.text(jqXHR.responseText);
}
})
}
// document loading
$(document).ready(function() {
// retrieve page component references
url = $("#url");
posted = $("#posted");
response = $("#response");
});
Lasciamo al lettore il compito di comprendere questo codice. Tutto è già stato trattato in un momento o nell'altro. Tuttavia, alcune righe meritano una spiegazione:
- riga 11:
- [document] si riferisce al documento caricato dal browser, noto come DOM (Document Object Model),
- [document.forms[0]] si riferisce al primo modulo nel documento; un documento può contenere più moduli. Qui ce n'è solo uno,
- [document.forms[0].elements['method']] si riferisce all'elemento del modulo con l'attributo [name='method']. Ce ne sono due:
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
- riga 11:
- [document.forms[0].elements['method'].value] è il valore che verrà inviato per il componente con l'attributo [name='method']. Sappiamo che il valore inviato è il valore dell'attributo [value] del pulsante di opzione selezionato. Qui, quindi, sarà una delle stringhe ['get', 'post'];
- righe 23–25: stiamo comunicando con un server che richiede un'intestazione HTTP [Authorization: Basic code]. Creiamo questa intestazione per l'utente [admin / admin], che è l'unico autorizzato a interrogare il server;
- riga 26: l'utente inserirà URL del tipo [/getAllDoctors, /deleteAppointment, ...]. Questi URL devono quindi essere completati;
- riga 28: il server restituisce JSON, che è un formato di testo. Specifichiamo il tipo [text/plain] come tipo di risposta in modo che venga visualizzato esattamente come ricevuto;
- riga 33: visualizza la risposta testuale del server;
- riga 39: visualizza eventuali messaggi di errore in formato testo;
- riga 52: per indicare che il client sta inviando JSON;
Nell'applicazione client/server che stiamo realizzando:
- il client è un'applicazione web disponibile all'URL [http://localhost:8081]. Questa è l'applicazione che stiamo attualmente realizzando;
- il server è un'applicazione web disponibile all'URL [http://localhost:8080]. Questo è il nostro server web/JSON;
Poiché il client non è in esecuzione sulla stessa porta del server, si pone il problema delle richieste cross-domain. [http://localhost:8080] e [http://localhost:8081] sono due domini diversi.
L'applicazione Spring Boot è un'applicazione console avviata dalla seguente classe eseguibile [Client]:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Client.class, args);
}
// static pages
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
}
// configuration dispatcherServlet
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
// embedded Tomcat server
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8081);
}
}
- Riga 14: la classe [Client] è una classe di configurazione Spring;
- riga 15: viene configurata un'applicazione Spring MVC. Questa annotazione attiva una serie di configurazioni automatiche;
- riga 16: per sovrascrivere determinati valori predefiniti del framework Spring MVC, è necessario estendere la classe [WebMvcConfigurerAdapter];
- righe 23–26: il metodo [addResourceHandlers] consente di specificare le directory in cui si trovano le risorse statiche dell'applicazione (HTML, CSS, JS, ecc.). Qui specifichiamo la directory [static] situata nel classpath del progetto:
![]() |
- righe 29–37: configurazione del bean [dispatcherServlet], che designa il servlet Spring MVC;
- righe 40-43: il server Tomcat incorporato funzionerà sulla porta 8081;
8.4.14.2. L'URL [/getAllMedecins]
Avviamo:
- il server web/JSON sulla porta 8080;
- il client per questo server sulla porta 8081;
quindi richiediamo l'URL [http://localhost:8081/client.html] [1]:
![]() |
- in [2], eseguiamo una richiesta GET sull'URL [http://localhost:8080/getAllMedecins];
Non riceviamo alcuna risposta dal server. Quando controlliamo la console degli sviluppatori (Ctrl-Shift-I), vediamo un errore:
![]() |
- in [1], ci troviamo nella scheda [Rete];
- In [2], vediamo che la richiesta HTTP effettuata non è [GET] ma [OPTIONS]. Nel caso di una richiesta cross-domain, il browser verifica con il server che determinate condizioni siano soddisfatte inviando una richiesta HTTP [OPTIONS]. In questo caso, le richieste sono quelle indicate dai cerchi [5-6];
- In [5], il browser chiede se l'URL di destinazione sia raggiungibile con un GET. L'intestazione della richiesta [Access-Control-Request-Method] richiede una risposta con un'intestazione HTTP [Access-Control-Allow-Methods] che indichi che il metodo richiesto è accettato;
- in [5], il browser invia l'intestazione HTTP [Origin: http://localhost:8081]. Questa intestazione richiede una risposta in un'intestazione HTTP [Access-Control-Allow-Origin] che indichi che l'origine specificata è accettata;
- In [6], il browser chiede se le intestazioni HTTP [Accept] e [Authorization] sono accettate. L'intestazione di richiesta [Access-Control-Request-Headers] si aspetta una risposta con un'intestazione HTTP [Access-Control-Allow-Headers] che indichi che le intestazioni richieste sono accettate;
- si verifica un errore in [3]. Facendo clic sull'icona si ottiene l'errore [4];
- in [4], il messaggio indica che il server non ha inviato l'intestazione HTTP [Access-Control-Allow-Origin], che specifica se l'origine della richiesta è accettata;
- In [7], possiamo vedere che il server effettivamente non ha inviato questa intestazione. Di conseguenza, il browser ha rifiutato di effettuare la richiesta HTTP GET inizialmente richiesta;
Dobbiamo modificare il server web/JSON. Apportiamo una modifica iniziale in [ApplicationModel], che è uno degli elementi di configurazione del servizio web:
![]() |
@Component
public class ApplicationModel implements IMetier {
...
// configuration data
private boolean corsAllowed = true;
private boolean secured = true;
...
public boolean isCorsAllowed() {
return corsAllowed;
}
- riga 6: creiamo una variabile booleana che indica se i client al di fuori del dominio del server sono accettati o meno;
- righe 10–12: il metodo per accedere a queste informazioni;
Quindi creiamo un nuovo controller Spring MVC:
![]() |
La classe [RdvMedecinsCorsController] è la seguente:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// sending options to the customer
public void sendOptions(String origin, HttpServletResponse response) {
// Cors allowed ?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// set header CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
// we authorize GET
response.addHeader("Access-Control-Allow-Methods", "GET");
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
}
- righe 12-13: la classe [RdvMedecinsCorsController] è un controller Spring;
- righe 33–36: definiscono un'azione che gestisce l'URL [/getAllMedecins] quando viene richiesto con il metodo HTTP [OPTIONS];
- riga 34: il metodo [getAllMedecins] accetta i seguenti parametri:
- l'oggetto [@RequestHeader(value = "Origin", required = false)] che recupera l'intestazione HTTP [Origin] dalla richiesta. Questa intestazione è stata inviata dal mittente della richiesta:
Specifichiamo che l'intestazione HTTP [Origin] è facoltativa [required = false]. In questo caso, se l'intestazione manca, il parametro [String origin] avrà il valore null. Con [required = true], che è il valore predefinito, viene generata un'eccezione se l'intestazione manca. Volevamo evitare questo scenario;
- riga 34:
- l'oggetto [HttpServletResponse response] che verrà inviato al client che ha effettuato la richiesta;
Questi due parametri vengono iniettati da Spring;
- riga 35: deleghiamo l'elaborazione della richiesta al metodo nelle righe 19–30;
- righe 15–16: viene iniettato l'oggetto [ApplicationModel];
- righe 21–23: se l'applicazione è configurata per accettare richieste cross-domain, e se il mittente ha inviato l'intestazione HTTP [Origin], e se tale origine inizia con [http://localhost], allora accettiamo la richiesta cross-domain; altrimenti, la rifiutiamo;
- riga 25: se il client si trova nel dominio [http://localhost:port], viene inviata l'intestazione HTTP:
Access-Control-Allow-Origin: http://localhost:port
il che significa che il server accetta l'origine del client;
- Riga 25: Abbiamo specificato due header HTTP specifici nella richiesta HTTP [OPTIONS]:
In risposta all'intestazione HTTP [Access-Control-Request-X], il server risponde con un'intestazione HTTP [Access-Control-Allow-X] che specifica ciò che è consentito. Le righe 23–26 ripetono semplicemente la richiesta del client per indicare che è stata accettata;
Ora siamo pronti per ulteriori test. Lanciamo la nuova versione del servizio web e scopriamo che il problema rimane invariato. Non è cambiato nulla. Se aggiungiamo un output della console alla riga 35 sopra, non viene mai visualizzato, indicando che il metodo [getAllMedecins] alla riga 34 non viene mai chiamato.
Dopo alcune ricerche, scopriamo che Spring MVC gestisce autonomamente le richieste HTTP [OPTIONS] utilizzando la sua gestione predefinita. Pertanto, è sempre Spring a rispondere, e mai il metodo [getAllMedecins] alla riga 34. Questo comportamento predefinito di Spring MVC può essere modificato. Modifichiamo la classe [WebConfig] esistente:
![]() |
package rdvmedecins.web.config;
...
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
public class WebConfig {
// dispatcherservlet configuration for CORS headers
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
// mapping jSON
...
- righe 10-11: il bean [dispatcherServlet] viene utilizzato per definire il servlet che gestisce le richieste dei client. In questo caso, è di tipo [DispatcherServlet], il servlet del framework Spring MVC;
- riga 12: creiamo un'istanza di tipo [DispatcherServlet];
- riga 13: istruiamo il servlet a inoltrare le richieste HTTP [OPTIONS] all'applicazione;
- riga 14: visualizziamo il servlet così configurato;
Eseguiamo nuovamente i test con questa nuova configurazione. Otteniamo il seguente risultato:
![]() |
- in [1], vediamo che ci sono due richieste HTTP all'URL [http://localhost:8080/getAllMedecins];
- in [2], la richiesta [OPTIONS];
- in [3], le tre intestazioni HTTP che abbiamo appena configurato nella risposta del server;
Ora esaminiamo la seconda richiesta:
![]() |
- in [1], la richiesta in esame;
- in [2], questa è la richiesta GET. Grazie alla prima richiesta [OPTIONS], il browser ha ricevuto le informazioni richieste. Ora sta eseguendo la richiesta [GET] inizialmente richiesta;
- in [3], la risposta del server;
- in [4], il server invia JSON;
- in [5], si è verificato un errore;
- in [6], il messaggio di errore;
È più difficile spiegare cosa sia successo in questo caso. La risposta del server [3] è normale [HTTP/1.1 200 OK]. Dovremmo quindi avere il documento richiesto. È possibile che il server abbia effettivamente inviato il documento, ma che il browser ne impedisca l'utilizzo perché richiede che, anche per la richiesta GET, la risposta includa l'intestazione HTTP [Access-Control-Allow-Origin:http://localhost:8081].
Modifichiamo il controller [RdvMedecinsController] come segue:
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins(HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// the answer
Response<List<Medecin>> response;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// application status
...
- righe 1-2: viene iniettato il controller [RdvMedecinsCorsController];
- righe 7-8: l'oggetto HttpServletResponse, che incapsula la risposta da inviare al client, e l'intestazione HTTP [Origin] vengono iniettati nei parametri del metodo [getAllMedecins];
- riga 12: viene chiamato il metodo [sendOptions] del controller [RdvMedecinsCorsController] — lo stesso metodo che è stato chiamato per gestire la richiesta HTTP [OPTIONS]. Invierà quindi le stesse intestazioni HTTP di quella richiesta;
Dopo questa modifica, i risultati sono i seguenti:
![]() |
Abbiamo ottenuto con successo l'elenco dei medici.
8.4.14.3. Gli altri URL [GET]
Esamineremo ora gli altri URL interrogati tramite una richiesta GET. Nei controller, il codice delle azioni che li gestiscono segue lo stesso schema delle azioni che in precedenza gestivano l'URL [/getAllMedecins]. Il lettore può verificare il codice negli esempi forniti con questo documento. Ecco un esempio:
in [RdvMedecinsCorsController]
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
in [RdvMedecinsController]
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour,
HttpServletResponse httpServletResponse, @RequestHeader(value = "Origin", required = false) String origin)
throws JsonProcessingException {
// the answer
Response<List<Rv>> response = null;
boolean erreur = false;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// application status
...
Ecco alcuni screenshot dell'esecuzione:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
8.4.14.4. Gli URL [POST]
Esaminiamo il seguente scenario:
![]() |
- Effettuiamo un POST [1] all'URL [2];
- in [3], il valore inviato. Si tratta di una stringa JSON;
- In generale, stiamo cercando di eliminare l'appuntamento con [id] 100;
A questo punto non stiamo modificando alcun codice. Il risultato ottenuto è il seguente:
![]() |
- in [1], come per le richieste [GET], il browser effettua una richiesta [OPTIONS];
- in [2], richiede l'autorizzazione di accesso per una richiesta [POST]. In precedenza era [GET];
- In [3], richiede l'autorizzazione per inviare le intestazioni HTTP [accept, authorization, content-type]. In precedenza, avevamo solo le prime due intestazioni;
Modifichiamo il metodo [RdvMedecinsCorsController.sendOptions] come segue:
public void sendOptions(String origin, HttpServletResponse response) {
// Cors allowed ?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// set header CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// we authorize GET
response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
- riga 9: abbiamo aggiunto l'intestazione HTTP [Content-Type] (non fa differenza maiuscolo/minuscolo);
- riga 11: abbiamo aggiunto il metodo HTTP [POST];
Ciò significa che i metodi [POST] vengono gestiti allo stesso modo delle richieste [GET]. Ecco un esempio dell'URL [/deleteAppointment]:
in [RdvMedecinsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// the answer
Response<Void> response = null;
boolean erreur = false;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// application status
if (messages != null) {
...
in [RdvMedecinsCorsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
Il risultato è il seguente:
![]() |
Per l'URL [/addRv], si ottiene il seguente risultato:
![]() |
8.4.14.5. Conclusione
La nostra applicazione ora supporta le richieste cross-domain. Queste possono essere abilitate o disabilitate tramite la configurazione nella classe [ApplicationModel]:
// données de configuration
private boolean corsAllowed = false;
8.5. Client del servizio web / JSON
Torniamo all'architettura generale dell'applicazione che vogliamo realizzare:
![]() |
La parte superiore del diagramma è stata già implementata. Si tratta del server web/JSON. Ora ci occuperemo della parte inferiore, iniziando dal suo livello [DAO]. Lo implementeremo e poi lo testeremo con un client da console. L'architettura di test sarà la seguente:
![]() |
8.5.1. Il progetto del client console
Il progetto STS per il client console sarà il seguente:
![]() |
8.5.2. Configurazione di Maven
Il file [pom.xml] per il client console è il seguente:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webjson-client-console</name>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- jSON library used by Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- component used by Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
</project>
- righe 15–20: il progetto Spring Boot principale;
- righe 24–27: il client del server web/console JSON si basa su un componente chiamato [RestTemplate] fornito dalla dipendenza [spring-web];
- righe 29–36: la serializzazione e la deserializzazione degli oggetti JSON richiedono una libreria JSON. Utilizziamo una variante della libreria Jackson utilizzata da Spring Web;
- righe 38–41: al livello più basso, il componente [RestTemplate] comunica con il server tramite socket TCP/IP. Vogliamo impostare il [timeout] per questi, ovvero il tempo massimo di attesa per una risposta dal server. Il componente [RestTemplate] non ci permette di impostarlo. Per farlo, passeremo un componente di basso livello fornito dalla dipendenza [org.apache.httpcomponents.httpclient] al costruttore [RestTemplate]. È questa dipendenza che ci permetterà di impostare il [timeout] di comunicazione;
8.5.3. Il pacchetto [rdvmedecins.client.entities]
![]() |
Il pacchetto [rdvmedecins.client.entities] contiene tutte le entità che il servizio web / JSON invia tramite i suoi vari URL. Non entreremo nuovamente nei dettagli al riguardo. Basti dire che alle entità JPA [Client, Slot, Doctor, Appointment, Person] sono state rimosse tutte le annotazioni JPA e quelle JSON. Ecco, ad esempio, la classe [Appointment]:
package rdvmedecins.client.entities;
import java.util.Date;
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// day of appointment
private Date jour;
// an appointment is linked to a customer
private Client client;
// an appointment is linked to a time slot
private Creneau creneau;
// foreign keys
private long idClient;
private long idCreneau;
// default builder
public Rv() {
}
// with parameters
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// getters and setters
...
}
8.5.4. Il pacchetto [rdvmedecins.client.requests]
![]() |
Il pacchetto [rdvmedecins.client.requests] contiene le due classi i cui valori JSON vengono inviati agli URL [/ajouterRv] e [supprimerRv]. Sono identiche alle loro controparti lato server.
8.5.5. Il pacchetto [rdvmedecins.client.responses]
![]() |
[Response] è il tipo di tutte le risposte dei servizi web / JSON. Si tratta di un tipo generico:
package rdvmedecins.client.responses;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- riga 5: il tipo [T] varia a seconda dell'URL del servizio web / JSON;
8.5.6. Il pacchetto [rdvmedecins.client.dao]
![]() |
- [IDao] è l'interfaccia del livello [DAO] e [Dao] ne è l'implementazione. Torneremo su questa implementazione;
8.5.7. Il pacchetto [rdvmedecins.client.config]
![]() |
La classe [DaoConfig] configura l'applicazione. Il suo codice è il seguente:
package rdvmedecins.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
@Bean
public RestTemplate restTemplate() {
// creation of the RestTemplate component
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// result
return restTemplate;
}
// mappers jSON
@Bean
public ObjectMapper jsonMapper(){
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",
creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- riga 13: la classe [DaoConfig] è una classe di configurazione Spring;
- riga 14: il pacchetto [rdvmedecins.client.dao] verrà cercato per i componenti Spring. Il componente [Dao] verrà trovato lì;
- Righe 17–24: definiscono un singleton Spring denominato [restTemplate] (il nome del metodo). Questo metodo restituisce un'istanza [RestTemplate], che è lo strumento di base fornito da Spring per comunicare con un servizio web o JSON;
- Riga 21: potremmo scrivere [RestTemplate restTemplate = new RestTemplate();]. Questo è sufficiente nella maggior parte dei casi. Ma qui vogliamo impostare i [timeout] del client. Per farlo, iniettiamo un componente di basso livello di tipo [HttpComponentsClientHttpRequestFactory] (riga 20) nel componente [RestTemplate], il che ci permetterà di impostare questi [timeout]. La dipendenza Maven richiesta è stata fornita;
- righe 28–57: definiscono i mappatori JSON. Questi sono i mappatori JSON utilizzati sul lato server (vedere la sezione 8.4.11.3) per serializzare il tipo T della risposta [Response<T>]. Questi stessi convertitori saranno ora utilizzati sul lato client per deserializzare il tipo T;
8.5.8. L'interfaccia [IDao]
Torniamo all'architettura dell'applicazione:
![]() |
Il livello [DAO] funge da adattatore tra il livello [console] e gli URL esposti dal servizio web / JSON. La sua interfaccia [IDao] sarà la seguente:
package rdvmedecins.client.dao;
import java.util.List;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
public interface IDao {
// Web service url
public void setUrlServiceWebJson(String url);
// timeout
public void setTimeout(int timeout);
// authentication
public void authenticate(User user);
// customer list
public List<Client> getAllClients(User user);
// list of doctors
public List<Medecin> getAllMedecins(User user);
// list of physician slots
public List<Creneau> getAllCreneaux(User user, long idMedecin);
// find a customer identified by its id
public Client getClientById(User user, long id);
// find a customer identified by its id
public Medecin getMedecinById(User user, long id);
// find an Rv identified by its id
public Rv getRvById(User user, long id);
// find a time slot identified by its id
public Creneau getCreneauById(User user, long id);
// add a RV to the list
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
// delete a RV
public void supprimerRv(User user, long idRv);
// list of doctor's appointments on a given day
public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
// agenda
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
}
- riga 14: il metodo per impostare l'URL radice del servizio web / JSON, ad esempio [http://localhost:8080];
- riga 17: il metodo utilizzato per impostare i [timeout] lato client. Vogliamo controllare questo parametro perché alcuni client HTTP possono impiegare molto tempo ad attendere una risposta che non arriverà mai;
- riga 20: il metodo per l'autenticazione di un utente [login, passwd]. Genera un'eccezione se l'utente non viene riconosciuto;
- righe 22–53: ogni URL esposto dal servizio web / JSON è associato a un metodo dell'interfaccia, la cui firma deriva dalla firma del metodo lato server che gestisce l'URL esposto. Prendiamo, ad esempio, il seguente URL del server:
@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) {
- riga 1: vediamo che [idMedecin] e [jour] sono i parametri URL. Questi saranno i parametri di input per il metodo associato a questo URL sul lato client;
- riga 2: vediamo che il metodo server restituisce un tipo [Response<String>]. Questo tipo [String] è il tipo del valore JSON di tipo [AgendaMedecinJour]. Il tipo di risultato del metodo associato a questo URL sul lato client sarà [AgendaMedecinJour];
Sul lato client, dichiariamo il seguente metodo:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
Questa firma funziona quando il server invia una risposta di tipo [int status, List<String> messages, String body] con [status==0]. In questo caso, abbiamo [messages==null && body!=null]. Non funziona quando [status!=0]. In questo caso, abbiamo [messages!=null && body==null]. Dobbiamo segnalare in qualche modo che si è verificato un errore. Per farlo, lanceremo un'eccezione di tipo [RdvMedecinsException] come segue:
package rdvmedecins.client.dao;
import java.util.List;
public class RdvMedecinsException extends RuntimeException {
private static final long serialVersionUID = 1L;
// error code
private int status;
// list of error messages
private List<String> messages;
public RdvMedecinsException() {
}
public RdvMedecinsException(int code, List<String> messages) {
super();
this.status = code;
this.messages = messages;
}
// getters and setters
...
}
- righe 9 e 11: l'eccezione assumerà i valori dei campi [status, messages] dall'oggetto [Response<T>] inviato dal server;
- riga 5: la classe [RdvMedecinsException] estende la classe [RuntimeException]. Si tratta quindi di un'eccezione non gestita, il che significa che non è necessario gestirla con un blocco try/catch né dichiararla nelle firme dei metodi dell'interfaccia;
Inoltre, tutti i metodi dell'interfaccia [IDao] che interrogano il servizio web/JSON hanno come parametro il seguente tipo [User]:
package rdvmedecins.client.entities;
public class User {
// data
private String login;
private String passwd;
// manufacturers
public User() {
}
public User(String login, String passwd) {
this.login = login;
this.passwd = passwd;
}
// getters and setters
...
}
Infatti, ogni scambio con il servizio web / JSON deve essere accompagnato da un'intestazione di autenticazione HTTP.
8.5.9. Il pacchetto [rdvmedecins.clients.console]
Ora che abbiamo familiarizzato con l'interfaccia del livello [DAO], possiamo presentare l'applicazione console.
![]() |
La classe [Main] è la seguente:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
// serializer jSON
static private ObjectMapper mapper = new ObjectMapper();
// connection timeout in milliseconds
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// we retrieve a reference on the [DAO] layer
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// set the URL of the web/json service
dao.setUrlServiceWebJson("http://localhost:8080");
// set timeouts in milliseconds
dao.setTimeout(TIMEOUT);
// Authentication
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,x]";
try {
dao.authenticate(new User("user", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [x,x]";
try {
dao.authenticate(new User("x", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// customer list
message = "/getAllClients";
try {
showResponse(message, dao.getAllClients(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// list of doctors
message = "/getAllMedecins";
try {
showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// list of slots for doctor 2
message = "/getAllCreneaux/2";
try {
showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// customer no. 1
message = "/getClientById/1";
try {
showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doctor no. 2
message = "/getMedecinById/2";
try {
showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// slot no. 3
message = "/getCreneauById/3";
try {
showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// rv n° 4
message = "/getRvById/4";
try {
showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// adding an appointment
message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
long idRv = 0;
try {
Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
idRv = response.getId();
showResponse(message, response);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doctor's appointment list 1 on 2015-01-08
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doctor's agenda 1 on 2015-01-08
message = "/getAgendaMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// delete added rv
message = String.format("/supprimerRv [idRv=%s]", idRv);
try {
dao.supprimerRv(new User("admin", "admin"), idRv);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doctor's appointment list 1 on 2015-01-08
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// closing context
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
private static <T> void showResponse(String message, T response) throws JsonProcessingException {
System.out.println(String.format("URL [%s]", message));
System.out.println(mapper.writeValueAsString(response));
}
}
- riga 19: il serializzatore JSON che ci consentirà di visualizzare la risposta del server, riga 184;
- riga 25: il componente [AnnotationConfigApplicationContext] è un componente Spring in grado di utilizzare le annotazioni di configurazione da un'applicazione Spring. Passiamo la classe [AppConfig], che configura l'applicazione, al suo costruttore;
- riga 26: recuperiamo un riferimento al livello [DAO];
- righe 27–30: lo configuriamo;
- righe 32–169: testiamo tutti i metodi dell'interfaccia [IDao];
I risultati ottenuti sono i seguenti:
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
Lasciamo al lettore il compito di mettere in relazione i risultati con il codice. Il codice mostra come chiamare ciascun metodo del livello [DAO]. Notiamo solo alcuni punti:
- righe 2–14: mostrano che, in caso di errore di autenticazione, il server restituisce uno stato HTTP [403 Forbidden] o [401 Unauthorized], a seconda dei casi;
- righe 30–31: viene aggiunto un appuntamento per il dottore n. 1;
- righe 32–33: vediamo questo appuntamento. È l'unico della giornata;
- righe 34–35: è visibile anche nel calendario del medico;
- righe 36-37: l'appuntamento è scomparso. Il codice lo ha cancellato nel frattempo;
I log della console sono controllati dai seguenti file:
![]() |
[application.properties]
logging.level.org.springframework.web=OFF
logging.level.org.hibernate=OFF
spring.main.show-banner=false
logging.level.httpclient.wire=OFF
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="info"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
8.5.10. Implementazione del livello [DAO]
Ora dobbiamo presentare il cuore del livello [DAO]: l'implementazione della sua interfaccia [IDao]. Lo faremo passo dopo passo.
![]() |
L'interfaccia [IDao] è implementata dalla classe astratta [AbstractDao] e dalla sua classe figlia [Dao].
La classe padre [AbstractDao] è la seguente:
package rdvmedecins.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import rdvmedecins.client.entities.User;
public abstract class AbstractDao implements IDao {
// data
@Autowired
protected RestTemplate restTemplate;
protected String urlServiceWebJson;
// URL web service / jSON
public void setUrlServiceWebJson(String url) {
this.urlServiceWebJson = url;
}
public void setTimeout(int timeout) {
// set the timeout for web client requests
HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
.getRequestFactory();
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
}
private String getBase64(User user) {
// encodes user and password in base 64 - requires
// java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
// generic request
protected String getResponse(User user, String url, String jsonPost) {
...
}
}
- riga 20: la classe è astratta, il che ci impedisce di designarla come componente Spring. La sua classe figlia sarà designata come tale;
- righe 23–24: iniettiamo il bean [restTemplate] che abbiamo definito nella classe di configurazione [AppConfig];
- riga 25: l'URL radice del servizio web / JSON;
- righe 32–38: impostiamo il timeout del client in attesa di una risposta dal server;
- riga 34: recuperiamo il componente [HttpComponentsClientHttpRequestFactory] che abbiamo iniettato nel bean [restTemplate] al momento della sua creazione (vedi [AppConfig]);
- riga 36: impostiamo il tempo massimo di attesa per il client durante la creazione di una connessione con il server;
- riga 37: impostiamo il tempo massimo di attesa per il client mentre attende una risposta a una delle sue richieste;
L'implementazione dei metodi per la comunicazione con il server sarà integrata nel seguente metodo generico:
// generic request
protected String getResponse(User user, String url, String jsonPost) {
...
}
- Riga 2: I parametri di [getResponse] sono i seguenti:
- [User user]: l'utente che effettua la connessione;
- [String url]: l'URL da interrogare. Questa è la parte finale dell'URL; la prima parte è fornita dal campo [urlServiceWebJson] della classe,
- [String jsonPost]: la stringa JSON da inviare. Se questo valore è presente, l'URL verrà richiesto con un POST; altrimenti, con un GET;
Continuiamo:
// generic request
protected String getResponse(User user, String url, String jsonPost) {
// url : URL to contact
// jsonPost: the jSON value to be posted
try {
// request execution
RequestEntity<?> request;
if (jsonPost == null) {
HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
if (user != null) {
headersBuilder = headersBuilder.header("Authorization", getBase64(user));
}
request = headersBuilder.build();
} else {
BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
if (user != null) {
bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
}
request = bodyBuilder.body(jsonPost);
}
// execute the query
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e) {
throw new RdvMedecinsException(20, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(21, getMessagesForException(e));
}
}
- Righe 23–24: L'istruzione che invia la richiesta al server e ne riceve la risposta. Il componente [RestTemplate] offre un'ampia gamma di metodi per interagire con il server. Avremmo potuto scegliere un metodo diverso da [exchange]. Il secondo parametro della chiamata specifica il tipo di risposta prevista, in questo caso una stringa JSON. Il primo parametro è la richiesta [RequestEntity] (riga 7). Il risultato del metodo [exchange] è di tipo [ResponseEntity<String>]. Il tipo [ResponseEntity] incapsula la risposta completa del server, inclusi gli header HTTP e il documento inviato dal server. Analogamente, il tipo [RequestEntity] incapsula l'intera richiesta del client, inclusi gli header HTTP e qualsiasi dato inviato;
- riga 23: questo è il corpo dell'oggetto [ResponseEntity<String>] che viene restituito al metodo chiamante, ovvero la stringa JSON inviata dal server;
- Righe 9–21: Dobbiamo costruire la richiesta [RequestEntity]. Essa varia a seconda che si utilizzi una richiesta GET o POST;
- riga 9: la richiesta per un GET. La classe [RequestEntity] fornisce metodi statici per creare richieste GET, POST, HEAD e altre. Il metodo [RequestEntity.get] consente di creare una richiesta GET concatenando i vari metodi che la compongono:
- Il metodo [RequestEntity.get] accetta l'URL di destinazione come parametro sotto forma di un'istanza URI;
- il metodo [accept] consente di definire gli elementi dell'intestazione HTTP [Accept]. Qui, specifichiamo che accettiamo il tipo [application/json] che il server invierà;
- il risultato di questo concatenamento di metodi è un tipo [HeadersBuilder];
- righe 10–12: se il parametro [User user] non è nullo, includiamo l'intestazione HTTP [Authorization] nella richiesta;
- riga 13: il metodo [HeadersBuilder.build] utilizza queste informazioni per costruire il tipo [RequestEntity] della richiesta;
- riga 15: la richiesta è di tipo POST. Il metodo [RequestEntity.post] consente di creare una richiesta POST concatenando i vari metodi che la compongono:
- il metodo [RequestEntity.post] accetta l'URL di destinazione come parametro sotto forma di un'istanza URI,
- il metodo [header] consente di definire le intestazioni HTTP che si desidera utilizzare, in questo caso l'intestazione di autorizzazione,
- il metodo [header] successivo include l'intestazione [Content-Type: application/json] nella richiesta per indicare che i dati inviati arriveranno come stringa JSON;
- il metodo [accept] indica che accettiamo il tipo [application/json] che il server invierà;
- righe 17–19: se il parametro [User user] non è nullo, l'intestazione HTTP [Authorization] viene inclusa nella richiesta;
- riga 20: il metodo [BodyBuilder.body] imposta il valore inviato. Questo è il secondo parametro del metodo generico [getResponse] (riga 2);
- righe 25–28: se si verifica un errore, viene generata un'eccezione [RdvMedecinsException];
Il metodo [getMessagesForException] alle righe 26 e 28 è il seguente:
// list of exception error messages
protected static List<String> getMessagesForException(Exception exception) {
// retrieve the list of exception error messages
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// the message is retrieved only if it is !=null and not blank
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// next cause
cause = cause.getCause();
}
return erreurs;
}
Il metodo privato [getBase64] restituisce la codifica Base64 della stringa 'login:passwd' per l'intestazione di autenticazione HTTP:
private String getBase64(User user) {
// encodes user and password in base 64 - requires java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
La classe [Dao] estende la classe [AbstractDao] come segue:
package rdvmedecins.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
@Service
public class Dao extends AbstractDao implements IDao {
// mappers jSON
@Autowired
ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
public List<Client> getAllClients(User user) {
...
}
public List<Medecin> getAllMedecins(User user) {
...
}
...
}
- riga 22: la classe [Dao] è un componente Spring. Qui è stata utilizzata l'annotazione [@Service]. Avremmo potuto continuare a utilizzare l'annotazione [@Component] usata fino a questo punto;
- righe 26–36: iniezione dei quattro mappatori JSON definiti nella classe di configurazione [DaoConfig];
I metodi della classe [Dao] seguono tutti lo stesso schema. Descriveremo in dettaglio un'operazione GET e un'operazione POST.
Innanzitutto, una richiesta [GET]:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
// the answer
Response<AgendaMedecinJour> response;
// the diary
String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
try {
// diary AgendaMedecinJour
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
});
} catch (IOException e) {
throw new RdvMedecinsException(401, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(402, getMessagesForException(e));
}
// response analysis
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- Riga 5: Viene chiamato il metodo generico [getResponse]. I parametri effettivi utilizzati sono i seguenti:
- 1: l'utente;
- 2: l'URL di destinazione;
- 3: il valore da inviare. In questo caso, non ce n'è nessuno;
- riga 5: la chiamata non è stata racchiusa in un blocco try/catch. Il metodo [getResponse] potrebbe generare un'eccezione [RdvMedecinsException]. Se generata, questa eccezione si propagherà al metodo che ha chiamato il metodo [getAgendaMedecinJour] sopra;
- riga 8: l'URL [/getAgendaMedecinJour] restituisce un [Response<AgendaMedecinJour>] che è stato serializzato in JSON sul lato server dal mappatore JSON [jsonMapperLongRv]. Usiamo questo stesso mappatore per deserializzare la stringa JSON ricevuta;
- righe 10–13: se si verifica un errore alla riga 9, viene generata un'eccezione [RdvMedecinsException];
- righe 16–21: la risposta inviata dal server viene analizzata;
- righe 17–18: se il server ha segnalato un errore, viene generata un'eccezione con le informazioni fornite dal server;
- righe 19–21: altrimenti, restituisce l'orario del medico;
La richiesta POST in esame sarà la seguente:
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
// the answer
Response<Rv> response;
try {
// the Rv
String jsonResponse = getResponse(user, "/ajouterRv",
jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
// the Rv Rv
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
});
} catch (RdvMedecinsException e) {
throw e;
} catch (IOException e) {
throw new RdvMedecinsException(381, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(382, getMessagesForException(e));
}
// response analysis
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- Riga 6: Il metodo [getResponse] viene chiamato con i seguenti parametri:
- 1: l'utente;
- 2: l'URL di destinazione,
- 3: il valore inviato: passiamo il valore JSON di tipo [PostAjouter] costruito con le informazioni ricevute come parametri dal metodo. Utilizziamo un mappatore JSON senza filtri;
- riga 9: sul lato server, il mappatore JSON [jsonMapperLongRv] ha serializzato la risposta del server. Sul lato client, utilizziamo questo stesso mappatore per deserializzarla;
- riga 6: l'URL [/ajouterRv] restituisce il valore JSON di tipo [Response<Rv>];
- righe 4–11: qui, il metodo [getResponse] è stato inserito in un blocco try/catch perché la serializzazione del valore inviato potrebbe generare un'eccezione. Il metodo [getResponse] è suscettibile di generare un'eccezione [RdvMedecinsException]. In questo caso, lo riproviamo semplicemente (righe 11–12);
Il codice seguente (righe 13–24) è simile a quello appena discusso. L'unica differenza rispetto a un'operazione GET è quindi il secondo parametro del metodo [getResponse], che deve essere la rappresentazione JSON del valore da inviare.
Gli altri metodi sono costruiti sullo stesso modello.
8.5.11. Eccezione
Durante l'esecuzione di vari test, abbiamo riscontrato un'anomalia riassunta nella seguente classe [Anomalie]:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Anomalie {
// serializer jSON
static private ObjectMapper mapper = new ObjectMapper();
// connection timeout in milliseconds
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// we retrieve a reference on the [DAO] layer
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// set the URL of the web/json service
dao.setUrlServiceWebJson("http://localhost:8080");
// set timeouts in milliseconds
dao.setTimeout(TIMEOUT);
// Authentication
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Authentication
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Authentication
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// closing context
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
}
- righe 31–38: l'utente [admin, admin] viene autenticato;
- righe 40-47: autenticazione dell'utente [admin, x], che ha inserito una password errata;
- righe 49-56: l'utente [user, user] viene autenticato; questo utente esiste ma non è autorizzato;
Ecco i risultati:
- riga 2: contrariamente alle aspettative, l'utente [admin, x] è stato accettato;
Se commentiamo le righe 33–38 del codice, otteniamo il seguente risultato:
che è il risultato previsto. Sembra che, una volta che l'utente [admin, admin] ha effettuato con successo il primo accesso, la sua password non sia più richiesta per gli accessi successivi. È proprio così. Per impostazione predefinita, Spring Security utilizza un meccanismo di sessione che garantisce che, una volta che un utente si è autenticato, non debba farlo nuovamente nelle richieste successive. È possibile modificare la configurazione [Spring Security] nel server web / JSON in modo che ciò non avvenga più:
![]() |
Il file [SecurityConfig] deve essere modificato come segue:
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// no session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
- La riga 5 specifica che non deve esserci alcuna sessione di sicurezza;
Questo ha risolto il problema.
8.6. Rendering lato server con Spring / Thymeleaf
8.6.1. Introduzione
Torniamo all'architettura dell'applicazione client/server da realizzare:
![]() |
- il server web/JSON [Web2] è stato realizzato;
- è stato realizzato il livello [DAO] del client [Web1];
La relazione tra il server [Web1] e i browser client è una relazione client/server in cui il server è un server web/JSON. Infatti, [Web1] fornirà flussi HTML incapsulati in una stringa JSON. L'architettura client/server è la seguente:
![]() |
- abbiamo un'architettura client [2] / server [1] in cui il client e il server comunicano tramite JSON;
- In [1], il livello web Spring MVC/Thymeleaf fornisce viste, frammenti di vista e dati in JSON. Il server è quindi un server web/JSON come il server [Web1]. È inoltre stateless;
- in [2]: il codice JavaScript incorporato nella vista caricata all'avvio dell'applicazione è strutturato a livelli:
- Il livello [presentazione] gestisce le interazioni dell'utente,
- il livello [DAO] gestisce l'accesso ai dati tramite il server [Web2];
- il client [2] memorizzerà nella cache alcune viste per ridurre il carico sul server;
Realizzeremo il server web/JSON [Web1], implementato con Spring MVC/Thymeleaf, in diverse fasi:
- esplorando il framework CSS Bootstrap;
- scrivendo le viste;
- scrivendo il controller;
Successivamente, separatamente, realizzeremo il client JS per il server [Web1]. Per dimostrare chiaramente che questo client ha un certo grado di indipendenza dal server [Web1], lo realizzeremo utilizzando lo strumento [WebStorm] anziché STS.
D'ora in poi, alcuni dettagli verranno tralasciati poiché potrebbero distrarci dall'argomento principale, ovvero l'organizzazione del codice. I lettori interessati possono trovare il codice completo sul sito web dedicato a questo documento.
8.6.2. Il progetto STS
![]() |
- in [1], il codice Java;
- in [2], le viste;
La configurazione Maven in [pom.xml] è la seguente:
<?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>
- righe 16–19: il progetto è un progetto Thymeleaf;
- righe 20–24: che si basa sul livello [DAO] che abbiamo appena creato;
La configurazione Java è gestita da due file:
![]() |
Il livello [web] è configurato dal seguente file [WebConfig]:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {
// ----------------- layer configuration [web]
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
// dispatcherservlet configuration for CORS headers
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
Abbiamo già incontrato tutti gli elementi di questa configurazione in un momento o nell'altro. Ricordiamo che le righe 42–47 sono necessarie quando si desidera poter interrogare il server con richieste cross-origin (CORS). Questo sarà il caso in questione.
La classe [AppConfig] configura l'intera applicazione:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.client.config.DaoConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// root web service / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// timeout in milliseconds
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
}
- Riga 11: [AppConfig] importa la configurazione per il livello [DAO] e il livello [web];
- righe 15-16: le credenziali che consentiranno all'applicazione di accedere al processo di avvio dell'applicazione per memorizzare nella cache medici e clienti;
- riga 18: l'URL del servizio web [Web1] / JSON;
- riga 20: il timeout per le chiamate HTTP dell'applicazione;
- riga 22: un valore booleano per abilitare o disabilitare le chiamate cross-domain;
Infine, in [application.properties], il server Tomcat è configurato per funzionare sulla porta 8081:
![]() |
server.port=8081
8.6.3. Funzionalità dell'applicazione
Queste sono state descritte nella Sezione 8.2. Ora le esamineremo. Utilizzando un browser, inviamo una richiesta all'URL [http://localhost:8081/boot.html]:
![]() |
- in [1], la pagina di accesso dell'applicazione;
- nei campi [2] e [3], il nome utente e la password dell'utente che desidera utilizzare l'applicazione. Sono presenti due utenti: admin/admin (nome utente/password) con il ruolo (ADMIN) e user/user con il ruolo (USER). Solo il ruolo ADMIN dispone dell'autorizzazione per utilizzare l'applicazione. Il ruolo USER è incluso esclusivamente per illustrare la risposta del server in questo caso d'uso;
- in [4], il pulsante che consente di connettersi al server;
- in [5], la lingua dell'applicazione. Ce ne sono due: francese (predefinita) e inglese;
- in [6], l'URL del server [rdvmedecins-springthymeleaf-server];
![]() |
- in [1], effettui l'accesso;
![]() |
- una volta effettuato l'accesso, puoi scegliere il medico che desideri consultare [2] e la data dell'appuntamento [3]. Non appena vengono selezionati il medico e la data, viene visualizzato automaticamente il calendario:
![]() |
- una volta visualizzato il calendario del medico, è possibile prenotare una fascia oraria [5];
![]() |
- In [6], selezionare il paziente per l'appuntamento e confermare la selezione in [7];
![]() |
Una volta confermato l'appuntamento, si torna automaticamente al calendario, dove il nuovo appuntamento è ora elencato. Questo appuntamento può essere cancellato in un secondo momento [8].
Le funzionalità principali sono state descritte. Sono semplici. Concludiamo con le impostazioni della lingua:
![]() |
- in [1], si passa dal francese all'inglese;
![]() |
- In [2], la visualizzazione passa all'inglese, compreso il calendario;
8.6.4. Passo 1: Introduzione al framework CSS Bootstrap
![]() |
Nel client web sopra riportato, le pagine HTML utilizzeranno il framework CSS Bootstrap [http://getbootstrap.com/], che presenteremo ora.
8.6.4.1. Il progetto di esempio
Il progetto di esempio sarà il seguente:
![]() |
- in [1]: il progetto nel suo complesso;
- in [2]: il codice Java;
- in [3]: gli script JavaScript;
![]() |
- in [4]: le librerie JavaScript;
- in [5]: le viste Thymeleaf;
- in [6]: i fogli di stile;
8.6.4.1.1. Configurazione Maven
Il file [pom.xml] è per un progetto 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 /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
8.6.4.1.2. Configurazione Java
![]() |
La classe [BootstrapDemo] configura l'applicazione 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;
}
}
Abbiamo già incontrato questo tipo di codice.
8.6.4.1.3. Il controller Spring
![]() |
Il [BootstrapController] è il seguente:
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";
}
}
Le azioni servono solo a visualizzare le viste elaborate da Thymeleaf.
8.6.4.1.4. Il file [application.properties]
Il file [application.properties] configura il server Tomcat incorporato:
server.port=8082
8.6.4.2. Esempio n. 1: il jumbotron
L'azione [/bs-01] visualizza la seguente vista [bs-01.xml]:
![]() |
La vista [bs-01.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
</head>
<body id="body">
<div class="container">
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- error -->
<div id="erreur" class="alert alert-danger">
<span>Ici, un texte d'erreur</span>
</div>
</div>
</body>
</html>
- riga 7: il file CSS del framework Bootstrap;
- riga 8: un file CSS locale;
- riga 13: displays [1];
- righe 19–21: visualizza [2];
- riga 11: la classe CSS [container] definisce un'area di visualizzazione all'interno del browser;
- riga 19: la classe CSS [alert] visualizza un'area colorata. La classe [alert-danger] utilizza un colore predefinito. Ne esistono diverse [alert-info, alert-warning,...];
Il jumbotron [1] è generato dalla seguente vista [jumbotron.xml]:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1>
Les Médecins
<br />
associés
</h1>
</div>
</div>
</div>
</section>
- riga 4: l'area ha la classe CSS [jumbotron];
- riga 5: la classe [row] definisce una riga con 12 colonne;
- riga 6: la classe [col-md-2] definisce un'area a due colonne all'interno della riga;
- riga 7: un'immagine viene inserita in queste due colonne;
- righe 9–15: il testo viene inserito nelle restanti 10 colonne;
8.6.4.3. Esempio #2: La barra di navigazione
L'azione [/bs-02] visualizza la seguente vista [bs-02.xml]:
![]() |
La nuova funzionalità è la barra di navigazione [1] con il suo modulo di inserimento dati e i pulsanti:
La vista [bs-02.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- scripts JS -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/js/bs-02.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar1"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 10: importiamo jQuery;
- riga 11: uno script JS locale;
- riga 16: la barra di navigazione;
La barra di navigazione è generata dalla seguente vista [navbar1.xml]:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- identification form -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
</div>
</div>
</div>
</div>
</section>
![]() |
- riga 3: la classe [navbar] definisce lo stile della barra di navigazione. La classe [navbar-inverse] le assegna uno sfondo nero. La classe [navbar-fixed-top] garantisce che, quando si scorre la pagina visualizzata dal browser, la barra di navigazione rimanga nella parte superiore dello schermo;
- Righe 5–13: definiscono l'area [1]. Si tratta in genere di una serie di classi che non capisco. Utilizzo il componente così com'è;
- righe 14–26: definiscono un'area “responsive” della barra di navigazione. Su uno smartphone, quest'area si riduce a un'area menu;
- riga 15: un'immagine attualmente nascosta;
- righe 17–25: la classe [navbar-form] applica uno stile a un modulo nella barra di navigazione. La classe [navbar-right] lo posiziona a destra della barra di navigazione;
- righe 21–23: i due campi di input del modulo alla riga 17 [2]. Si trovano all’interno di una classe [form-group] che racchiude gli elementi di un modulo, e ciascuno di essi ha la classe [form-control];
- riga 24: la classe [btn], che definisce un pulsante, arricchita dalla classe [btn-success], che gli conferisce il colore verde;
- riga 24: quando si clicca sul pulsante [Login], viene eseguita la seguente funzione JS:
function connecter() {
showInfo("Connexion demandée...");
}
function showInfo(message) {
$("#info").text(message);
}
Ecco un esempio:

8.6.4.4. Esempio n. 3: Il pulsante di elenco
L'azione [/bs-03] visualizza la seguente vista [bs-03.xml]:
![]() |
- La nuova funzionalità è il pulsante di elenco [1], noto anche come "menu a tendina";
Il codice per la vista [bs-03.xml] è il seguente:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-03.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar2"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 11: il pulsante a tendina richiede il file JS di Bootstrap;
- riga 18: la nuova barra di navigazione;
La vista [navbar2.xml] è la seguente:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- identification form -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initNavBar2();
/*]]>*/
</script>
</section>
- righe 25–40: definiscono il pulsante a tendina;
- riga 27: la classe [btn-danger] gli conferisce il colore rosso;
- righe 32–39: le voci dell'elenco. Ciascuna è un link associato a una funzione JavaScript;
- righe 46–51: uno script JavaScript eseguito dopo il caricamento del documento;
Lo script JS [bs-03.js] è il seguente:
function initNavBar2() {
// dropdown des langues
$('.dropdown-toggle').dropdown();
}
function connecter() {
showInfo("Connexion demandée...");
}
function setLang(lang) {
var msg;
switch (lang) {
case 'fr':
msg = "Vous avez choisi la langue française...";
break;
case 'en':
msg = "You have selected english language...";
break;
}
showInfo(msg);
}
function showInfo(message) {
$("#info").text(message);
}
- Righe 1-4: La funzione che inizializza il [dropdown]. [$('.dropdown-toggle')] individua l'elemento con la classe [dropdown-toggle]. Questo è il pulsante a tendina (riga 28 della vista). La funzione JS [dropdown()] — definita nel file JS [bootstrap.js] — viene applicata ad esso. Solo dopo questa operazione il pulsante si comporta come un pulsante a tendina;
- righe 10–21: la funzione eseguita quando viene selezionata una lingua;
Ecco un esempio:

8.6.4.5. Esempio #4: un menu
L'azione [/bs-04] visualizza la seguente vista [bs-04.xml]:
![]() |
È stato aggiunto un menu [1].
La vista [bs-04.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-04.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 18: inserire una nuova barra di navigazione;
La vista [navbar3.xml] è la seguente:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<ul class="nav navbar-nav">
<li class="active" id="lnkAfficherAgenda">
<a href="javascript:afficherAgenda()">Agenda </a>
</li>
<li class="active" id="lnkAccueil">
<a href="javascript:retourAccueil()">Retour Accueil </a>
</li>
<li class="active" id="lnkRetourAgenda">
<a href="javascript:retourAgenda()">Retour Agenda </a>
</li>
<li class="active" id="lnkValiderRv">
<a href="javascript:validerRv()">Valider </a>
</li>
</ul>
<!-- right-hand buttons -->
<div class="navbar-form navbar-right" role="form">
<!-- disconnect -->
<button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initNavBar3();
/*]]>*/
</script>
</section>
- righe 16–29: creano il menu con quattro opzioni, ciascuna collegata a uno script JS;
- righe 55-60: uno script eseguito al caricamento della pagina;
Lo script JS [bs-04.js] è il seguente:
...
function initNavBar3() {
// dropdown des langues
$('.dropdown-toggle').dropdown();
// l'moving image
loading = $("#loading");
loading.hide();
}
function afficherAgenda() {
showInfo("option [Agenda] cliquée...");
}
function retourAccueil() {
showInfo("option [Retour accueil] cliquée...");
}
function retourAgenda() {
showInfo("option [Retour agenda] cliquée...");
}
function validerRv() {
showInfo("option [Valider] cliquée...");
}
function setMenu(show) {
// les liens du menu
var lnkAfficherAgenda = $("#lnkAfficherAgenda");
var lnkAccueil = $("#lnkAccueil");
var lnkValiderRv = $("#lnkValiderRv");
var lnkRetourAgenda = $("#lnkRetourAgenda");
// on les met dans un dictionnaire
var options = {
"lnkAccueil" : lnkAccueil,
"lnkAfficherAgenda" : lnkAfficherAgenda,
"lnkValiderRv" : lnkValiderRv,
"lnkRetourAgenda" : lnkRetourAgenda
}
// on cache tous les liens
for ( var key in options) {
options[key].hide();
}
// on affiche ceux qui sont demandés
for (var i = 0; i < show.length; i++) {
var option = show[i];
options[option].show();
}
}
- righe 2–18: la funzione di inizializzazione della pagina;
- riga 4: per visualizzare il pulsante di selezione della lingua;
- righe 6-7: l'immagine animata viene nascosta;
- righe 26-48: una funzione [setMenu] che consente di specificare quali opzioni devono essere visibili;
Andiamo alla console degli sviluppatori (Ctrl-Shift-I) e inseriamo il seguente codice [1]:
![]() |
Quindi torniamo al browser. Il menu è cambiato [2]:
8.6.4.6. Esempio n. 5: Un elenco a discesa
L'azione [/bs-05] visualizza la seguente vista [bs-05.xml]:
![]() |
La nuova funzionalità si trova al punto [1]. Qui stiamo utilizzando un componente fornito al di fuori di Bootstrap, [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].
Il codice per la vista [bs-05.xml] è il seguente:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-05.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content" th:include="choixmedecin">
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 8: il CSS necessario per l'elenco a discesa;
- riga 13: il file JS necessario per l'elenco a discesa;
- riga 24: l'elenco a discesa;
La vista [choixmedecin.xml] è la seguente:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
</div>
<!-- local script -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initChoixMedecin();
/*]]>*/
</script>
</section>
- righe 7–12: Si tratta di un elemento [select] standard, ma con una classe specifica [combobox]. L'attributo [data-style="btn-primary"] conferisce al componente il colore blu;
- righe 16–21: uno script eseguito al caricamento della pagina;
Il file JS [bs-05.js] è il seguente:
...
function afficherAgenda() {
var idMedecin = $('#idMedecin option:selected').val();
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
function initChoixMedecin() {
// le select des médecins
$('#idMedecin').selectpicker();
// le menu
setMenu([ "lnkAfficherAgenda" ]);
}
- righe 7–12: la funzione eseguita al caricamento della pagina;
- riga 9: l'istruzione che trasforma il [select] della pagina in un elenco a discesa Bootstrap. [$('#idMedecin')] fa riferimento al [select] (riga 7 della vista [choixmedecin]) e la funzione JS [selectpicker] proviene dal file JS [bootstrap-select.js];
- riga 11: viene visualizzata solo una delle opzioni del menu;
- righe 2–5: la funzione JavaScript eseguita quando si clicca sull'opzione di menu [Agenda];
- riga 3: recuperiamo il valore dell'opzione selezionata nell'elenco a discesa: [$('#idMedecin option:selected')] individua prima il componente [id=idMedecin] e poi, all'interno di quel componente, l'opzione selezionata. L'operazione [..].val() recupera quindi il valore dell'elemento trovato, ovvero l'attributo [value] dell'opzione selezionata;
Ecco un esempio di selezione di un medico:
![]() |
8.6.4.7. Esempio n. 6: un calendario
L'azione [/bs-06] visualizza la seguente vista [bs-06.xml]:

La selezione di un medico o di una data attiva una funzione JS che visualizza sia il medico selezionato che la data selezionata. Ecco un esempio:
![]() |
Utilizzando il pulsante dell'elenco delle lingue, è possibile impostare il calendario (e solo il calendario) in inglese:

Questo è l'esempio più complesso della serie. Il calendario è un componente [bootstrap-datepicker] [http://eternicode.github.io/bootstrap-datepicker].
La vista [bs-06.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-06.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content" th:include="choixmedecinjour">
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 8: il file CSS per il componente [bootstrap-datepicker];
- riga 16: il file JS per il componente [bootstrap-datepicker];
- riga 17: il file JS per la gestione di un calendario francese. Per impostazione predefinita, è in inglese;
- riga 15: il file JS per una libreria chiamata [moment] che fornisce l'accesso a numerose funzioni di calcolo del tempo [http://momentjs.com/];
- riga 28: la vista del calendario;
La vista [choixmedecinjour.xml] è la seguente:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
<div class="col-md-3">
<h2>Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- local script -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initChoixMedecinJour();
/*]]>*/
</script>
</section>
- righe 17-23: il calendario;
- riga 18: la classe [btn-primary] gli conferisce il colore blu;
- riga 18: l'attributo [disabled="true"] impedisce l'inserimento manuale della data. È necessario utilizzare il calendario;
- riga 16: il calendario è stato inserito in una sezione [id="calendar_container"]. Per cambiare la lingua del calendario, è necessario eliminarlo e poi rigenerarlo. Quindi, eliminare il contenuto del componente [id="calendar_container"] e poi inserirvi il nuovo calendario con la nuova lingua;
- Righe 28–33: il codice di inizializzazione della pagina;
Il file JS [bs-06.js] è il seguente:
...
var calendar_infos = {};
function initChoixMedecinJour() {
// calendrier
var calendar_container = $("#calendar_container");
calendar_infos = {
"container" : calendar_container,
"html" : calendar_container.html(),
"today" : moment().format('YYYY-MM-DD'),
"langue" : "fr"
}
// création calendrier
updateCalendar();
// le select des médecins
$('#idMedecin').selectpicker();
$('#idMedecin').change(function(e) {
afficherAgenda();
})
// le menu
setMenu([]);
}
- Riga 2: Il calendario è gestito da diverse funzioni JS. La variabile [calendar_infos] raccoglierà le informazioni relative al calendario. È globale in modo da essere accessibile alle varie funzioni;
- riga 6: identifichiamo il contenitore del calendario;
- righe 7–12: le informazioni memorizzate per il calendario;
- riga 8: un riferimento al suo contenitore,
- riga 9: il codice HTML del calendario. Con queste due informazioni, possiamo rimuovere il calendario e rigenerarlo;
- riga 10: la data odierna nel formato [yyyy-mm-dd],
- riga 11: la lingua del calendario;
- riga 14: creazione del calendario;
- riga 16: il menu a tendina dei medici;
- righe 17–19: ogni volta che il valore selezionato in questo menu a tendina cambia, verrà eseguito il metodo [displayCalendar];
- riga 21: nessun menu nella barra di navigazione;
La funzione [updateCalendar] è la seguente:
function updateCalendar(renew) {
if (renew) {
// régénération du calendrier actuel
calendar_infos.container.html(calendar_infos.html);
}
// initialisation du calendrier
var calendar = $("#calendar");
var settings = {
format : "yyyy-mm-dd",
startDate : calendar_infos.today,
language : calendar_infos.langue,
};
calendar.datepicker(settings);
// sélection de la date courante
if (calendar_infos.date) {
calendar.datepicker('setDate', calendar_infos.date)
}
// évts
calendar.datepicker().on('hide', function(e) {
// affichage jour sélectionné
displayJour();
});
calendar.datepicker().on('changeDate', function(e) {
// on note la nouvelle date
calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
// affichage infos agenda
afficherAgenda();
// affichage jour sélectionné
displayJour();
});
// affichage jour sélectionné
displayJour();
}
- riga 1: la funzione [updateCalendar] accetta un parametro che può essere presente o meno. Se è presente, il calendario viene rigenerato (riga 4) in base alle informazioni contenute in [calendar_infos];
- riga 7: viene richiamato il calendario;
- righe 8–12: i suoi parametri di inizializzazione;
- riga 9: il formato delle date gestite [yyyy-mm-dd],
- riga 10: la prima data che può essere selezionata nel calendario. In questo caso, la data odierna. Le date precedenti a questa non possono essere selezionate;
- riga 11: la lingua del calendario. Ce ne saranno due: ['en'] e ['fr'];
- riga 13: il calendario viene configurato;
- righe 15–17: se la data da [calendar_infos] è stata inizializzata, allora questa data viene impostata come data corrente del calendario;
- righe 19–22: ogni volta che il calendario si chiude, verrà visualizzata la data selezionata;
- righe 23–30: ogni volta che c'è un cambio di data nel calendario:
- riga 25: la data selezionata viene registrata in [calendar_infos],
- riga 27: visualizziamo le informazioni relative al calendario,
- riga 29: visualizziamo il giorno selezionato;
- riga 32: visualizza il giorno selezionato, se presente;
Il metodo [displayJour] che visualizza il giorno selezionato è il seguente:
// affiche le jour sélectionné
function displayJour() {
if (calendar_infos.date) {
var displayjour = $("#displayjour");
moment.locale(calendar_infos.langue);
jour = moment(calendar_infos.date).format('LL');
displayjour.val(jour);
}
}
- riga 3: se è già stata selezionata una data (inizialmente, il calendario non ha alcuna data selezionata);
- riga 4: individuiamo il componente in cui inseriremo la data;
- riga 5: questa data può essere scritta in inglese o in francese. Impostiamo la lingua della libreria [moment];
- riga 6: visualizza la data selezionata nella lingua scelta e in formato lungo;
- riga 7: questa data viene visualizzata;
Ecco due esempi:
![]() | ![]() |
Quando cambia il medico o la data, viene eseguito il metodo [displayCalendar]:
function afficherAgenda() {
// on affiche médecin et date
var idMedecin = $('#idMedecin option:selected').val();
if (calendar_infos.date) {
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin + " et le jour " + calendar_infos.date);
}
}
8.6.4.8. Esempio #7: Una tabella HTML "responsive"
Nota: "responsive" è un termine che indica che un componente è in grado di adattarsi alle dimensioni dello schermo su cui viene visualizzato. Ne mostreremo un esempio.
L'azione [/bs-07] visualizza la seguente vista [bs-07.xml] (a schermo intero):
![]() |
La nuova funzionalità è la tabella HTML [1]. Questa tabella è gestita dalla libreria JS [footable]: [https://github.com/fooplugins/FooTable].
Se ridimensioni la finestra del browser, otterrai quanto segue:
![]() |
- la tabella HTML si è adattata alle dimensioni dello schermo;
- in [1], per visualizzare il link [Libro], è necessario cliccare sul segno [+];
- in [2], ciò che si vede quando si clicca sul segno [+];
La vista [bs-07.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-07.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3" />
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron" />
<!-- content -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda" />
<!-- info -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 10: il CSS per la libreria [footable];
- riga 19: il JavaScript della libreria [footable];
- riga 31: la tabella HTML per un calendario;
La vista [agenda.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="row alert alert-danger">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="javascript:reserver(14)" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="javascript:supprimer(17)" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initAgenda();
/*]]>*/
</script>
</body>
</html>
- riga 4: inserisce la tabella in una riga [row] e in un riquadro colorato [alert alert-danger];
- riga 5: la tabella occuperà 6 colonne [col-md-6];
- riga 6: la tabella HTML è formattata da Bootstrap [class='table'];
- riga 9: l'attributo [data-toggle] specifica la colonna contenente il simbolo [+/-] che espande/comprime la riga;
- riga 15: l'attributo [data-hide='phone'] specifica che la colonna deve essere nascosta se lo schermo ha le dimensioni di uno schermo di telefono. È possibile utilizzare anche il valore 'tablet';
- riga 31: una funzione JS è associata al link [Book];
- riga 46: una funzione JS è associata al link [Delete];
- righe 56–61: inizializzazione della pagina;
Numerose classi CSS utilizzate sopra provengono dal file CSS [bootstrapDemo.css]:
@CHARSET "UTF-8";
#creneaux th {
text-align: center;
}
#creneaux td {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
Gli stili [status-*] provengono da un esempio di utilizzo della tabella [footable] presente sul sito web della libreria.
Nel file JS [bs-07.js], la pagina viene inizializzata come segue:
function initAgenda() {
// time slot table
$("#creneaux").footable();
}
Ecco fatto. [$("#creneaux")] si riferisce alla tabella HTML che vogliamo rendere reattiva. Inoltre, ecco le funzioni JS associate ai due link [Prenota] e [Elimina]:
function reserver(idCreneau) {
showInfo("Réservation du créneau n° " + idCreneau);
}
function supprimer(idRv) {
showInfo("Suppression du rv n° " + idRv);
}
8.6.4.9. Esempio #8: Una finestra modale
L'azione [/bs-08] visualizza la seguente vista [bs-08.xml]:

Mentre in precedenza, cliccando sul link [Prenota] venivano visualizzate le informazioni nella finestra informativa, qui verrà visualizzata una finestra modale per selezionare un cliente per l'appuntamento:

Il componente utilizzato è il componente [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/].
La vista [bs-08.xml] è la seguente:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-modal.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-08.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3" />
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron" />
<!-- content -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda-modal" />
<div th:include="resa" />
<!-- info -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 19: il file JS necessario per le finestre modali;
- riga 32: la vista [agenda-modal] è identica alla vista [agenda] tranne che per un dettaglio: la funzione JS che gestisce il link [Prenota]:
<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>
La funzione [showDialogResa] è responsabile della visualizzazione della finestra modale per la selezione di un cliente;
- riga 33: la vista [resa.xml] è la finestra modale per la selezione di un cliente:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Modal title</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span>Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2>Clients</h2>
<select id="idClient" class="combobox" data-style="btn-primary">
<option value="1">Mme Marguerite Planton</option>
<option value="2">Mr Maxime Franck</option>
<option value="3">Mlle Elisabeth Oron</option>
<option value="4">Mr Gaëtan Calot</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validateResa()">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initResa();
/*]]>*/
</script>
</section>
- righe 3-37: la finestra modale;
- righe 13-30: il contenuto di questa finestra (ciò che verrà visualizzato);
- righe 31-34: i pulsanti della finestra di dialogo;
- riga 32: un pulsante [Annulla] gestito dalla funzione JS [cancelDialogResa];
- riga 33: un pulsante [Conferma] gestito dalla funzione JS [validateResa];
- righe 39–44: lo script di inizializzazione della finestra modale;
Il risultato è la seguente visualizzazione:
![]() |
Si noti che la finestra modale non viene visualizzata per impostazione predefinita. Questo è il motivo per cui non è visibile all'avvio dell'applicazione, anche se il suo codice HTML è presente nel documento.
Il file JS [bs-08.js] è il seguente:
var idCreneau;
var idClient;
var resa;
function showDialogResa(idCreneau) {
// on mémorise l'id du créneau
this.idCreneau = idCreneau;
// on affiche le dialogue de réservation
var resa = $("#resa");
resa.modal('show');
// log
showInfo("Réservation du créneau n° " + idCreneau);
}
function cancelDialogResa() {
// on cache la boîte de dialogue
resa.modal('hide');
}
// validation résa
function validateResa() {
// on récupère les infos
var idClient = $('#idClient option:selected').val();
// on cache la boîte de dialogue
resa.modal('hide');
// infos
showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
function initResa() {
// le select des clients
$('#idClient').selectpicker();
// boîte modale
resa = $("#resa");
resa.modal({});
}
- righe 30–36: la funzione di inizializzazione della finestra modale;
- riga 32: la finestra modale contiene un elenco a discesa che deve essere inizializzato;
- righe 34-35: inizializzazione della finestra modale stessa;
- righe 5-13: la funzione JS associata al link [Book];
- riga 7: il parametro della funzione viene memorizzato nella variabile globale della riga 1;
- righe 9-10: la finestra modale viene resa visibile;
- riga 12: le informazioni vengono registrate nella casella informativa;
- righe 15–18: gestione del pulsante [Cancel]. Semplicemente nascondiamo la finestra modale (riga 17);
- righe 21–31: la funzione JS associata al pulsante [Invia];
- riga 23: recupero dell'attributo [value] del client selezionato;
- riga 25: nascondiamo la finestra di dialogo;
- riga 27: registriamo le due informazioni: il numero dello slot prenotato e il cliente a cui è destinato;
8.6.5. Fase 2: Scrittura delle viste
Descriveremo ora le viste restituite dal server [Web1] e i relativi modelli.
![]() |
8.6.5.1. La vista [navbar-start]
Visualizza la barra di navigazione nella pagina di avvio:

Il codice per [navbar-start.xml] è il seguente:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- identification form -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" th:placeholder="#{service.url}" class="form-control" id="urlService" />
</div>
<div class="form-group">
<input type="text" th:placeholder="#{username}" class="form-control" id="login" />
</div>
<div class="form-group">
<input type="password" th:placeholder="#{password}" class="form-control" id="passwd" />
</div>
<button type="button" class="btn btn-success" th:text="#{login}" onclick="javascript:connecter()">Sign in</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Action</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initNavBarStart();
/*]]>*/
</script>
</section>
Questa vista non ha alcun modello. Dispone dei seguenti gestori di eventi:
event | gestore |
clic sul pulsante di accesso | |
clicca sul link [Francese] | |
fare clic sul link [English] |
8.6.5.2. La vista [jumbotron]
Questa è la vista visualizzata sotto la barra di navigazione [navbar-start] nella pagina di avvio:

Il suo codice [jumbotron.xml] è il seguente:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1 th:utext="#{application.header}" />
</div>
</div>
</div>
</section>
La vista [jumbotron] non ha modelli né eventi.
8.6.5.3. La vista [login]
Questa è la vista visualizzata sotto il jumbotron nella pagina di avvio:

Il suo codice [login.xml] è il seguente:
<!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>
La vista non ha né un modello né eventi.
8.6.5.4. La vista [navbar-run]
Questa è la barra di navigazione visualizzata quando l'accesso ha esito positivo:

Il suo codice [navbar-run.xml] è il seguente:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- right-hand buttons -->
<form class="navbar-form navbar-right" role="form">
<!-- disconnect -->
<button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</form>
</div>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initNavBarRun();
/*]]>*/
</script>
</section>
Questa vista non ha alcun modello. Dispone dei seguenti gestori di eventi:
event | gestore |
clic sul pulsante di logout | |
clicca sul link [Francese] | |
fare clic sul link [English] |
8.6.5.5. La vista [home]
Questa è la vista visualizzata immediatamente sotto la barra di navigazione [navbar-run]:

Il suo codice [home.html] è il seguente:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{choixmedecinjour.title}">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{rv.medecin}">Médecin</h2>
<select name="idMedecin" id="idMedecin" class="combobox" data-style="btn-primary">
<option th:each="medecinItem : ${rdvmedecins.medecinItems}" th:text="${medecinItem.texte}" th:value="${medecinItem.id}"/>
</select>
</div>
<div class="col-md-3">
<h2 th:text="#{rv.jour}">Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- agenda -->
<div id="agenda"></div>
<!-- local script -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initChoixMedecinJour();
/*]]>*/
</script>
</html>
Il suo modello è il seguente:
- [rdvmedecins.medecinItems] (riga 8): l'elenco dei medici;
Nella sua forma attuale, la vista non sembra avere alcun gestore di eventi. In realtà, questi sono definiti nella funzione [initChoixMedecinJour]. Questa funzione è stata presentata nella sezione 8.6.4.7, a pagina 466 e più specificatamente a pagina 469. Contiene i seguenti gestori di eventi:
event | gestore |
selezione medico | |
seleziona una data |
8.6.5.6. La vista [calendario]
La vista [agenda] mostra un giorno dal calendario di un medico:

Il codice [agenda.xml] è il seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3 class="alert alert-info" th:text="${agenda.titre}">Agenda de Mme Pélissier le 13/10/2014</h3>
<h4 class="alert alert-danger" th:if="${agenda.creneaux.length}==0" th:text="#{agenda.medecinsanscreneaux}">Ce médecin n'a pas encore de créneaux
de consultation</h4>
<th:block th:if="${agenda.creneaux.length}!=0">
<div class="row tab-content alert alert-warning">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span th:text="#{agenda.creneauhoraire}">Créneau horaire</span>
</th>
<th>
<span th:text="#{agenda.client}">Client</span>
</th>
<th data-hide="phone">
<span th:text="#{agenda.action}">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr th:each="creneau,iter : ${agenda.creneaux}">
<td>
<span th:if="${creneau.action}==1" class="status-metro status-active" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
<span th:if="${creneau.action}==2" class="status-metro status-suspended" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
</td>
<td>
<span th:text="${creneau.client}">Client</span>
</td>
<td>
<a th:if="${creneau.action}==1" th:href="@{'javascript:reserverCreneau('+${creneau.id}+')'}" th:text="${creneau.commande}"
class="status-metro status-active">Réserver
</a>
<a th:if="${creneau.action}==2" th:href="@{'javascript:supprimerRv('+${creneau.idRv}+')'}" th:text="${creneau.commande}"
class="status-metro status-suspended">Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- reservation -->
<section th:include="resa" />
</th:block>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initAgenda();
/*]]>*/
</script>
</body>
</html>
Il modello per questa vista ha un solo elemento:
- [agenda] (riga 4): un modello piuttosto complesso progettato specificamente per visualizzare il calendario;
Dispone dei seguenti gestori di eventi:
event | gestore |
clic sul pulsante [Elimina] | |
fare clic sul link [Prenota] |
La vista [resa] alla riga 47 è la vista che viene visualizzata quando l'utente clicca su un link [Prenota]:

Il suo codice [resa.xml] è il seguente:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Modal title</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span th:text="#{resa.titre}">Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{resa.client}">Client</h2>
<select name="idClient" id="idClient" class="combobox" data-style="btn-primary">
<option th:each="clientItem : ${clientItems}" th:text="${clientItem.texte}" th:value="${clientItem.id}" />
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()" th:text="#{resa.annuler}">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validerRv()" th:text="#{resa.valider}">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initResa();
/*]]>*/
</script>
</body>
</html>
Il suo modello ha un solo elemento:
- [clientItems] (riga 24): l'elenco dei clienti;
Dispone dei seguenti gestori di eventi:
event | gestore |
clic sul pulsante [Annulla] | |
Clicca sul pulsante [Conferma] |
8.6.5.7. La vista [errori]
Questa è la vista che appare se l'azione richiesta dall'utente non ha potuto essere completata:

Il codice [errors.xml] è il seguente:
<!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>
Il suo template ha un solo elemento:
- [errors] (riga 8): l'elenco degli errori da visualizzare;
La vista non ha alcun gestore di eventi.
8.6.5.8. Riepilogo
La tabella seguente elenca le viste e i relativi modelli:
vista | Modello | Gestori di eventi |
navbar-start | ||
jumbotron | ||
accedi | ||
barra di navigazione-run | ||
home | ||
calendario | ||
prenotare | ||
errori |
8.6.6. Fase 3: Scrittura delle azioni
Torniamo all'architettura del servizio web [Web1]:
![]() |
Ora vedremo quali URL sono esposti da [Web1] e la loro implementazione:
8.6.6.1. Gli URL esposti dal servizio [Web1]
Sono i seguenti:
- un URL per ciascuna delle viste precedenti o una loro combinazione;
- un URL per aggiungere un appuntamento;
- un URL per cancellare un appuntamento;
Tutti restituiscono una risposta di tipo [Response] come segue:
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the navigation bar
private String navbar;
// the jumbotron
private String jumbotron;
// the body of the page
private String content;
// the diary
private String agenda;
...
}
- riga 5: uno stato di risposta: 1 (OK), 2 (errore);
- riga 7: il flusso HTML per le viste [navbar-start] o [navbar-run], a seconda dei casi;
- riga 9: il feed HTML per la vista [jumbotron];
- riga 13: il feed HTML per la vista [agenda];
- riga 9: il feed HTML per le viste [home], [errors] o [login], a seconda dei casi;
Gli URL esposti sono i seguenti
inserisce la vista [navbar-start] in [Response.navbar] | |
inserisce la vista [navbar-run] in [Response.navbar] | |
inserisce la vista [home] in [Response.content] | |
inserisce la vista [jumbotron] in [Response.jumbotron] | |
inserisce la vista [agenda] in [Response.agenda] | |
inserisce la vista [login] in [Response.content] | |
| |
inserisce la vista [navbar-run] in [Response.navbar], la vista [jumbotron] in [Response.jumbotron], la vista [home] in [Response.content] e la vista [calendar] in [Response.calendar] | |
aggiunge l'appuntamento selezionato e inserisce la nuova agenda in [Response.agenda] | |
elimina l'appuntamento selezionato e inserisce il nuovo calendario in [Response.calendar] |
8.6.6.2. Il singleton [ApplicationModel]
![]() |
La classe [ApplicationModel] viene istanziata come singola istanza e iniettata nel controller dell'applicazione. Il suo codice è il seguente:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
....
}
- riga 6: [ApplicationModel] è un componente Spring;
- riga 7: che implementa l'interfaccia del livello [DAO]. Lo facciamo in modo che le azioni non debbano conoscere il livello [DAO], ma solo il singleton [ApplicationModel]. L'architettura di [Web1] diventa quindi la seguente:
![]() |
Torniamo al codice della classe [ApplicationModel]:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
// the [DAO] layer
@Autowired
private IDao dao;
// configuration
@Autowired
private AppConfig appConfig;
// data from the [DAO] layer
private List<ClientItem> clientItems;
private List<MedecinItem> medecinItems;
// configuration data
private String userInit;
private String mdpUserInit;
private boolean corsAllowed;
// exception
private RdvMedecinsException rdvMedecinsException;
// manufacturer
public ApplicationModel() {
}
@PostConstruct
public void init() {
// config
userInit = appConfig.getUSER_INIT();
mdpUserInit = appConfig.getMDP_USER_INIT();
dao.setTimeout(appConfig.getTIMEOUT());
dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
corsAllowed = appConfig.isCORS_ALLOWED();
// caching of physician and customer drop-down lists
List<Medecin> medecins = null;
List<Client> clients = null;
try {
medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
clients = dao.getAllClients(new User(userInit, mdpUserInit));
} catch (RdvMedecinsException ex) {
rdvMedecinsException = ex;
}
if (rdvMedecinsException == null) {
// create drop-down list items
medecinItems = new ArrayList<MedecinItem>();
for (Medecin médecin : medecins) {
medecinItems.add(new MedecinItem(médecin));
}
clientItems = new ArrayList<ClientItem>();
for (Client client : clients) {
clientItems.add(new ClientItem(client));
}
}
}
// getters and setters
...
// interface implementation [IDao]
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
return dao.ajouterRv(user, jour, idCreneau, idClient);
}
...
}
- riga 11: inserimento del riferimento all'implementazione del livello [DAO]. Questo riferimento viene poi utilizzato per implementare l'interfaccia [IDao] (righe 64–80);
- riga 14: inserimento della configurazione dell'applicazione;
- righe 33–37: utilizzo di questa configurazione per configurare vari elementi dell'architettura dell'applicazione;
- Righe 38–46: Memorizziamo nella cache le informazioni che andranno a popolare gli elenchi a discesa relativi ai medici e ai clienti. Partiamo quindi dal presupposto che, se un medico o un cliente cambia, l'applicazione debba essere riavviata. L'idea è quella di dimostrare che un singleton Spring può fungere da cache per l'applicazione web;
Le classi [MedecinItem] e [ClientItem] derivano entrambe dalla seguente classe [PersonneItem]:
package rdvmedecins.springthymeleaf.server.models;
import rdvmedecins.client.entities.Personne;
public class PersonneItem {
// element of a list
private Long id;
private String texte;
// manufacturer
public PersonneItem() {
}
public PersonneItem(Personne personne) {
id = personne.getId();
texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
}
// getters and setters
...
}
- riga 8: il campo [id] sarà il valore dell'attributo [value] di un'opzione dell'elenco a discesa;
- riga 9: il campo [text] sarà il testo visualizzato da un'opzione dell'elenco a discesa;
8.6.6.3. La classe [BaseController]
![]() |
La classe [BaseController] è la classe padre dei controller [RdvMedecinsController] e [RdvMedecinsCorsController]. Non era obbligatorio creare questa classe padre. Abbiamo raggruppato qui i metodi di utilità della classe [RdvMedecinsController], nessuno dei quali è essenziale tranne uno. Possono essere classificati in tre gruppi:
- metodi di utilità;
- metodi che rendono le viste unite ai loro modelli;
- il metodo per l'inizializzazione di un'azione
| due metodi di utilità che forniscono un elenco di messaggi di errore. Li abbiamo già incontrati e utilizzati; |
| restituisce la vista [home] senza un template |
| restituisce la vista [agenda] e il relativo modello |
| restituisce la vista [login] senza un modello |
| restituisce la risposta al client quando l'azione richiesta da ha generato un errore |
| il metodo di inizializzazione per tutte le azioni del controller [RdvMedecinsController] |
Esaminiamo due di questi metodi.
Il metodo [getPartialViewAgenda] rende la vista più complessa da generare, quella del calendario. Il suo codice è il seguente:
// feed [agenda]
protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
// contexts
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
// build the [agenda] page template
ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
// the agenda with its model
thymeleafContext.setVariable("agenda", modelAgenda);
thymeleafContext.setVariable("clientItems", application.getClientItems());
return engine.process("agenda", thymeleafContext);
}
- Righe 9–10: i due elementi del modello del calendario:
- riga 9: il calendario visualizzato.
- riga 10: l'elenco dei clienti visualizzato quando l'utente fissa un appuntamento;
Il metodo [setModelforAgenda] alla riga 7 è il seguente:
// agenda] page template
private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
// page title
String dateFormat = springContext.getMessage("date.format", null, locale);
Medecin médecin = agenda.getMedecin();
String titre = springContext.getMessage("agenda.titre", new String[] { médecin.getTitre(), médecin.getPrenom(),
médecin.getNom(), new SimpleDateFormat(dateFormat).format(agenda.getJour()) }, locale);
// reservation slots
ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
int i = 0;
for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
// doctor's slot
Creneau créneau = creneauMedecinJour.getCreneau();
ViewModelCreneau modelCréneau = new ViewModelCreneau();
modelCréneaux[i] = modelCréneau;
// id
modelCréneau.setId(créneau.getId());
// time slot
modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
créneau.getHfin(), créneau.getMfin()));
Rv rv = creneauMedecinJour.getRv();
// customer and order
String commande;
if (rv == null) {
modelCréneau.setClient("");
commande = springContext.getMessage("agenda.reserver", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setAction(ViewModelCreneau.ACTION_RESERVER);
} else {
Client client = rv.getClient();
modelCréneau.setClient(String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom()));
commande = springContext.getMessage("agenda.supprimer", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setIdRv(rv.getId());
modelCréneau.setAction(ViewModelCreneau.ACTION_SUPPRIMER);
}
// next slot
i++;
}
// we render the agenda model
ViewModelAgenda modelAgenda = new ViewModelAgenda();
modelAgenda.setTitre(titre);
modelAgenda.setCreneaux(modelCréneaux);
return modelAgenda;
}
- Riga 6: L'agenda ha un titolo:

oppure:

Possiamo notare che il formato della data dipende dalla lingua. Recuperiamo questo formato dai file dei messaggi (riga 4).
- righe 11–40: per ogni intervallo di tempo, dobbiamo visualizzare la vista:
![]()
oppure la vista:
![]()
- Righe 19–20: visualizza la fascia oraria;
- righe 25–28: il caso in cui la fascia oraria è disponibile. In questo caso, deve essere visualizzato il pulsante [Prenota];
- righe 31–36: il caso in cui la fascia oraria è occupata. In questo caso, devono essere visualizzati sia il cliente che il pulsante [Elimina];
L'altro metodo di cui parleremo più nel dettaglio è il metodo [getActionContext]. Viene chiamato all'inizio di ogni azione nel [RdvMedecinsController]. La sua firma è la seguente:
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController)
Restituisce il seguente tipo [ActionContext]:
public class ActionContext {
// data
private WebContext thymeleafContext;
private WebApplicationContext springContext;
private Locale locale;
private List<String> erreurs;
...
}
- riga 4: il contesto Thymeleaf dell'azione;
- riga 5: il contesto Spring dell'azione;
- riga 6: le impostazioni locali dell'azione;
- riga 7: un elenco di possibili messaggi di errore;
I suoi parametri sono i seguenti:
- [lang]: la lingua richiesta per l'azione, 'en' o 'fr';
- [origin]: l'intestazione HTTP [origin] nel caso di una richiesta cross-domain;
- [request]: la richiesta HTTP attualmente in elaborazione, ciò che da tempo viene definito "azione";
- [response]: la risposta che verrà inviata in risposta a questa richiesta;
- [result]: ogni azione di [RdvMedecinsController] riceve un valore inviato la cui validità viene verificata. [result] è il risultato di questa verifica;
- [rdvMedecinsController]: il controller che contiene le azioni;
Il metodo [getActionContext] è implementato come segue:
// context of an action
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
// language?
if (lang == null) {
lang = "fr";
}
// local
Locale locale = null;
if (lang.trim().toLowerCase().equals("fr")) {
// french
locale = new Locale("fr", "FR");
} else {
// everything else in English
locale = new Locale("en", "US");
}
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, response);
// ActionContext
ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
// initialization errors
RdvMedecinsException e = application.getRdvMedecinsException();
if (e != null) {
actionContext.setErreurs(e.getMessages());
return actionContext;
}
// POST errors?
if (result != null && result.hasErrors()) {
actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
return actionContext;
}
// no errors
return actionContext;
}
- righe 3–15: in base al parametro [lang], impostiamo le impostazioni locali dell'azione;
- riga 17: inviamo le intestazioni HTTP necessarie per le richieste cross-domain. Non entreremo nei dettagli in questa sede. La tecnica utilizzata è quella descritta nella sezione 8.4.14;
- riga 19: creazione di un oggetto [ActionContext] senza errori;
- riga 21: abbiamo visto nella sezione 8.6.6.2 che il singleton [ApplicationModel] ha effettuato l'accesso al database per recuperare sia i clienti che i medici. Questo accesso potrebbe fallire. Registriamo quindi l'eccezione che si verifica. Alla riga 21, recuperiamo questa eccezione;
- righe 22–25: se si è verificata un'eccezione durante l'avvio dell'applicazione, non è possibile alcuna azione. Restituiamo quindi un oggetto [ActionContext] per qualsiasi azione, contenente i messaggi di errore dell'eccezione;
- righe 27–20: analizziamo il parametro [result] per determinare se il valore inviato fosse valido o meno. Se non era valido, restituiamo un oggetto [ActionContext] con i messaggi di errore appropriati;
- riga 32: caso senza errori;
Esamineremo ora le azioni del [RdvMedecinsController]
8.6.6.4. L'azione [/getNavBarStart]
L'azione [/getNavBarStart] rende la vista [navbar-start]. La sua firma è la seguente:
@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)
Restituisce il seguente tipo [Response]:
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the navigation bar
private String navbar;
// the jumbotron
private String jumbotron;
// the body of the page
private String content;
// the diary
private String agenda;
...
}
e presenta i seguenti parametri:
- [PostLang postlang]: il valore pubblicato successivo:
public class PostLang {
// data
@NotNull
private String lang;
...
}
La classe [PostLang] è la classe padre di tutti i valori inviati. Questo perché il client deve sempre specificare la lingua in cui deve essere eseguita l'azione.
Il metodo [getNavbarStart] è implementato come segue:
// navbar-start
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// returns the [navbar-start] view
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
return reponse;
}
- riga 7: inizializzazione dell'azione;
- righe 10–13: se il metodo di inizializzazione dell'azione ha segnalato errori, questi vengono inviati nella risposta al client (riga 12) con lo stato 2:
- righe 15-18: invia la vista [navbar-start] con lo stato 1:
Di seguito descriveremo solo le nuove funzionalità.
8.6.6.5. L'azione [/getNavbarRun]
L'azione [/getNavBarRun] visualizza la vista [navbar-run]:
// navbar-run
@RequestMapping(value = "/getNavbarRun", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarRun(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// returns the [navbar-run] view
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
return reponse;
}
L'azione può restituire due tipi di risposte:
- la risposta con un errore (righe 10–13):
- la risposta con la vista [navbar-run]:
8.6.6.6. L'azione [/getJumbotron]
L'azione [/getJumbotron] restituisce la vista [jumbotron]:
// jumbotron
@RequestMapping(value = "/getJumbotron", method = RequestMethod.POST)
@ResponseBody
public Reponse getJumbotron(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// return view [jumbotron]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
return reponse;
}
L'operazione può restituire due tipi di risposta:
- la risposta con un errore (righe 10–13):
- risposta con la vista [jumbotron]:
8.6.6.7. L'azione [/getLogin]
L'azione [/getLogin] visualizza la vista [login]:
@RequestMapping(value = "/getLogin", method = RequestMethod.POST)
@ResponseBody
public Reponse getLogin(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// returns the [login] view
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
reponse.setContent(getPartialViewLogin(thymeleafContext));
return reponse;
}
L'azione può restituire due tipi di risposte:
- La risposta con un errore (righe 9–11):
- La risposta con la vista [login]:
8.6.6.8. L'azione [/getHome]
L'azione [/getHome] restituisce la vista [home]. La sua firma è la seguente:
@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)
- Riga 3: Il valore inviato è di tipo [PostUser] come segue:
public class PostUser extends PostLang {
// data
@NotNull
private User user;
...
}
- riga 1: la classe [PostUser] estende la classe [PostLang] e quindi include una lingua;
- riga 4: l'utente che tenta di recuperare la vista;
Il codice di implementazione è il seguente:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// the [home] view is protected
try{
// user
User user = postUser.getUser();
// we check identifiers [userName, password]
application.authenticate(user);
}catch(RdvMedecinsException e){
// an error is returned
return getViewErreurs(thymeleafContext, e.getMessages());
}
// returns the [home] view
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
- Righe 15–22: Si noti che la pagina [home] è protetta, quindi l'utente deve essere autenticato;
L'azione può restituire due tipi di risposte:
- la risposta di errore (righe 11 e 21):
- risposta con la vista [home] (righe 24–27):
8.6.6.9. L'azione [/getNavbarRunJumbotronHome]
L'azione [/getNavbarRunJumbotronHome] rende le viste [navbar-run, jumbotron, home]. Ha la seguente firma:
@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)
- riga 3: il valore inviato è di tipo [PostUser];
L'implementazione dell'azione è la seguente:
// navbar+ jumbotron + home
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// the [home] view is protected
try {
// user
User user = postUser.getUser();
// we check identifiers [userName, password]
application.authenticate(user);
} catch (RdvMedecinsException e) {
// an error is returned
return getViewErreurs(thymeleafContext, e.getMessages());
}
// we send the answer
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
L'azione può restituire due tipi di risposte:
- la risposta con un errore (righe 13, 23):
- la risposta con le viste [navbar-run, jumbotron, home] (righe 26–31):
8.6.6.10. L'azione [/getAgenda]
L'azione [/getAgenda] visualizza la vista [agenda]. La sua firma è la seguente:
@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)
- Riga 3: Il valore inviato è di tipo [PostGetAgenda] come segue:
public class PostGetAgenda extends PostUser {
// data
@NotNull
private Long idMedecin;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- riga 1: la classe [PostGetAgenda] estende la classe [PostUser] e quindi include una lingua e un utente;
- riga 5: l'ID del medico di cui si desidera il calendario;
- riga 8: il giorno desiderato del calendario;
L'implementazione è la seguente:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result, rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
Locale locale = actionContext.getLocale();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// check the validity of the post
if (result != null) {
new PostGetAgendaValidator().validate(postGetAgenda, result);
if (result.hasErrors()) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
...
}
- Fino alla riga 14, il codice è ora standard;
- righe 16–21: eseguiamo un controllo aggiuntivo sul valore inviato. La data deve essere uguale o successiva alla data odierna. Per verificarlo, utilizziamo un validatore:
package rdvmedecins.web.validators;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import rdvmedecins.springthymeleaf.server.requests.PostGetAgenda;
import rdvmedecins.springthymeleaf.server.requests.PostValiderRv;
public class PostGetAgendaValidator implements Validator {
public PostGetAgendaValidator() {
}
@Override
public boolean supports(Class<?> classe) {
return PostGetAgenda.class.equals(classe) || PostValiderRv.class.equals(classe);
}
@Override
public void validate(Object post, Errors errors) {
// the day chosen for the appointment
Date jour = null;
if (post instanceof PostGetAgenda) {
jour = ((PostGetAgenda) post).getJour();
} else {
if (post instanceof PostValiderRv) {
jour = ((PostValiderRv) post).getJour();
}
}
// transform dates into yyyy-MM-dd format
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String strJour = sdf.format(jour);
String strToday = sdf.format(new Date());
// the chosen day must not precede today's date
if (strJour.compareTo(strToday) < 0) {
errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
}
}
}
- Riga 19: Il validatore funziona per due classi: [PostGetAgenda] e [PostValiderRv];
Torniamo al codice dell'azione [/getAgenda]:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
...
// action
try {
// doctor's diary
AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
// answer
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException e1) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, e1.getMessages());
} catch (Exception e2) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- righe 9-10: utilizzando i parametri inviati, richiediamo l'orario del medico;
- righe 12-13: restituiamo l'orario:
- righe 17, 21: restituiamo una risposta con errori:
8.6.6.11. L'azione [/getNavbarRunJumbotronHomeCalendar]
L'azione [/getNavbarRunJumbotronHomeCalendar] rende le viste [navbar-run, jumbotron, home, calendar]. La sua implementazione è la seguente:
@RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// agenda
Reponse agenda = getAgenda(post, result, request, response, null);
if (agenda.getStatus() != 1) {
return agenda;
}
// we send the answer
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
reponse.setAgenda(agenda.getAgenda());
return reponse;
}
- righe 15–18: Sfruttiamo l'esistenza dell'azione [/getAgenda] per richiamarla. Quindi controlliamo lo stato della risposta (riga 16). Se viene rilevato un errore, ci fermiamo lì e restituiamo la risposta;
- riga 20: inviamo le viste richieste:
8.6.6.12. L'azione [/supprimerRv]
L'azione [/deleteRv] consente di eliminare un appuntamento. La sua firma è la seguente:
@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)
- Riga 3: Il valore inviato è di tipo [PostSupprimerRv] come segue:
public class PostSupprimerRv extends PostUser {
// data
@NotNull
private Long idRv;
..
}
- riga 1: la classe [PostSupprimerRv] estende la classe [PostUser] e quindi include una lingua e un utente;
- riga 5: il numero dell'appuntamento da eliminare;
L'implementazione dell'azione è la seguente:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// posted values
User user = postSupprimerRv.getUser();
long idRv = postSupprimerRv.getIdRv();
// we delete the appointment
AgendaMedecinJour agenda = null;
try {
// we get it back
Rv rv = application.getRvById(user, idRv);
Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
long idMedecin = creneau.getIdMedecin();
Date jour = rv.getJour();
// delete the associated rv
application.supprimerRv(user, idRv);
// we regenerate the doctor's diary
agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
// we return the new diary
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- riga 22: recupera l'appuntamento da eliminare. Se non esiste, viene generata un'eccezione;
- righe 23–25: in base a questo appuntamento, individuiamo il medico e il giorno corrispondente. Queste informazioni sono necessarie per rigenerare l'agenda del medico;
- riga 27: l'appuntamento viene cancellato;
- riga 29: richiediamo il nuovo orario del medico. Questo è importante. Oltre allo slot appena liberato, altri utenti dell’applicazione potrebbero aver apportato modifiche all’orario. È importante restituire all’utente la versione più recente dell’orario;
- righe 31–34: viene restituito il calendario:
8.6.6.13. L'azione [/validerRv]
L'azione [/validerRv] aggiunge un appuntamento al calendario di un medico. La sua firma è la seguente:
@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)
- Riga 3: Il valore inviato è di tipo [PostValiderRv] come segue:
public class PostValiderRv extends PostUser {
// data
@NotNull
private Long idCreneau;
@NotNull
private Long idClient;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- riga 1: la classe [PostValiderRv] estende la classe [PostUser] e quindi include una lingua e un utente;
- riga 5: il numero della fascia oraria;
- riga 7: l'ID del cliente per il quale è stata effettuata la prenotazione;
- riga 10: il giorno dell'appuntamento;
L'implementazione dell'azione è la seguente:
// appointment validation
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebApplicationContext springContext = actionContext.getSpringContext();
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// check the validity of the appointment date
if (result != null) {
new PostGetAgendaValidator().validate(postValiderRv, result);
if (result.hasErrors()) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
// posted values
User user = postValiderRv.getUser();
long idClient = postValiderRv.getIdClient();
long idCreneau = postValiderRv.getIdCreneau();
Date jour = postValiderRv.getJour();
// action
try {
// get information on the niche
Creneau créneau = application.getCreneauById(user, idCreneau);
long idMedecin = créneau.getIdMedecin();
// we add the Rv
application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
// we regenerate the agenda
AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
new SimpleDateFormat("yyyy-MM-dd").format(jour));
// we return the new diary
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
}
Il codice è simile a quello dell'azione [/deleteRv].
8.6.7. Passaggio 4: Test del server Spring/Thymeleaf
Ora testeremo le varie azioni descritte sopra utilizzando il plugin di Chrome [Advanced Rest Client] (vedi sezione 9.6).
8.6.7.1. Configurazione del test
Tutte le azioni richiedono un valore inviato. Invieremo diverse varianti della seguente stringa JSON:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Questo valore inviato include informazioni superflue per la maggior parte delle azioni. Tuttavia, queste vengono ignorate dalle azioni che le ricevono e non causano un errore. Questo valore inviato ha il vantaggio di coprire i vari valori da inviare.
8.6.7.2. L'azione [/getNavbarStart]
![]() |
- in [1], l'azione in fase di test;
- in [2], il valore inviato;
- in [3], il valore inviato è una stringa JSON;
- in [4], la vista [navbar-start] viene richiesta in inglese;
Il risultato ottenuto è il seguente:
![]() |
Abbiamo ricevuto la vista [navbar-start] in inglese (aree evidenziate).
Ora, introduciamo un errore. Impostiamo l'attributo [lang] del valore inviato su null. Otteniamo il seguente risultato:
![]() |
Abbiamo ricevuto una risposta di errore (stato 2) che indica che il campo [lang] era obbligatorio.
8.6.7.3. L'azione [/getNavbarRun]
Richiediamo l'azione [getNavbarRun] con il seguente valore inviato:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato ottenuto è il seguente:
![]() |
8.6.7.4. L'azione [/getJumbotron]
Richiediamo l'azione [getJumbotron] con i seguenti dati POST:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato ottenuto è il seguente:
![]() |
8.6.7.5. L'azione [/getLogin]
Richiediamo l'azione [getLogin] con i seguenti dati POST:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato è il seguente:
![]() |
8.6.7.6. L'azione [/getAccueil]
Richiediamo l'azione [getAccueil] con il seguente valore inviato:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato ottenuto è il seguente:
![]() |
Proviamo di nuovo con un utente sconosciuto:
{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato è il seguente:
![]() |
Ricominciamo con un utente esistente che non è autorizzato a utilizzare l'applicazione:
{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato è il seguente:
![]() |
8.6.7.7. L'azione [/getAgenda]
Richiediamo l'azione [getAgenda] con il seguente valore inviato:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato ottenuto è il seguente:
![]() |
Proviamo di nuovo con una data precedente a oggi:
![]() |
Ricominciamo con un medico inesistente:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":11, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato è il seguente:
![]() |
8.6.7.8. L'azione [/getNavbarRunJumbotronAccueil]
Richiediamo l'azione [getNavbarRunJumbotronAccueil] con il seguente valore inviato:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato è il seguente:
![]() |
Lo stesso vale per un utente sconosciuto:
![]() |
8.6.7.9. L'azione [/getNavbarRunJumbotronHomeCalendar]
Richiediamo l'azione [getNavbarRunJumbotronHomeCalendar] con il seguente valore inviato:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato è il seguente:
![]() |
Inseriamo un medico che non esiste:
![]() |
8.6.7.10. L'azione [/deleteAppointment]
Richiediamo l'azione [deleteAppointment] con il seguente valore inviato:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
L'appuntamento n. 93 non esiste. Il risultato ottenuto è il seguente:
![]() |
Con un appuntamento esistente:
![]() |
Possiamo verificare nel database che l'appuntamento sia stato effettivamente cancellato. Viene restituito il nuovo calendario.
8.6.7.11. L'azione [/validateAppointment]
Richiediamo l'azione [validateAppointment] con il seguente valore inviato:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato è il seguente:
![]() |
Possiamo verificare nel database che l'appuntamento è stato creato correttamente. Il nuovo calendario è stato restituito.
Facciamo la stessa cosa con un numero di slot inesistente:
![]() |
Facciamo lo stesso con un ID cliente inesistente:
![]() |
8.6.8. Passaggio 5: Scrittura del client JavaScript
Torniamo all'architettura del server [Web1]:
![]() |
Il client [2] del server [Web1] è un client JavaScript di tipo SPV (Single-Page Application):
- Il client richiede la pagina di avvio da un server web (non necessariamente [Web1]);
- richiede le pagine seguenti dal server [Web1] tramite chiamate Ajax;
Per creare questo client, useremo lo strumento [Webstorm] (vedi sezione 9.8). Ho trovato questo strumento più pratico di STS. Il suo vantaggio principale è che offre il completamento automatico del codice e alcune opzioni di refactoring. Questo aiuta a prevenire molti errori.
8.6.8.1. Il progetto JS
Il progetto JS presenta la seguente struttura di directory:
![]() |
- in [1], il client JS nel suo complesso. [boot.html] è la pagina di avvio. Questa sarà l'unica pagina caricata dal browser;
- in [2], i fogli di stile per i componenti Bootstrap;
- in [3], le poche immagini utilizzate dall'applicazione;
![]() |
- in [4], gli script JS. È qui che si svolge il nostro lavoro;
- in [5], le librerie JS utilizzate: principalmente jQuery e quelle per i componenti Bootstrap;
8.6.8.2. L'architettura del codice
Il codice è stato suddiviso in tre livelli:
![]() |
- il livello [presentazione] contiene le funzioni di inizializzazione della pagina [boot.xml] e quelle relative ai vari componenti Bootstrap. È implementato dal file [ui.js];
- il livello [events] contiene tutti i gestori di eventi per il livello [presentation]. È implementato dal file [evts.js];
- il livello [DAO] effettua richieste HTTP al server [Web1]. È implementato dal file [dao.js];
8.6.8.3. Il livello [presentation]
![]() |
Il livello [presentazione] è implementato dal seguente file [ui.js]:
//la couche [présentation]
var ui = {
// variables globales;
"agenda": "",
"resa": "",
"langue": "",
"urlService": "http://localhost:8081",
"page": "login",
"jourAgenda": "",
"idMedecin": "",
"user": {},
"login": {},
"exceptionTitle": {},
"calendar_infos": {},
"erreur": "",
"idCreneau": "",
"done": "",
// composants de la vue
"body": "",
"navbar": "",
"jumbotron": "",
"content": "",
"exception": "",
"exception_text": "",
"exception_title": "",
"loading": ""
};
// la couche des evts
var evts = {};
// la couche [dao]
var dao = {};
// ------------ document ready
$(document).ready(function () {
// initialisation document
console.log("document.ready");
// composants de la page
ui.navbar = $("#navbar");
ui.jumbotron = $("#jumbotron");
ui.content = $("#content");
ui.erreur = $("#erreur");
ui.exception = $("#exception");
ui.exception_text = $("#exception-text");
ui.exception_title = $("#exception-title");
// on mémorise la page de login pour pouvoir la restituer
ui.login.lang = ui.langue;
ui.login.navbar = ui.navbar.html();
ui.login.jumbotron = ui.jumbotron.html();
ui.login.content = ui.content.html();
// URL du service
$("#urlService").val(ui.urlService);
});
// ------------------------ Bootstrap component initialization functions
ui.initNavBarStart = function () {
...
};
ui.initNavBarRun = function () {
...
};
ui.initChoixMedecinJour = function () {
...
};
ui.updateCalendar = function (renew) {
...
};
// affiche le jour sélectionné
ui.displayJour = function () {
...
};
ui.initAgenda = function () {
...
};
ui.initResa = function () {
...
};
- Per isolare i livelli l'uno dall'altro, si è deciso di inserirli in tre oggetti:
- [ui] per il livello [presentazione] (righe 2–27),
- [evts] per il livello di gestione degli eventi (riga 29),
- [dao] per il livello [DAO] (riga 31);
Questa separazione dei livelli in tre oggetti aiuta a evitare una serie di conflitti tra i nomi delle variabili e delle funzioni. Ogni livello utilizza variabili e funzioni precedute dal prefisso dell'oggetto che incapsula il livello.
- righe 38–44: memorizziamo i campi che saranno sempre presenti indipendentemente dalle viste visualizzate. Questo evita ricerche jQuery ripetitive e inutili;
- righe 46–49: la pagina di avvio viene memorizzata localmente in modo da poter essere ripristinata quando l'utente esce dal sistema e non ha modificato la lingua;
- righe 54–83: funzioni di inizializzazione dei componenti Bootstrap. Queste sono state tutte trattate nella discussione sui componenti Bootstrap nella sezione 8.6.4;
8.6.8.4. Funzioni di utilità del livello [events]
![]() |
I gestori di eventi sono stati inseriti nel file [evts.js]. Diverse funzioni vengono utilizzate regolarmente dai gestori di eventi. Le presentiamo ora:
// début d'attente
evts.beginWaiting = function () {
// début attente
ui.loading = $("#loading");
ui.loading.show();
ui.exception.hide();
ui.erreur.hide();
evts.travailEnCours = true;
};
// fin d'attente
evts.stopWaiting = function () {
// fin attente
evts.travailEnCours = false;
ui.loading = $("#loading");
ui.loading.hide();
};
// affichage résultat
evts.showResult = function (result) {
// on affiche les données reçues
var data = result.data;
// on analyse le status
switch (result.status) {
case 1:
// erreur ?
if (data.status == 2) {
ui.erreur.html(data.content);
ui.erreur.show();
} else {
if (data.navbar) {
ui.navbar.html(data.navbar);
}
if (data.jumbotron) {
ui.jumbotron.html(data.jumbotron);
}
if (data.content) {
ui.content.html(data.content)
}
if (data.agenda) {
ui.agenda = $("#agenda");
ui.resa = $("#resa");
}
}
break;
case 2:
// affichage erreur
evts.showException(data);
break;
}
};
// ------------ fonctions diverses
evts.showException = function (data) {
// affichage erreur
ui.exception.show();
ui.exception_text.html(data);
ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
- riga 2: la funzione [evts.beginwaiting] viene chiamata prima di qualsiasi azione [DAO] asincrona;
- righe 4-5: viene visualizzata l'immagine animata dell'attesa;
- righe 6-7: l'area di visualizzazione degli errori e delle eccezioni viene nascosta (non sono la stessa cosa);
- riga 8: si nota che è in corso un'attività asincrona;
- riga 12: la funzione [evts.stopwaiting] viene chiamata dopo che un'azione [DAO] asincrona ha restituito il proprio risultato;
- riga 14: si nota che l'operazione asincrona è completata;
- riga 15: l'immagine animata di attesa viene nascosta;
- riga 20: la funzione [evts.showResult] visualizza il risultato [result] di un'azione asincrona [DAO]. Il risultato è un oggetto JS della seguente forma: {'status':status,'data':data,'sendMeBack':sendMeBack}.
- righe 47–50: utilizzate se [result.status == 2]. Ciò si verifica quando il server [Web1] invia una risposta con un'intestazione di errore HTTP (ad es. 403 Forbidden). In questo caso, [data] è la stringa JSON inviata dal server per indicare l'errore;
- riga 25: caso in cui sia stata ricevuta una risposta valida dal server [Web1]. Il campo [data] contiene quindi la risposta del server: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
- riga 27: caso in cui il server [Web1] ha inviato una risposta di errore {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':errors};
- righe 28–29: viene visualizzata la vista [errori];
- righe 31-33: visualizzazione opzionale della barra di navigazione;
- righe 34-36: visualizzazione opzionale del jumbotron;
- righe 37-39: il campo [data.content] può essere visualizzato. A seconda dei casi, questo rappresenta una delle viste [home, calendar];
- righe 40-43: se il calendario è stato rigenerato, vengono recuperati alcuni riferimenti ai suoi componenti in modo che non debbano essere ricercati ogni volta che sono necessari;
- riga 54: la funzione [evts.showException] visualizza il testo dell'eccezione contenuto nel suo parametro [data];
- righe 57-58: viene visualizzato il testo dell'eccezione;
- riga 58: il titolo dell'eccezione dipende dalla lingua corrente;
Il file [evts.js] contiene oltre 300 righe di codice, che non commenterò nella loro interezza. Mi limiterò a evidenziare alcuni esempi per illustrare lo scopo di questo livello.
8.6.8.5. Accesso utente

Il login dell'utente è gestito dalla seguente funzione:
// ------------------------ connexion
evts.connecter = function () {
// retrieve the values to be posted
var login = $("#login").val().trim();
var passwd = $("#passwd").val().trim();
// set the server's URL
ui.urlService = $("#urlService").val().trim();
dao.setUrlService(ui.urlService);
// query parameters
var post = {
"user": {
"login": login,
"passwd": passwd
},
"lang": ui.langue
};
var sendMeBack = {
"user": {
"login": login,
"passwd": passwd
},
"caller": evts.connecterDone
};
// query
evts.execute([{
"name": "accueil-sans-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- righe 4-5: recupera il nome utente e la password dell'utente;
- righe 7-8: recupera l'URL del servizio [Web1]. Viene memorizzato sia nel livello [ui] che nel livello [dao];
- righe 10-16: il valore da inviare: la lingua corrente e l'utente che sta tentando di effettuare l'accesso;
- righe 17–23: l'oggetto [sendMeBack] viene passato alla funzione [DAO] che verrà chiamata, e questa funzione deve restituirlo alla funzione alla riga 22. Qui, l'oggetto [sendMeBack] incapsula l'utente che sta tentando di effettuare il login;
- righe 25–29: la funzione [evts.execute] è in grado di eseguire una sequenza di azioni asincrone. Qui, passiamo una lista composta da una singola azione. I suoi campi sono i seguenti:
- [name]: il nome dell'azione asincrona da eseguire,
- [post]: il valore da inviare al server [Web1],
- [sendMeBack]: il valore che l'azione asincrona deve restituire insieme al suo risultato;
Prima di entrare nei dettagli della funzione [evts.execute], diamo un'occhiata alla funzione [evts.connecterDone] alla riga 22. Questa è la funzione alla quale la funzione [DAO] asincrona chiamata deve restituire il proprio risultato:
evts.connecterDone = function (result) {
// affichage résultat
evts.showResult(result);
// connexion réussie ?
if (result.status == 1 && result.data.status == 1) {
// page
ui.page = "accueil-sans-agenda";
// on note l'utilisateur
ui.user = result.sendMeBack.user;
}
};
- riga 3: viene visualizzato il risultato restituito dal server [Web1];
- riga 5: se questo risultato non contiene errori, memorizziamo il tipo della nuova pagina (riga 7) e l'utente autenticato (riga 9);
La funzione [evts.execute] esegue una sequenza di azioni asincrone:
// exécution d'une suite d'actions
evts.execute = function (actions) {
// travail en cours ?
if (evts.travailEnCours) {
// on ne fait rien
return;
}
// attente
evts.beginWaiting();
// exécution des actions
dao.doActions(actions, evts.stopWaiting);
};
- riga 2: il parametro [actions] è un elenco di azioni asincrone da eseguire;
- righe 4–7: l'esecuzione è accettata solo se non è già in corso nessun'altra azione;
- riga 9: viene avviata l'attesa;
- riga 11: al livello [DAO] viene richiesto di eseguire la sequenza di azioni. Il secondo parametro è il nome della funzione da eseguire una volta che tutte le azioni della sequenza hanno restituito i propri risultati;
Non entreremo nei dettagli della funzione [dao.doActions] in questa sede. Esamineremo un altro evento.
8.6.8.6. Cambio di lingua

Il cambio di lingua è gestito dalla seguente funzione:
// ------------------------ changement de langue
evts.setLang = function (lang) {
// chgt de langue ?
if (lang == ui.langue) {
// on ne fait rien
return;
}
// nouvelle langue
ui.langue = lang;
// quelle page faut-il traduire ?
switch (ui.page) {
case "login":
evts.getLogin();
break;
case "accueil-sans-agenda":
evts.getAccueilSansAgenda();
break;
case "accueil-avec-agenda":
evts.getAccueilAvecAgenda(ui);
break;
}
};
- riga 2: il parametro [lang] è la nuova lingua: 'fr' o 'en';
- righe 4–7: se la nuova lingua è quella corrente, non fare nulla;
- riga 9: la nuova lingua viene memorizzata;
- righe 12–20: se la lingua è cambiata, la pagina attualmente visualizzata dal browser deve essere ricaricata. Ci sono tre pagine possibili:
- quella denominata [login], in cui viene visualizzata la pagina di accesso,
- quella denominata [home-without-calendar], che è la pagina visualizzata immediatamente dopo l'autenticazione riuscita,
- quella denominata [home-with-calendar], che è la pagina visualizzata non appena viene visualizzato il primo calendario. Rimane quindi sullo schermo fino a quando l'utente non effettua il logout;
Affronteremo il caso della pagina [home-with-calendar]. Esistono tre versioni di questa funzione:
![]() |
- la versione [getAccueilAvecAgenda-one] esegue una singola azione asincrona;
- la versione [getAccueilAvecAgenda-parallel] esegue quattro azioni asincrone in parallelo;
- la versione [getAccueilAvecAgenda-sequence] esegue quattro azioni asincrone una dopo l'altra;
8.6.8.7. La funzione [getAccueilAvecAgenda-one]
Si tratta della seguente funzione:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// query parameters
var post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
var sendMeBack = {
"caller": evts.getAccueilAvecAgendaDone
};
// request
evts.execute([{
"name": "accueil-avec-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- righe 4-9: il valore da inviare incapsula l'utente connesso, la lingua desiderata, l'ID del medico di cui si desidera conoscere l'orario e il giorno dell'appuntamento desiderato;
- righe 10–12: l'oggetto [sendMeBack] è l'oggetto che verrà restituito alla funzione alla riga 11. Qui non contiene alcuna informazione;
- righe 14–18: esecuzione di una sequenza di azioni asincrone, in particolare quella denominata [welcome-with-calendar] (riga 15);
- riga 11: la funzione eseguita quando l'azione asincrona [welcome-with-calendar] restituisce il suo risultato;
La funzione [evts.getAccueilAvecAgendaDone] alla riga 11 visualizza il risultato della funzione asincrona denominata [accueil-avec-agenda]:
evts.getAccueilAvecAgendaDone = function (result) {
// affichage résultat
evts.showResult(result);
// nouvelle page ?
if (result.status == 1 && result.data.status == 1) {
ui.page = "accueil-avec-agenda";
}
};
- riga 1: [result] è il risultato della funzione asincrona denominata [home-with-calendar];
- riga 3: questo risultato viene visualizzato;
- riga 5: se si tratta di un risultato senza errori, viene caricata la nuova pagina (riga 6);
8.6.8.8. La funzione [getHomeWithCalendar-parallel]
Si tratta della seguente funzione:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// actions [navbar-run, jumbotron, home, calendar] in //
// navbar-run
var navbarRun = {
"name": "navbar-run"
};
navbarRun.post = {
"lang": ui.langue
};
navbarRun.sendMeBack = {
"caller": evts.showResult
};
// jumbotron
var jumbotron = {
"name": "jumbotron"
};
jumbotron.post = {
"lang": ui.langue
};
jumbotron.sendMeBack = {
"caller": evts.showResult
};
// home
var accueil = {
"name": "accueil"
};
accueil.post = {
"lang": ui.langue,
"user": ui.user
};
accueil.sendMeBack = {
"caller": evts.showResult
};
// agenda
var agenda = {
"name": "agenda"
};
agenda.post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin': ui.idMedecin,
'jour': ui.jourAgenda,
"caller": evts.getAgendaDone
};
// execution actions in //
evts.execute([navbarRun, jumbotron, accueil, agenda])
};
- riga 51: questa volta vengono eseguite quattro azioni asincrone. Verranno eseguite in parallelo;
- righe 5–13: definizione dell'azione [navbarRun], che recupera la barra di navigazione [navbar-run];
- riga 12: la funzione da eseguire una volta che l'azione asincrona [navbarRun] ha restituito il suo risultato;
- righe 15–23: definizione dell'azione [jumbotron], che recupera la vista [jumbotron];
- riga 22: la funzione da eseguire quando l'azione asincrona [jumbotron] restituisce il suo risultato;
- righe 25–34: definizione dell'azione [home], che recupera la vista [home];
- riga 33: la funzione da eseguire quando l'azione asincrona [home] restituisce il suo risultato;
- righe 36–49: definizione dell'azione [agenda] che recupera la vista [jumbotron];
- riga 48: la funzione da eseguire quando l'azione asincrona [agenda] restituisce il suo risultato;
8.6.8.9. La funzione [getHomeWithAgenda-sequence]
Si tratta della seguente funzione:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// actions [navbar-run, jumbotron, home, agenda] in order
// agenda
var agenda = {
"name" : "agenda"
};
agenda.post = {
"user" : ui.user,
"lang" : ui.langue,
"idMedecin" : ui.idMedecin,
"jour" : ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin' : ui.idMedecin,
'jour' : ui.jourAgenda,
"caller" : evts.getAgendaDone
};
// home
var accueil = {
"name" : "accueil"
};
accueil.post = {
"lang" : ui.langue,
"user" : ui.user
};
accueil.sendMeBack = {
"caller" : evts.showResult,
"next" : agenda
};
// jumbotron
var jumbotron = {
"name" : "jumbotron"
};
jumbotron.post = {
"lang" : ui.langue
};
jumbotron.sendMeBack = {
"caller" : evts.showResult,
"next" : accueil
};
// navbar-run
var navbarRun = {
"name" : "navbar-run"
};
navbarRun.post = {
"lang" : ui.langue
};
navbarRun.sendMeBack = {
"caller" : evts.showResult,
"next" : jumbotron
};
// execution actions in sequence
evts.execute([ navbarRun ])
};
- riga 54: viene eseguita l'azione [navbarRun]. Una volta terminata, si passa a quella successiva: [jumbotron], riga 51. Anche questa azione viene eseguita a sua volta. Una volta terminata, si passa a quella successiva: [home], riga 40. Anche questa viene eseguita a sua volta. Una volta terminata, si passa a quella successiva: [agenda], riga 29. Anche questa viene eseguita a sua volta. Una volta terminata, ci si ferma perché l'azione [agenda] non ha azioni successive.
8.6.8.10. Il livello [DAO]
![]() |
Il file [dao.js] contiene tutte le funzioni del livello [DAO]. Le presenteremo gradualmente:
// URL exposed by the server
dao.urls = {
"login": "/getLogin",
"accueil": "/getAccueil",
"jumbotron": "/getJumbotron",
"agenda": "/getAgenda",
"supprimerRv": "/supprimerRv",
"validerRv": "/validerRv",
"navbar-start": "/getNavbarStart",
"navbar-run": "/getNavbarRun",
"accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
"accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- interface
// server url
dao.setUrlService = function (urlService) {
dao.urlService = urlService;
};
- righe 16–18: la funzione che imposta l'URL del servizio [Web1];
- righe 2-13: il dizionario che associa il nome di un'azione asincrona all'URL del server [Web1] da interrogare;
// ------------------ gestion générique des actions
// exécution d'une suite d'actions asynchrones
dao.doActions = function (actions, done) {
// traitement des actions
dao.actionsCount = actions.length;
dao.actionIndex = 0;
for (var i = 0; i < dao.actionsCount; i++) {
// requête DAO asynchrone
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, actions[i], done);
}
};
- riga 3: la funzione [dao.doActions] esegue una sequenza di azioni asincrone [actions]. Il parametro [done] è la funzione da eseguire una volta che tutte le azioni hanno restituito i propri risultati;
- righe 7–12: le azioni asincrone vengono eseguite in parallelo. Tuttavia, se una di esse ha un successore, tale successore viene eseguito al termine dell'azione precedente;
- riga 9: un oggetto [Deferred] nello stato [pending];
- Riga 10: quando questo oggetto entra nello stato [resolved], verrà eseguita la funzione [dao.actionDone];
- riga 11: l'azione n. i dell'elenco viene eseguita in modo asincrono. Il parametro [done] della riga 3 viene passato come argomento;
La funzione [dao.actionDone], che viene eseguita al termine di ogni azione asincrona, è la seguente:
// on a reçu un résultat
dao.actionDone = function (result) {
// caller ?
var sendMeBack = result.sendMeBack;
if (sendMeBack && sendMeBack.caller) {
sendMeBack.caller(result);
}
// next ?
if (sendMeBack && sendMeBack.next) {
// requête DAO asynchrone
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
}
// fini ?
dao.actionIndex++;
if (dao.actionIndex == dao.actionsCount) {
// done ?
if (sendMeBack && sendMeBack.done) {
sendMeBack.done(result);
}
}
};
- riga 2: la funzione [dao.actionDone] riceve il risultato [result] da una delle azioni asincrone presenti nell'elenco delle azioni da eseguire;
- righe 4–7: se l'azione asincrona completata ha specificato una funzione a cui restituire il risultato, tale funzione viene chiamata;
- righe 9–14: se l'azione asincrona completata ha un successore, allora quell'azione viene eseguita a sua volta;
- riga 16: un'azione viene completata. Il contatore delle azioni completate viene incrementato. Un'azione che ha un numero indeterminato di azioni successive conta come una sola azione;
- righe 19–21: se inizialmente era stata specificata una funzione [done] da eseguire una volta che tutte le azioni della sequenza avessero restituito i propri risultati, tale funzione viene ora eseguita;
Il metodo [dao.doAction] esegue un'azione asincrona:
// exécution d'une action
dao.doAction = function (deferred, action, done) {
// fonction done à embarquer dans l'action
if (action.sendMeBack) {
action.sendMeBack.done = done;
} else {
action.sendMeBack = {
"done": done
};
}
// exécution action
dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
- Righe 4–10: Come abbiamo appena visto, la funzione che gestirà il risultato dell'azione asincrona da eseguire deve avere accesso alla funzione [done]. Per farlo, inseriamo la funzione [done] nell'oggetto [sendMeBack], che farà parte del risultato dell'operazione asincrona;
- riga 12: Eseguiamo la funzione [dao.executePost], che effettua una richiesta HTTP al server [Web1]. L'URL di destinazione è l'URL associato al nome dell'azione da eseguire;
La funzione [dao.executePost] esegue una richiesta HTTP:
// requête HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
// on fait un appel Ajax à la main
$.ajax({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
url: dao.urlService + url,
type: 'POST',
data: JSON3.stringify(post),
dataType: 'json',
success: function (data) {
// on rend le résultat
deferred.resolve({
"status": 1,
"data": data,
"sendMeBack": sendMeBack
});
},
error: function (jqXHR, textStatus, errorThrown) {
var data;
if (jqXHR.responseText) {
data = jqXHR.responseText;
} else {
data = textStatus;
}
// on rend l'erreur
deferred.resolve({
"status": 2,
"data": data,
"sendMeBack": sendMeBack
});
}
});
};
Abbiamo già incontrato e discusso questa funzione. Si noti semplicemente alla riga 9 che l'URL di destinazione è la concatenazione dell'URL del server [Web1] con l'URL associato al nome dell'azione.
8.6.8.11. La pagina di avvio
![]() |

La pagina di avvio [boot.html] mostra la schermata riportata sopra. È l'unica pagina caricata direttamente dal browser. Le altre vengono recuperate tramite chiamate Ajax. Il suo codice è il seguente:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
<link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
<link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
<link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
<!-- Custom styles for this template -->
<link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap.js"></script>
<script type="text/javascript" src="vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="vendor/footable.js"></script>
<!-- user scripts -->
<script type="text/javascript" src="js/json3.js"></script>
<script type="text/javascript" src="js/ui.js"></script>
<script type="text/javascript" src="js/evts.js"></script>
<script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
<script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
<!-- identification form -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
</div>
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
</div>
<button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:evts.setLang('fr')">Français</a></li>
<li><a href="javascript:evts.setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Bootstrap Jumbotron -->
<div id="jumbotron">
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>
Cabinet médical<br/>Les Médecins associés
</h1>
</div>
</div>
</div>
</div>
<!-- error panels -->
<div id="erreur"></div>
<div id="exception" class="alert alert-danger" style="display: none">
<h3 id="exception-title"></h3>
<span id="exception-text"></span>
</div>
<!-- content -->
<div id="content">
<div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
</div>
</div>
<!-- init page -->
<script>
// on initialise la page
ui.langue = 'fr';
ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
ui.exceptionTitle['en'] = "The following server error was met:";
ui.initNavBarStart();
</script>
</body>
</html>
- Abbiamo già incontrato questo tipo di pagina nel capitolo su Bootstrap (sezione 8.6.4);
- righe 99–105: inizializzazione di alcuni elementi del livello [presentazione];
- riga 27: viene utilizzato lo script [getAccueilAvecAgenda-sequence.js]. Modificando lo script in questa riga, otteniamo tre diversi comportamenti per il recupero della pagina [accueil-avec-agenda]:
- [getAccueilAvecAgenda-one.js] recupera la pagina con una singola richiesta HTTP,
- [getAccueilAvecAgenda-parallel.js] recupera la pagina con quattro richieste HTTP simultanee,
- [getAccueilAvecAgenda-sequence.js] recupera la pagina con quattro richieste HTTP successive;
8.6.8.12. Test
Esistono diversi modi per eseguire i test. In questo caso, utilizzeremo lo strumento [Webstorm]:
![]() |
- in [1] apriamo un progetto. Selezioniamo semplicemente la cartella [2] contenente la struttura delle directory statiche (HTML, CSS, JS) del sito da testare;
![]() |
- in [3], il sito statico;
- In [4-5], carichiamo la pagina [boot.html];
![]() |
- in [5], vediamo che un server incorporato da [Webstorm] ha servito la pagina [boot.html] dalla porta [63342]. Questo è un punto importante da comprendere perché significa che gli script presenti nella pagina [boot.html] effettueranno richieste cross-domain al server [Web1], che è in esecuzione su [localhost:8081]. Il browser che ha caricato [boot.html] sa di averla caricata da [localhost:63342]. Non permetterà quindi a questa pagina di effettuare chiamate al sito [localhost:8081] poiché non si tratta della stessa porta. Applicherà quindi le regole cross-domain descritte nella sezione 8.4.14. Per questo motivo, l'applicazione [Web1] deve essere configurata per accettare queste richieste cross-domain. La configurazione avviene nel file [AppConfig] del server Spring/Thymeleaf:
![]() |
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// root web service / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// timeout in milliseconds
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
Lasciamo al lettore il compito di testare il client JS. Dovrebbe essere in grado di riprodurre la funzionalità descritta nella sezione 8.6.3.
Una volta convalidato il client JavaScript, è possibile distribuirlo nella cartella [Web1] del server per evitare di dover consentire le richieste cross-domain:
![]() |
In precedenza, abbiamo copiato il sito sottoposto a test nella cartella [src/main/resources/static]. A questo punto possiamo richiedere l'URL [http://localhost:8081/boot.html]:

Ora non abbiamo più bisogno di richieste cross-domain e possiamo scrivere quanto segue nel file di configurazione [AppConfig] del server [Web1]:
// CORS
private final boolean CORS_ALLOWED=false;
L'applicazione sopra riportata continuerà a funzionare. Se torniamo all'applicazione [WebStorm], questa non funziona più:


Se andiamo alla console di sviluppo (Ctrl-Shift-I), vediamo la causa dell'errore:

Si tratta di un errore di richiesta cross-domain non autorizzata.
8.6.8.13. Conclusione
Abbiamo implementato la seguente architettura JS:
![]() |
- i livelli sono separati in modo abbastanza chiaro;
- abbiamo un'applicazione a pagina singola (SPA). È questa caratteristica che ora ci consentirà di generare un'app nativa per varie piattaforme mobili (Android, iOS, Windows Phone);
- abbiamo creato un modello in grado di eseguire azioni asincrone in parallelo, in sequenza o con una combinazione di entrambe;
8.6.9. Passaggio 6: Generazione di un'app nativa per Android
Lo strumento [Phonegap] [http://phonegap.com/] consente di produrre un eseguibile per dispositivi mobili (Android, iOS, Windows 8, ecc.) a partire da un'applicazione HTML/JS/CSS. Esistono diversi modi per ottenere questo risultato. Useremo il metodo più semplice: uno strumento online disponibile sul sito web di Phonegap [http://build.phonegap.com/apps]. Questo strumento caricherà il file ZIP del sito statico da convertire. La pagina di avvio deve essere denominata [index.html]. Quindi rinominiamo la pagina [boot.html] in [index.html]:
![]() |
quindi comprimiamo la cartella, in questo caso [rdvmedecins-client-js-03]. Successivamente, andiamo sul sito web di Phonegap [http://build.phonegap.com/apps]:
![]() |
- Prima di [1], potrebbe essere necessario creare un account;
- in [1], iniziamo;
- al punto [2], scegliamo un piano gratuito che consente di creare una sola app Phonegap;
![]() |
- in [3], carichiamo l'app compressa [4];
![]() |
- in [5], assegniamo un nome all'app;
- in [6], la compiliamo. L'operazione potrebbe richiedere 1 minuto. Attendiamo che le icone delle varie piattaforme mobili indichino che la compilazione è terminata;
![]() |
- sono stati generati solo i file binari per Android [7] e Windows [8];
- Clicca su [7] per scaricare il file binario per Android;
![]() |
- in [9], il file binario [apk] scaricato;
Avviare un emulatore [GenyMotion] per tablet Android (vedere la sezione 9.9):
![]() |
Sopra, avviamo un emulatore di tablet con API Android 19. Una volta avviato l'emulatore,
- sbloccarlo trascinando il lucchetto (se presente) di lato e poi rilasciandolo;
- Con il mouse, trascinate il file [PGBuildApp-debug.apk] che avete scaricato e rilasciatelo sull'emulatore. Verrà quindi installato ed eseguito;
![]() |
È necessario modificare l'URL in [1]. Per farlo, in una finestra del prompt dei comandi, digita il comando [ipconfig] (riga 1 qui sotto), che visualizzerà i vari indirizzi IP del tuo computer:
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. . . :
...
Annotare l'indirizzo IP Wi-Fi (righe 6–9) o l'indirizzo IP della rete locale (righe 11–17). Quindi utilizzare questo indirizzo IP nell'URL del server web:
![]() |
Una volta fatto ciò, connettiti al servizio web:
![]() |
Prova l'applicazione sull'emulatore. Dovrebbe funzionare. Sul lato server, puoi scegliere se consentire o meno le intestazioni CORS nella classe [ApplicationModel]:
// CORS
private final boolean CORS_ALLOWED=false;
Questo non ha importanza per l'app Android. Non viene eseguita in un browser. Il requisito relativo alle intestazioni CORS proviene dal browser, non dal server.
8.6.10. Conclusione del caso di studio
Abbiamo sviluppato la seguente architettura:
![]() |
Si tratta di un'architettura complessa a 3 livelli. È stata progettata per riutilizzare il livello [Web2], che era il livello server dell'applicazione [AngularJS-Spring MVC] descritta nel documento [Tutorial AngularJS / Spring 4] all'URL [http://tahe.developpez.com/angularjs-spring4/]. Questo è l'unico motivo per cui abbiamo un'architettura a tre livelli. Mentre nell'applicazione [AngularJS-Spring MVC] il client di [Web2] era un client [AngularJS], qui il client di [Web2] è un'architettura a due livelli [jQuery] / [Spring MVC / Thymeleaf]. Abbiamo aumentato il numero di livelli, quindi perderemo un po' di prestazioni.
L'applicazione qui discussa è stata sviluppata nel corso del tempo in tre diversi documenti:
- [Introduzione a JSF2, PrimeFaces e PrimeFaces Mobile] all'URL [http://tahe.developpez.com/java/primefaces/]. Il caso di studio è stato poi sviluppato utilizzando i framework JSF2 / PrimeFaces. PrimeFaces è una libreria di componenti abilitati per AJAX che elimina la necessità di scrivere JavaScript. L'applicazione sviluppata in quel momento era meno complessa di quella studiata qui. Aveva una versione web classica per computer e una versione mobile per telefoni;
- [Tutorial AngularJS / Spring 4] all'URL [http://tahe.developpez.com/angularjs-spring4/]. L'applicazione sviluppata all'epoca aveva le stesse caratteristiche di quella discussa in questo documento. L'applicazione era stata inoltre portata su Android;
- questo documento;
Da questo lavoro, mi sembrano degni di nota i seguenti punti:
- l'applicazione [Primefaces] era di gran lunga la più semplice da scrivere e la sua versione web mobile si è dimostrata altamente performante. Non richiede alcuna conoscenza di JavaScript. Non è possibile portarla in modo nativo sui sistemi operativi dei vari dispositivi mobili, ma è davvero necessario? Sembra difficile modificare lo stile dell'applicazione. Stiamo, infatti, lavorando con i fogli di stile di Primefaces. Questo potrebbe essere uno svantaggio;
- L'applicazione [AngularJS-Spring MVC] era complessa da scrivere. Il framework [AngularJS] sembrava piuttosto difficile da comprendere una volta che si voleva padroneggiarlo. L'architettura [client Angular] / [servizio web / JSON implementato da Spring MVC] è particolarmente pulita e ad alte prestazioni. Questa architettura è replicabile per qualsiasi applicazione web. È l'architettura che mi sembra più promettente perché coinvolge diverse competenze sul lato client e sul lato server (JS+HTML+CSS sul lato client, Java o altro sul lato server), il che permette di sviluppare client e server in parallelo;
- Per l'applicazione sviluppata in questo documento utilizzando un'architettura a 3 livelli [client jQuery] / [server Web1 / Spring MVC / Thymeleaf] / [server Web2 / Spring MVC], alcuni potrebbero trovare la tecnologia [jQuery+Spring MVC+Thymeleaf] più facile da comprendere rispetto a quella di [AngularJS]. Il livello [DAO] del client JavaScript che abbiamo scritto è riutilizzabile in altre applicazioni;

























































































































































































































































































