8. Caso di studio
8.1. Introduction
Ci proponiamo di scrivere un'applicazione web per la prenotazione di appuntamenti per uno studio medico. Questo problema è stato trattato nel documento «Tutorial AngularJS / Spring 4» alle sezioni URL e [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 (Modello – Vista – Controller). Il modello in questo caso è sia quello delle viste sia quello del dominio, rappresentato qui dal livello [Services];
- l’utente interagirà con le viste che gli vengono presentate nel browser. Le sue azioni richiederanno talvolta l’interrogazione del server Spring 4 [2]. Quest’ultimo elaborerà la richiesta e restituirà una risposta jSON (JavaScript Object Notation) [3]. Questa verrà utilizzata per aggiornare la vista presentata all’utente.
Ci proponiamo di riprendere questa applicazione e di implementarla dall’inizio alla fine con Spring MVC. L’architettura diventa quindi la seguente:
![]() |
Il browser si collegherà a un’applicazione [Web 1] implementata con Spring MVC, che recupererà i dati da un servizio web [Web 2], anch’esso implementato con Spring MVC.
8.2. Funzionalità dell’applicazione
Il lettore è invitato a scoprire le funzionalità dell’applicazione provandola. Carichiamo in STS i progetti Maven presenti nella cartella [etude-de-cas]:
![]() | ![]() |
Per prima cosa creeremo il database MySQL 5 [dbrdvmedecins] con lo strumento [Wamp Server] (cfr. paragrafo 9.5):
![]() |
- in [1], si seleziona lo strumento [phpMyAdmin] da WampServer;
- in [2], si sceglie l'opzione [Importer];
![]() |
- in [3], selezionare il file [database/dbrdvmedecins.sql];
- in [4], lo si esegue;
- in [5], viene creato il database.
Successivamente, occorre avviare il server collegato al database. Si tratta del progetto [rdvmedecins-webjson-server]
![]() |
Il server sarà disponibile all'indirizzo URL [http://localhost:8080]. Questo può essere modificato nel file [application.properties] del progetto:
![]() |
server.port=8080
Le impostazioni di accesso al database sono memorizzate nella classe [DomainAndPersistenceConfig] del progetto [rdvmedecins-metier-dao]:
![]() |
// la fonte dei dati MySQL
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
Se si accede a SGBD MySQL con altre credenziali, è qui che avviene l'operazione.
Si avvia quindi, allo stesso modo del server precedente, il server [rdvmedecins-springthymeleaf-server]:
![]() | ![]() |
Questo server è disponibile per impostazione predefinita all'indirizzo URL [http://localhost:8081]. Anche in questo caso, è configurabile 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";
// radice servizio web / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// timeout in millisecondi
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
Se il primo server è stato avviato su una porta diversa dalla 8080, è necessario modificare la riga 5.
Successivamente, tramite un browser, si richiede l'accesso a URL e [http://localhost:8081/boot.html]:
![]() |
- in [1], la pagina di accesso all'applicazione;
- in [2] e [3], rispettivamente l'ID e la password di chi desidera utilizzare l'applicazione. Sono presenti due utenti: admin/admin (login/password) con un ruolo (ADMIN) e user/user con un ruolo (USER). Solo il ruolo ADMIN ha il diritto di utilizzare l'applicazione. Il ruolo USER serve solo a mostrare 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: il francese (predefinito) e l’inglese;
- in [6], l'URL del server [rdvmedecins-springthymeleaf-server];
![]() |
- in [1], si effettua l'accesso;
![]() |
- una volta effettuato l'accesso, è possibile scegliere il medico con cui si desidera fissare un appuntamento [2] e il giorno dell'appuntamento [3]. Non appena vengono inseriti il medico e il giorno, viene visualizzata automaticamente l'agenda:
![]() |
- una volta visualizzato il calendario del medico, è possibile prenotare una fascia oraria [5];
![]() |
- in [6], si seleziona il paziente per l’appuntamento e si conferma la scelta in [7];
![]() |
Una volta confermato l'appuntamento, si torna automaticamente all'agenda dove il nuovo appuntamento è ora registrato. Questo appuntamento potrà essere successivamente cancellato con [8].
Le funzionalità principali sono state descritte. Sono semplici. Concludiamo con la gestione della lingua:
1

- in [1], si passa dal francese all’inglese;
![]() |
- in [2], la visualizzazione è passata all'inglese, compreso il calendario;
8.3. Il database
![]() |
Il database denominato in seguito [dbrdvmedecins] è un database MySQL5 con le seguenti tabelle:
![]() |
Gli appuntamenti sono gestiti dalle seguenti tabelle:
- [medecins]: contiene l'elenco dei medici dello studio;
- [clients]: contiene l'elenco dei pazienti dello studio;
- [creneaux]: contiene le fasce orarie di ciascun medico;
- [rv]: contiene l'elenco degli appuntamenti dei medici.
Le tabelle [roles], [users] e [users_roles] sono tabelle relative all’autenticazione. Per il momento 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 riunisce sia un cliente che un medico tramite una fascia oraria di quest’ultimo;
- un cliente ha 0 o più appuntamenti;
- a una fascia oraria sono associati 0 o più appuntamenti (in giorni diversi).
8.3.1. La tabella [MEDECINS]
Contiene informazioni sui medici gestiti dall’applicazione [RdvMedecins].
![]() | ![]() |
- ID: numero identificativo del 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.
- NOM: il cognome del medico
- PRENOM: il suo nome
- TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)
8.3.2. La tabella [CLIENTS]
I pazienti dei diversi medici sono registrati nella tabella [CLIENTS]:
![]() | ![]() |
- 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.
- NOM: il nome del cliente
- PRENOM: il suo nome
- TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)
8.3.3. La tabella [CRENEAUX]
Elenca le fasce orarie in cui sono possibili i RV:
![]() |
![]() |
- 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.
- ID_MEDECIN: numero identificativo del medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna MEDECINS (ID).
- HDEBUT: ora di inizio della fascia oraria
- MDEBUT: minuti di inizio della fascia oraria
- HFIN: ora di fine della fascia oraria
- MFIN: minuti di fine della fascia oraria
La seconda riga della tabella [CRENEAUX] (cfr. [1] sopra) indica, ad esempio, che la fascia 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 i RV assegnati a ciascun medico:
![]() |
- ID: numero che identifica in modo univoco il RV – chiave primaria
- JOUR: giorno del RV
- ID_CRENEAU: fascia oraria del RV – chiave esterna sul campo [ID] della tabella [CRENEAUX] – determina sia la fascia oraria che il medico interessato.
- ID_CLIENT: numero del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]
Questa tabella presenta un vincolo di unicità sui valori delle colonne collegate (JOUR, ID_CRENEAU):
Se una riga della tabella [RV] presenta il valore (JOUR1, ID_CRENEAU1) per le colonne (JOUR, ID_CRENEAU), tale valore non può trovarsi in nessun altro punto. Altrimenti, ciò significherebbe che due RV sono stati registrati contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database avvia un SQLException quando si verifica questo caso.
La riga di id pari a 3 (cfr. [1] sopra) indica che un RV è stato prenotato per la fascia oraria n. 20 e il cliente n. 4 il 23/08/2006. La tabella [CRENEAUX] ci indica che la fascia n. 20 corrisponde alla fascia oraria 16:20 - 16:40 e appartiene al medico n. 1 (la sig.ra Marie PELISSIER). La tabella [CLIENTS] ci indica che il cliente n. 4 è la signorina Brigitte BISTROU.
8.3.5. Creazione del database
Per creare il database [dbrdvmedecins], viene fornito uno script [dbrdvmedecins.sql] insieme agli esempi contenuti nel presente documento [1-3]:
![]() |
Utilizziamo lo strumento [PhpMyAdmin] di WampServer:
![]() |
- in [1], si seleziona lo strumento [phpMyAdmin] da WampServer;
- in [2], si sceglie l'opzione [Importer];
![]() |
- in [3], selezionare il file [database/dbrdvmedecins.sql];
- in [4], lo si esegue;
- in [5], viene creato il database.
8.4. Il servizio web / jSON
![]() |
Nell'architettura sopra descritta, passiamo ora alla realizzazione del servizio web / jSON, sviluppato con il framework Spring MVC. Lo scriveremo in più fasi:
- innanzitutto i livelli [métier] e [DAO] (Data Access Object). Qui utilizzeremo Spring Data;
- poi il servizio web jSON senza autenticazione. Qui useremo Spring MVC;
- poi aggiungeremo la parte relativa all’autenticazione con Spring Security.
Quello che segue è una trascrizione del documento [http://tahe.developpez.com/angularjs-spring4/] con alcune modifiche.
8.4.1. Introduzione a Spring Data
Implementeremo il livello [DAO] del progetto con Spring Data, un ramo dell’ecosistema Spring.
![]() |
Sul sito di Spring sono disponibili numerosi tutorial per iniziare con Spring [http://spring.io/guides]. Ne utilizzeremo 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], si sceglie il tutorial [Accessing Data Jpa] che mostra come accedere a un database con Spring Data;
- in [3], si sceglie un progetto configurato da Maven;
- in [4], il tutorial può essere fornito 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>
<!-- utilizzare UTF-8 per tutto -->
<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 che definisce la maggior parte delle dipendenze del progetto. Possono essere sufficienti, nel qual caso non se ne aggiungono altre, oppure no, nel qual caso si aggiungono le dipendenze mancanti;
- righe 12-15: definiscono una dipendenza da [spring-boot-starter-data-jpa]. Questo artefatto contiene le classi di Spring Data;
- righe 16-19: definiscono una dipendenza da SGBD e H2, che consentono di creare e gestire database in memoria.
Diamo un'occhiata alle classi fornite da queste dipendenze:
![]() | ![]() | ![]() |
Sono davvero numerose:
- alcune appartengono all'ecosistema Spring (quelle che iniziano con spring);
- altre appartengono all'ecosistema Hibernate (hibernate, jboss), di cui qui si utilizza l'implementazione JPA;
- altre sono librerie di test (junit, hamcrest);
- altre sono librerie di log (log4j, logback, slf4j);
Le manterremo tutte. Per un'applicazione in produzione, bisognerebbe mantenere solo quelle necessarie.
Alla riga 26 del file [pom.xml] si trova la riga:
<start-class>hello.Application</start-class>
Questa riga è collegata alle righe seguenti:
<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 plugin [spring-boot-maven-plugin] consente di generare il file JAR eseguibile dell'applicazione. La riga 26 del file [pom.xml] indica quindi la classe eseguibile di questo JAR.
8.4.1.2. Il livello [JPA]
L’accesso al database avviene tramite un livello [JPA], Java Persistence API:
![]() |
![]() |
L'applicazione è semplice e gestisce i clienti [Customer]. La classe [Customer] 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 identificativo [id], un nome [firstName] e un cognome [lastName]. Ogni istanza [Customer] rappresenta una riga di una tabella del database.
- riga 8: annotazione JPA che fa sì che la persistenza delle istanze [Customer] (Create, Read, Update, Delete) venga gestita da un'implementazione JPA. In base alle dipendenze Maven, si vede 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 SGBD utilizzato, in questo caso H2;
Non sono presenti altre annotazioni relative a JPA. Verranno quindi utilizzati i valori predefiniti:
- la tabella di [Customer] porterà il nome della classe, ovvero [Customer];
- le colonne di questa tabella porteranno il nome dei campi della classe: [id, firstName, lastName], tenendo presente che nel nome di una colonna della tabella non viene fatta distinzione tra maiuscole e minuscole;
Si noti che in nessun momento viene menzionata l’implementazione JPA utilizzata.
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 di 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 è possibile eseguire su un tipo JPA T:
- riga 8: il metodo save consente di salvare un'entità T nel database. L'entità viene salvata con la chiave primaria assegnatale da SGBD. Consente inoltre di aggiornare un'entità T identificata dalla sua chiave primaria id. La scelta tra l'una o l'altra azione dipende dal valore della chiave primaria id: se questa è null, viene eseguita l'operazione di persistenza, altrimenti viene eseguita l'operazione di aggiornamento;
- riga 10: lo stesso, ma per un elenco di entità;
- riga 12: il metodo findOne consente di recuperare un'entità T identificata dalla sua chiave primaria id;
- riga 22: il metodo delete consente di eliminare un'entità T identificata dalla sua chiave primaria id;
- righe 24-28: varianti del metodo [delete];
- riga 16: il metodo [findAll] consente di recuperare tutte le entità T persistenti;
- riga 18: idem, ma limitato alle entità di cui è stato passato l’elenco degli 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 [Customer] tramite il suo nome [lastName];
E questo è tutto per il livello [DAO]. Non esiste una classe di implementazione dell’interfaccia precedente. Questa viene generata in fase di esecuzione da [Spring Data]. I metodi dell’interfaccia [CrudRepository] vengono implementati automaticamente. Per i metodi aggiunti nell’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 tramite la seguente query JPQL (Java Persistence Query Language):
È quindi necessario che il tipo T disponga di un campo denominato [something]. Pertanto, il metodo
verrà 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] indica il contesto di persistenza JPA. Ciò è possibile solo se la classe [Customer] dispone di un campo denominato [lastName], come 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);
// salva un paio di clienti
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"));
// recupera tutti i clienti
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();
// recupera un singolo cliente tramite ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// recupera i clienti in base al cognome
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();
}
}
- la riga 10: indica che la classe serve a configurare Spring. Le versioni recenti di Spring possono infatti essere configurate in Java anziché in XML. I due metodi possono essere utilizzati contemporaneamente. Nel codice di una classe con l’annotazione [Configuration] si trovano normalmente dei bean Spring, ovvero definizioni di classi da istanziare. In questo caso non è definito alcun bean. È importante ricordare che quando si lavora con un SGBD, devono essere definiti diversi 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.
- alla riga 11: l'annotazione [EnableAutoConfiguration] è un'annotazione proveniente dal progetto [Spring Boot] (righe 5-6). Questa annotazione richiede 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 SGBD si trova nel Classpath, il bean H2 verrà implementato con [dataSource]. Nel bean [dataSource], è necessario definire anche l'utente e la relativa password. In questo caso Spring Boot utilizzerà l’amministratore predefinito di H2, che non richiede password. Poiché la libreria [spring-tx] si trova nel Classpath, verrà utilizzato il gestore delle transazioni di Spring.
Inoltre, la cartella in cui si trova la classe [Application] verrà analizzata alla ricerca di bean riconosciuti implicitamente da Spring o definiti esplicitamente tramite annotazioni Spring. Verranno quindi ispezionate le classi [Customer] e [CustomerRepository]. Poiché la prima presenta 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] del progetto Spring Boot. Il suo parametro è la classe che presenta un'annotazione [Configuration] o [EnableAutoConfiguration]. A questo punto si verificherà tutto ciò che è stato spiegato in precedenza. Il risultato è un contesto applicativo Spring, ovvero un insieme di bean gestiti da Spring;
- riga 17: a questo contesto Spring viene richiesto un bean che implementi l’interfaccia [CustomerRepository]. Qui recuperiamo la classe generata da Spring Data per implementare tale interfaccia.
Le operazioni che seguono si limitano a utilizzare i metodi del bean che implementa l'interfaccia [CustomerRepository]. Si noti, alla riga 50, che il contesto è chiuso. I risultati visualizzati in console sono i seguenti:
- 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 dalla classe [LocalContainerEntityManagerFactory], una classe di Spring;
- riga 15: compare [Hibernate]. È stata scelta proprio questa implementazione, JPA;
- riga 19: un dialetto Hibernate è la variante SQL da utilizzare con SGBD. Qui il dialetto [H2Dialect] indica che Hibernate opererà con SGBD e H2;
- righe 21-22: viene creato il database. Viene creata la tabella [CUSTOMER]. Ciò significa che Hibernate è stato configurato per generare le tabelle a partire dalle definizioni JPA; in questo caso, la definizione JPA della classe [Customer];
- righe 27-31: i cinque clienti inseriti;
- righe 33-635: risultato del metodo [findOne] dell'interfaccia;
- righe 37-40: risultati del metodo [findByLastName];
- righe 41 e seguenti: log relativi alla 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 ci affideremo alla configurazione automatica effettuata da Spring Boot. La eseguiremo manualmente. Ciò può essere utile se le configurazioni predefinite non ci soddisfano.
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>
<!-- Transazioni Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.1.10.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- 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: le librerie di base di Spring;
- righe 19-29: le librerie di Spring per la gestione delle transazioni con un database;
- righe 30-35: la libreria di 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 SGBD H2;
- righe 60-70: i database vengono spesso utilizzati con pool di connessioni aperte che evitano ripetute aperture e chiusure di connessione. In questo caso, l’implementazione utilizzata è quella di [commons-dbcp];
Sempre in [pom.xml], si modifica 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, ma 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 ha più annotazioni 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 {
// la fonte dati H2
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// il 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();
}
// Gestore delle transazioni
@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] consente di indicare le cartelle in cui si trovano le interfacce Spring Data [CrudRepository]. Queste interfacce diventeranno componenti Spring e saranno disponibili nel relativo 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] consente di specificare le cartelle in cui devono essere ricercate le entità JPA. In questo caso è stata messa tra commenti, poiché tale 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 cartella della classe di configurazione;
- riga 18: l'annotazione [@ComponentScan] consente di elencare le cartelle in cui devono essere cercati i componenti Spring. I componenti Spring sono classi contrassegnate con annotazioni Spring quali @Service, @Component, @Controller, ... In questo caso non ce ne sono altre oltre a quelle definite all’interno della classe [Config], pertanto 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 può essere qualsiasi in questo caso. Tuttavia, deve essere denominato [dataSource] se il metodo EntityManagerFactory della riga 47 è assente e viene definito tramite autoconfigurazione;
- 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;
- riga 39: nessun log SQL;
- riga 30: il database verrà creato se non esiste;
- righe 46-54: definiscono l'EntityManagerFactory che gestirà la persistenza di JPA. Il metodo deve chiamarsi obbligatoriamente [entityManagerFactory];
- riga 47: il metodo riceve due parametri del tipo dei due bean definiti in precedenza. Questi verranno quindi creati e poi iniettati da Spring come parametri del metodo;
- riga 49: specifica l’implementazione JPA utilizzata;
- riga 50: specifica le cartelle in cui si trovano le entità JPA;
- riga 51: specifica la fonte dati da gestire;
- righe 57-62: il gestore delle transazioni. Il metodo deve chiamarsi obbligatoriamente [transactionManager]. Riceve come parametro il bean delle righe 46-54;
- riga 60: il gestore delle transazioni è associato a 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, quello del database H2:
![]() |
Infine, è possibile fare a meno di Spring Boot. Si crea una seconda classe eseguibile [Main2]:
![]() |
La classe [Main2] presenta 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]. Alla riga 5 si può notare che ora 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, è possibile procedere come segue:
![]() |
- in [1]: si crea una configurazione di esecuzione;
- in [2]: di tipo [Java Application]
- in [3]: si indica il progetto da eseguire (utilizzare il pulsante Browse);
- in [4]: indica la classe da eseguire;
- in [5]: il nome della configurazione di esecuzione – può essere qualsiasi;
![]() |
- in [6]: si esporta il progetto;
- in [7]: sotto forma di archivio eseguibile JAR;
- in [8]: indica il percorso e il nome del file eseguibile da creare;
- in [9]: il nome della configurazione di esecuzione creata in [5];
Fatto ciò, si apre una console nella cartella contenente l'archivio eseguibile:
L'archivio viene eseguito nel modo seguente:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
I risultati ottenuti nella console sono i seguenti:
8.4.1.7. Creare un nuovo progetto Spring Data
Per creare uno scheletro di progetto Spring Data, è possibile procedere come segue:
![]() |
- in [1], si crea un nuovo progetto;
- in [2]: di tipo [Spring Starter Project];
- il progetto generato sarà un progetto Maven. In [3], si indica il nome del gruppo del progetto;
- in [4]: si indica il nome dell'artefatto (in questo caso un jar) che verrà creato durante la compilazione del progetto;
- in [5]: si indica il pacchetto della classe eseguibile che verrà creata nel progetto;
- in [6]: il nome Eclipse del progetto – può essere qualsiasi nome (non deve necessariamente coincidere con [4]);
- in [7]: si indica che si intende creare un progetto con un livello [JPA]. Le dipendenze necessarie per tale progetto saranno quindi incluse nel file [pom.xml];
![]() |
- in [8]: il progetto creato;
Il file [pom.xml] integra 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/> <!-- ricerca del genitore dal 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: le dipendenze necessarie per JPA – includeranno [Spring Data];
- righe 13-17: le dipendenze necessarie per i test JUnit integrati con Spring;
La classe eseguibile [Application] non esegue alcuna operazione, 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 esegue alcuna operazione 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 beneficerà così di tutti i bean definiti da questo file;
- riga 8: l’annotazione [@RunWith] consente l’integrazione di Spring con JUnit: la classe potrà essere eseguita come un test JUnit. [@RunWith] è un'annotazione JUnit (riga 4), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 6);
Ora che disponiamo di uno scheletro dell’applicazione JPA, possiamo completarlo per scrivere il progetto del livello di persistenza del server della nostra applicazione di gestione degli appuntamenti.
8.4.2. Il progetto Eclipse del server
![]() |
![]() |
Gli elementi principali del progetto sono i seguenti:
- [pom.xml]: file di configurazione Maven del progetto;
- [rdvmedecins.entities]: le entità JPA;
- [rdvmedecins.repositories]: le interfacce Spring Data per l'accesso alle entità JPA;
- [rdvmedecins.metier]: il livello [métier];
- [rdvmedecins.domain]: le entità gestite dal livello [métier];
- [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>
<!-- Test Spring -->
<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>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- usa UTF-8 per tutto -->
<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 vengono dichiarate normalmente;
- 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 di SGBD MySQL5;
- righe 36-39: pool di connessioni Tomcat JDBC. Un pool di connessioni raggruppa le connessioni aperte verso un database. Quando il codice intende aprire una connessione, questa viene richiesta al pool. Quando il codice chiude la connessione, essa non viene chiusa ma restituita al pool. Tutto ciò avviene in modo trasparente a livello di codice. Si ottiene un miglioramento delle prestazioni poiché l’apertura e la chiusura ripetute di una connessione comportano un costo in termini di tempo. In questo caso il pool di connessioni stabilisce un certo numero di connessioni con il database al momento della sua istanziazione. Successivamente, non vi è né apertura né chiusura di connessioni, a meno che il numero di connessioni memorizzate nel pool non risulti 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 per la gestione delle collezioni;
8.4.4. Le entità JPA
![]() |
Le entità JPA sono gli oggetti che incapsulano le righe delle tabelle del database.
![]() |
La classe [AbstractEntity] è la classe padre delle entità [Personne, Creneau, Rv]. 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;
}
// inizializzazione
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1) || entity==null) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id.longValue() == other.id.longValue();
}
// getters e setters
..
}
- riga 11: l’annotazione [@MappedSuperclass] indica che la classe annotata è la classe padre delle entità JPA e [@Entity];
- righe 15-17: definiscono la chiave primaria [id] di ciascuna entità. È l’annotazione [@Id] a rendere il campo [id] una chiave primaria. L'annotazione [@GeneratedValue(strategy = GenerationType.IDENTITY)] indica che il valore di questa chiave primaria è generato da SGBD e che viene imposta la modalità di generazione [IDENTITY]. Per il SGBD MySQL, ciò significa che le chiavi primarie saranno generate dal SGBD 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à verrà modificata. Questo numero serve a impedire l’aggiornamento simultaneo 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 tale modifica nel database: il numero di versione passa quindi a V1+1. U2 a sua volta modifica E e salva tale modifica nel database: riceverà un'eccezione poiché possiede una versione (V1) diversa da quella presente nel database (V1+1);
- righe 29-33: il metodo [build] consente di inizializzare i due campi di [AbstractEntity]. Questo metodo rende il riferimento all’istanza [AbstractEntity] così inizializzato;
- righe 36-44: il metodo [equals] della classe viene ridefinito: due entità saranno considerate uguali se hanno lo stesso nome di classe e lo stesso identificatore id;
- righe 21-26: quando si ridefinisce il metodo [equals] di una classe, è necessario ridefinire anche il suo metodo [hashCode] (righe 21-26). La regola prevede che due entità considerate uguali dal metodo [equals] debbano quindi avere lo stesso [hashCode]. In questo caso, il [hashCode] di un'entità è uguale alla sua chiave primaria [id]. Il [hashCode] di una classe viene utilizzato in particolare nella gestione dei dizionari i cui valori sono istanze della classe;
L'entità [Personne] è la classe padre delle entità [Medecin] e [Client]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Personne extends AbstractEntity {
private static final long serialVersionUID = 1L;
// attributi di una persona
@Column(length = 5)
private String titre;
@Column(length = 20)
private String nom;
@Column(length = 20)
private String prenom;
// costruttore predefinito
public Personne() {
}
// costruttore con parametri
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);
}
// getter e setter
...
}
- riga 6: l'annotazione [@MappedSuperclass] indica che la classe annotata è la classe padre delle entità JPA e [@Entity];
- righe 10-15: una persona ha un titolo (Melle), 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;
// costruttore predefinito
public Medecin() {
}
// costruttore con parametri
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 [MEDECINS] del database;
- riga 8: l'entità [Medecin] deriva dall'entità [Personne];
Un medico potrà essere inizializzato nel modo seguente:
Se, inoltre, si desidera assegnargli un identificativo e una versione, si potrà 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;
// costruttore predefinito
public Client() {
}
// costruttore con parametri
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// identità
public String toString() {
return String.format("Client[%s]", super.toString());
}
}
- riga 6: la classe è un'entità JPA;
- riga 7: associata alla tabella [CLIENTS] del database;
- riga 8: l'entità [Client] deriva dall'entità [Personne];
L'entità [Creneau] è 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;
// caratteristiche di una fascia oraria di RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// un turno è associato a un medico
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// chiave esterna
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// costruttore predefinito
public Creneau() {
}
// costruttore con parametri
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);
}
// chiave esterna
public long getIdMedecin() {
return idMedecin;
}
// setter - getter
...
}
- riga 10: la classe è un'entità JPA;
- riga 11: associata alla tabella [CRENEAUX] del database;
- riga 12: l'entità [Creneau] deriva dall'entità [AbstractEntity] e eredita quindi l'identificativo [id] e la versione [version];
- riga 16: ora di inizio della fascia oraria (14);
- riga 17: minuti di inizio della fascia oraria (20);
- riga 18: ora di fine della fascia oraria (14);
- riga 19: minuti di fine della fascia oraria (40);
- righe 22-24: il medico titolare della fascia oraria. 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 da molti (fasce orarie) a uno (medico). L'attributo [fetch=FetchType.LAZY] indica che quando si richiede un'entità [Creneau] al contesto di persistenza e questa deve essere ricercata nel database, l'entità [Medecin] non viene recuperata insieme ad essa. Il vantaggio di questa modalità è che l’entità [Medecin] viene ricercata solo se lo sviluppatore lo richiede. In questo modo si risparmia memoria e si migliorano le prestazioni;
- riga 23: indica il nome della colonna 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 che lo standard JPA non ammette. Si aggiungono quindi gli attributi [insertable = false, updatable = false], in modo 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;
// caratteristiche di un Rv
@Temporal(TemporalType.DATE)
private Date jour;
// un RV è associato a un cliente
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// un RV è collegato a una fascia oraria
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// chiavi esterne
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
// Costruttore predefinito
public Rv() {
}
// con parametri
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);
}
// chiavi esterne
public long getIdCreneau() {
return idCreneau;
}
public long getIdClient() {
return idClient;
}
// getter e setter
...
}
- riga 14: la classe è un'entità JPA;
- riga 15: associata alla tabella [RV] del database;
- riga 16: l'entità [Rv] deriva dall'entità [AbstractEntity] e eredita quindi l'identificativo [id] e la versione [version];
- riga 21: la data dell'appuntamento;
- riga 20: il tipo [Date] di Java contiene sia una data che un'ora. Qui si specifica 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] con Spring Data:
![]() |
Il livello [DAO] è implementato con quattro interfacce Spring Data:
- [ClientRepository]: fornisce l’accesso alle entità JPA e [Client];
- [CreneauRepository]: fornisce l'accesso alle entità JPA e [Creneau];
- [MedecinRepository]: consente l'accesso alle entità JPA e [Medecin];
- [RvRepository]: consente l'accesso alle entità JPA e [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] si limita a ereditare i metodi dell'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] si limita a ereditare i metodi dell'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> {
// elenco delle fasce orarie di un medico
@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] consente di ottenere le fasce orarie di un medico;
- riga 11: il parametro è l’identificativo del medico. Il risultato è un elenco di fasce orarie sotto forma di oggetto [Iterable<Creneau>];
- riga 10: l'annotazione [@Query] consente di 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 dall'interfaccia [CrudRepository];
- righe 12-13: il metodo [getRvMedecinJour] consente di ottenere gli appuntamenti di un medico per un determinato giorno;
- riga 13: i parametri sono l’identificativo 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] verrà sostituito dal parametro [idMedecin] del metodo e il parametro [?2] verrà sostituito dal parametro [jour] del metodo. Non è sufficiente la seguente query JPQL:
poiché i campi della classe Rv, di tipo [Client] e [Creneau], vengono recuperati in modalità [FetchType.LAZY], il che significa che devono essere richiesti esplicitamente per essere ottenuti. Ciò avviene nella query JPQL con la sintassi [left join fetch entité], che richiede l’esecuzione di un join con la tabella a cui punta la chiave esterna al fine di recuperare l’entità indicata;
8.4.6. Il livello [métier]
![]() |
![]() |
- [IMetier] è l'interfaccia del livello [métier] e [Metier] ne costituisce l'implementazione;
- [AgendaMedecinJour] e [CreneauMedecinJour] sono due entità di business;
8.4.6.1. Le entità
L'entità [CreneauMedecinJour] associa una fascia oraria e l'eventuale appuntamento fissato in 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;
// campi
private Creneau creneau;
private Rv rv;
// costruttori
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);
}
// getter e setter
...
}
- riga 12: la fascia oraria;
- riga 13: l'eventuale appuntamento – 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;
// campi
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// costruttori
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());
}
// getter e setter
...
}
- riga 13: il medico;
- riga 14: il giorno nell'agenda;
- linea 15: gli orari di apertura con o senza appuntamento;
8.4.6.2. Il servizio
L'interfaccia del livello [métier] è 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 {
// elenco dei clienti
public List<Client> getAllClients();
// elenco dei medici
public List<Medecin> getAllMedecins();
// elenco delle fasce orarie di un medico
public List<Creneau> getAllCreneaux(long idMedecin);
// elenco degli appuntamenti di un medico in un determinato giorno
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// trovare un cliente identificato dal suo ID
public Client getClientById(long id);
// trovare un cliente identificato dal suo ID
public Medecin getMedecinById(long id);
// trovare un appuntamento identificato dal proprio ID
public Rv getRvById(long id);
// trovare una fascia oraria identificata dal proprio ID
public Creneau getCreneauById(long id);
// aggiungere un RV
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// eliminare un RV
public void supprimerRv(Rv rv);
// professione
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
I commenti spiegano la funzione di ciascuno dei metodi.
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 {
// repository
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// implementazione dell'interfaccia
@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 a un componente. Questo è denominato [métier];
- riga 25: la classe [Metier] implementa l'interfaccia [IMetier];
- riga 28: l'annotazione [@Autowired] è un'annotazione Spring. Il valore del campo così annotato verrà inizializzato (iniettato) da Spring con il riferimento a un componente Spring del tipo o del nome specificati. In questo caso l’annotazione [@Autowired] non specifica alcun nome. Verrà quindi effettuata un’iniezione per tipo;
- riga 29: il campo [medecinRepository] verrà inizializzato con il riferimento a un componente Spring di tipo [MedecinRepository]. Si tratterà del 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 esaminate;
- righe 39-41: implementazione del metodo [getAllClients];
- riga 40: utilizziamo il metodo [findAll] dell’interfaccia [ClientRepository]. Questo metodo restituisce un tipo [Iterable<Client>] che trasformiamo in [List<Client>] con il metodo statico [Lists.newArrayList]. La classe [Lists] è definita nella libreria Google Guava. In [pom.xml] è stata importata questa dipendenza:
<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 con l'ausilio delle classi del livello [DAO];
Solo il metodo alla riga 88 è specifico del livello [métier]. È stato inserito qui perché esegue un'elaborazione di business che non si limita a un semplice accesso ai dati. Senza questo metodo, non ci sarebbe stato alcun motivo per creare un livello [métier]. Il metodo [getAgendaMedecinJour] è il seguente:
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
// elenco delle fasce orarie del medico
List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
// elenco delle prenotazioni dello stesso medico per lo stesso giorno
List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
// si crea un dizionario a partire dagli appuntamenti fissati
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// si crea l'agenda per il giorno richiesto
AgendaMedecinJour agenda = new AgendaMedecinJour();
// il medico
agenda.setMedecin(getMedecinById(idMedecin));
// il giorno
agenda.setJour(jour);
// le fasce orarie di prenotazione
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// compilazione delle fasce orarie di prenotazione
for (int i = 0; i < creneauxHoraires.size(); i++) {
// riga i dell'agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// fascia oraria
Creneau créneau = creneauxHoraires.get(i);
long idCreneau = créneau.getId();
creneauxMedecinJour[i].setCreneau(créneau);
// la fascia oraria è libera o prenotata?
if (hReservations.containsKey(idCreneau)) {
// la fascia oraria è occupata - si registra la prenotazione
Rv resa = hReservations.get(idCreneau);
creneauxMedecinJour[i].setRv(resa);
}
}
// si restituisce il risultato
return agenda;
}
Si invita il lettore a leggere i commenti. L’algoritmo è il seguente:
- si recuperano tutte le fasce orarie del medico indicato;
- si recuperano tutti i suoi appuntamenti per il giorno indicato;
- con queste due informazioni, si è in grado di stabilire se una fascia oraria è libera o occupata;
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 {
// pacchetti delle entità JPA
public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
// la fonte dei dati MySQL
@Bean
public DataSource dataSource() {
// fonte dati TomcatJdbc
DataSource dataSource = new DataSource();
// configurazione JDBC
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
// connessioni inizialmente aperte
dataSource.setInitialSize(5);
// risultato
return dataSource;
}
// il provider JPA è 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();
}
// Gestore delle transazioni
@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 in cui si trovano le interfacce [CrudRepository] di Spring Data. Queste saranno aggiunte al contesto Spring;
- riga 19: aggiunge al contesto Spring tutte le classi del pacchetto [rdvmedecins] e dei suoi sottoclassi che presentano un'annotazione Spring. Nel pacchetto [rdvmdecins.metier], la classe [Metier] con la sua annotazione [@Service] verrà individuata e aggiunta al contesto Spring;
- righe 26-39: configurano il pool di connessioni Tomcat JDBC (riga 5);
- riga 36: il pool di connessioni avrà di default 5 connessioni aperte. Questa riga è riportata a titolo di esempio. Nel nostro caso, sarebbe sufficiente una sola connessione. Nel caso in cui il livello [DAO] venisse utilizzato da più thread, questa riga sarebbe necessaria. Ciò avverrà in seguito, quando il livello [DAO] fungerà da supporto 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 SGBD utilizzato è MySQL;
- righe 53-61: definiscono l'EntityManagerFactory del livello JPA. Da questo oggetto si ottiene l’oggetto [EntityManager] che consente di eseguire le operazioni JPA;
- riga 57: si indicano i pacchetti in cui si trovano le entità JPA;
- riga 58: indica la fonte 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 dell’ingresso nel metodo e viene terminata (con un commit o un rollback) dopo l’uscita dallo stesso;
8.4.8. I test del livello [métier]
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(){
// Visualizzazione clienti
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
// Visualizzazione medici
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
// Visualizzazione delle fasce orarie di un medico
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);
// Elenco degli appuntamenti di un medico in un determinato giorno
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));
// aggiungere un RV
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// verifica
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));
// aggiungere un RV nella stessa fascia oraria dello stesso giorno
// dovrebbe generare un'eccezione
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();
}
// si registra l'errore
erreur = true;
}
// si verifica che si sia verificato un errore
Assert.assertTrue(erreur);
// elenco dei RV
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// visualizzazione calendario
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// eliminare un RV
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// verifica
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));
}
// metodo di utilità - visualizza gli elementi di una collezione
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] esaminato 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 potrà 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 nella classe di test di un riferimento al livello [métier];
- molti test sono semplici test visivi:
- righe 32-33: elenco dei clienti;
- righe 35-36: elenco dei medici;
- righe 39-40: elenco degli slot di un medico;
- riga 43: elenco degli appuntamenti di un medico;
- riga 50: aggiunta di un nuovo appuntamento. Il metodo [ajouterRv] restituisce l'appuntamento con un'informazione aggiuntiva, ovvero la sua chiave primaria id;
- riga 53: si utilizza questa chiave primaria per cercare l'appuntamento nel database;
- riga 54: si verifica che l'appuntamento cercato e quello trovato siano lo stesso. Si ricorda che il metodo [equals] dell’entità [Rv] è stato ridefinito: due appuntamenti sono uguali se hanno lo stesso ID. In questo caso, ciò dimostra che l’appuntamento aggiunto è stato effettivamente inserito nel database;
- righe 61-73: si tenta di aggiungere una seconda volta lo stesso appuntamento. Ciò deve essere rifiutato dal SGBD poiché 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 indica che la combinazione [JOUR, ID_CRENEAU] deve essere unica, il che impedisce di inserire due appuntamenti nello stesso giorno e nella stessa fascia oraria.
- riga 73: si verifica che si sia effettivamente verificata un'eccezione;
- riga 77: si richiede l’agenda del medico per il quale è stato appena aggiunto un appuntamento;
- riga 79: si verifica che l'appuntamento aggiunto sia effettivamente presente nel suo calendario;
- riga 82: si elimina l’appuntamento aggiunto;
- riga 84: si recupera dal database l’appuntamento eliminato;
- riga 85: si verifica di aver recuperato un puntatore null, a dimostrazione che l'appuntamento cercato non esiste;
L'esecuzione del test ha esito positivo:
![]() |
8.4.9. Il programma da console
![]() |
Il programma da console è semplice. Illustra 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 {
// avvio
public static void main(String[] args) {
// si prepara la configurazione
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// si avvia
ConfigurableApplicationContext context = app.run(args);
// attività
IMetier métier = context.getBean(IMetier.class);
try {
// aggiungere un RV
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// verifica
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());
}
// chiusura del contesto Spring
context.close();
}
// metodo di utilità - visualizza gli elementi di una collezione
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: eliminazione 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: si recupera un riferimento al bean che implementa l'interfaccia [IMetier]. Si tratta quindi di un riferimento al livello [métier];
- righe 27-31: aggiunta di un nuovo appuntamento per oggi, per il cliente n. 1 nella fascia oraria n. 1. Il cliente e la fascia oraria sono stati creati ad hoc per dimostrare che vengono utilizzati solo gli identificativi. Qui è stata inizializzata la versione, ma si sarebbe potuto inserire qualsiasi valore. In questo caso non viene utilizzata;
- riga 34: si vuole conoscere il medico associato alla fascia n. 1. A tal fine è necessario recuperare dalla banca dati la fascia n. 1. Poiché ci si trova in modalità [FetchType.LAZY], il medico non viene restituito insieme alla fascia. Tuttavia, abbiamo provveduto a prevedere un campo [idMedecin] nell’entità [Creneau] per recuperare la chiave primaria del medico;
- riga 35: si recupera la chiave primaria del medico;
- riga 36: viene visualizzato l'elenco degli appuntamenti del medico;
I risultati della console sono i seguenti:
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. È possibile definire numerosi parametri per modificare 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 log di Hibernate - in questo caso nessun log
- 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">
<!-- agli encoder viene assegnato per impostazione predefinita il tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- controllo del livello dei log -->
<root level="info"> <!-- disattivato, info, debug, avviso -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- il livello generale dei log è controllato dalla riga 9 - qui i log sono di livello [info];
Ciò produce il seguente risultato:
Se si imposta il livello di log di Hibernate su [info] (senza modificare nient’altro):
logging.level.org.hibernate=INFO
spring.main.show-banner=false
si ottiene il seguente risultato:
Se si imposta il livello di log su [debug] (senza modificare nient'altro):
logging.level.org.hibernate=DEBUG
spring.main.show-banner=false
si ottiene 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]
![]() |
![]() |
Costruiremo il livello [web / jSON] in diverse fasi:
- fase 1: un livello web operativo senza autenticazione;
- fase 2: implementazione dell’autenticazione con Spring Security;
- fase 3: implementazione di CORS e [Cross-Origin Resource Sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]. Il client del nostro servizio web sarà un client web Angular che non apparterrà necessariamente allo stesso dominio del nostro servizio web. Per impostazione predefinita, non potrà quindi accedervi a meno che il servizio web non glielo consenta. Vedremo come;
8.4.11.1. 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" 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>
<!-- livello web Spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- livello di test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- livello 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 padre;
- righe 19-22: le dipendenze per un progetto Spring MVC;
- righe 24-28: le dipendenze per i test JUnit / Spring;
- righe 30-34: dipendenze relative al progetto dei livelli [métier, DAO, JPA];
8.4.11.2. L'interfaccia del servizio web
![]() |
- in [1], sopra indicato, il browser può richiedere solo un numero limitato di URL con una sintassi precisa;
- in [4], riceve una risposta jSON;
Le risposte del nostro servizio web avranno tutte la stessa forma, corrispondente alla trasformazione jSON di un oggetto di tipo [Response] come segue:
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- proprietà
// stato dell'operazione
private int status;
// eventuali messaggi di errore
private List<String> messages;
// il corpo della risposta
private T body;
// costruttori
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getter e setter
...
}
- riga 7: codice di errore della risposta 0: OK, altro: KO;
- riga 11: un elenco di messaggi di errore, se presenti;
- riga 13: il corpo della risposta;
Di seguito presentiamo 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 delle fasce orarie di un medico [/getAllCreneaux/{idMedecin}]
![]() |
Elenco degli appuntamenti di un medico [/getRvMedecinJour/{idMedecin}/{aaaa-mm-jj}
![]() |
Agenda di un medico [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]
![]() |
Per aggiungere o eliminare un appuntamento utilizziamo l'estensione Chrome [Advanced Rest Client], poiché queste operazioni vengono eseguite tramite POST.
Aggiungere un appuntamento [/ajouterRv]
![]() |
- in [0], l'URL del servizio web;
- in [1], viene utilizzato il metodo POST;
- in [2], il testo jSON delle informazioni trasmesse al servizio web nella forma {giorno, idClient, idCreneau};
- in [3], il cliente specifica al servizio web che gli invia informazioni nel formato jSON;
La risposta è quindi la seguente:
![]() |
- in [4]: il client invia l'intestazione che indica che i dati che sta inviando sono in formato jSON;
- in [5]: il servizio web risponde che anche lui invia jSON;
- in [6]: la risposta jSON del servizio web. Il campo [body] contiene la forma jSON dell'appuntamento aggiunto;
È possibile verificare la presenza del nuovo appuntamento:
![]() |
Si noti l’ID [50] dell’appuntamento. Procederemo alla sua eliminazione.
Eliminare un appuntamento [/supprimerRv]
![]() |
- in [1], l'URL del servizio web;
- in [2], viene utilizzato il metodo POST;
- in [3], il testo jSON delle informazioni trasmesse al servizio web nella forma {idRv};
- in [4], il cliente specifica al servizio web che gli invia le informazioni jSON;
La risposta è quindi la seguente:
![]() |
- in [5]: il campo [status] è pari a 0, a indicare che l’operazione è andata a buon fine;
È possibile verificare l’eliminazione dell’appuntamento:
![]() |
Come si vede sopra, l'appuntamento del paziente [Mme GERMAIN] non è più presente.
Il servizio web consente inoltre di recuperare le entità tramite il loro identificativo:
![]() |
![]() |
![]() |
![]() |
Tutte queste entità URL vengono gestite dal controller [RdvMedecinsController], che presenteremo prossimamente.
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: si richiede che i componenti Spring vengano ricercati nel pacchetto [rdvmedecins.web] e nei suoi sottocomponenti. In questo modo verranno individuati i componenti:
- [@RestController RdvMedecinsController] nel pacchetto [rdvmedecins.web.controllers];
- [@Component ApplicationModel] nel pacchetto [rdvmedecins.web.models];
- riga 11: si importa la classe [DomainAndPersistenceConfig] che configura il progetto [rdvmedecins-metier-dao] per poter accedere ai bean di tale 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 {
// Configurazione del DispatcherServlet per le intestazioni CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mappatori 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. Svolge la funzione di [FrontController]: intercetta le richieste indirizzate al sito Spring MVC e le fa elaborare da uno dei controller (Controller) del sito;
- riga 22: istanziazione della classe;
- riga 23: questa riga può essere ignorata per il momento;
- righe 27-30: il servlet [dispatcherServlet] gestisce tutte le richieste URL;
- righe 27-30: attivano il server Tomcat integrato nelle dipendenze del progetto. Funzionerà sulla porta 8080;
- righe 38-67: quattro mappatori jSON configurati con filtri jSON diversi;
- 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] ci servirà per 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 {
// il livello [métier]
@Autowired
private IMetier métier;
// dati provenienti dal livello [métier]
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// dati di configurazione
private boolean CORSneeded = false;
private boolean secured = false;
@PostConstruct
public void init() {
// si recuperano i medici e i clienti
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// getter
public List<String> getMessages() {
return messages;
}
// ------------------------- interfaccia del livello [métier]
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return médecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return métier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return métier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return métier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return métier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return métier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return métier.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return métier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(long idRv) {
métier.supprimerRv(idRv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
// getter e setter
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 [métier];
- riga 34: l'annotazione [@PostConstruct] fa sì che il metodo [init] venga eseguito subito dopo l'istanziazione della classe [ApplicationModel];
- righe 38-39: si recuperano gli elenchi dei medici e dei clienti dal livello [métier];
- riga 41: se si verifica un'eccezione, i messaggi dello stack delle eccezioni vengono memorizzati nel campo della riga 17;
L'architettura del livello web si evolve come segue:
![]() |
- in [2b], i metodi del o dei controller comunicano con il singleton [ApplicationModel];
Questa strategia garantisce flessibilità nella gestione della cache. Attualmente gli orari dei medici non vengono memorizzati nella cache. Per inserirli, è sufficiente modificare la classe [ApplicationModel]. Ciò non ha alcun impatto sul controller, che continuerà a utilizzare il metodo [List<Creneau> getAllCreneaux(long idMedecin)] come faceva in precedenza. Sarà modificata l’implementazione di questo metodo nella classe [ApplicationModel].
8.4.11.5. La classe Static
La classe [Static] raggruppa una serie di metodi statici di utilità che non hanno alcuna attinenza con il «business» o il «web»:
![]() |
Il suo codice è il seguente:
package rdvmedecins.web.helpers;
import java.util.ArrayList;
import java.util.List;
public class Static {
public Static() {
}
// elenco dei messaggi di errore di un'eccezione
public static List<String> getErreursForException(Exception exception) {
// si recupera l'elenco dei messaggi di errore dell'eccezione
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.getErreursForException] che è stato utilizzato (riga 8 qui sotto) nel metodo [init] della classe [ApplicationModel]:
@PostConstruct
public void init() {
// si recuperano i medici e i clienti
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
Il metodo crea un oggetto [List<String>] con i messaggi di errore [exception.getMessage()] di un'eccezione [exception] e di quelle in essa contenute [exception.getCause()].
8.4.11.6. Lo scheletro del controller [RdvMedecinsController]
![]() |
Ora descriveremo in dettaglio l’elaborazione dei URL del servizio web. In questa elaborazione sono coinvolte tre classi principali:
- il controller [RdvMedecinsController];
- la classe di metodi di utilità [Static];
- la classe di 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;
// elenco dei messaggi
private List<String> messages;
// mappatori jSON
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
@PostConstruct
public void init() {
// messaggi di errore dell'applicazione
messages = application.getMessages();
}
// elenco dei medici
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {...}
// elenco dei clienti
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {...}
// elenco degli orari di visita di un medico
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
// elenco degli appuntamenti di un medico
@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, il C di MVC;
- righe 38-39: un oggetto di tipo [ApplicationModel] verrà iniettato qui da Spring. Lo abbiamo già presentato;
- righe 41-42: qui Spring inietterà un oggetto di tipo [RdvMedecinsCorsController]. Presenteremo questo oggetto solo in seguito;
- righe 48-58: i mappatori jSON definiti nella classe di configurazione [WebConfig];
- riga 60: l'annotazione [@PostConstruct] contrassegna un metodo da eseguire subito dopo l'istanziazione della classe. Quando questo viene eseguito, gli oggetti iniettati da Spring sono disponibili;
- riga 63: si recuperano 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, si ottiene [messages!=null]. Ciò consentirà ai metodi del controller di verificare se l’applicazione si è inizializzata correttamente;
- righe 67-118: i metodi URL esposti dal servizio [web / jSON]. Tutti i metodi restituiscono la stringa jSON di un oggetto di tipo [Response<T>] come segue:
![]() |
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- proprietà
// stato dell'operazione
private int status;
// eventuali messaggi di errore
private List<String> messages;
// il corpo della risposta
private T body;
// costruttori
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getter e setter
...
}
- 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 assume il valore null in caso di errore;
Questo oggetto viene serializzato in jSON prima di essere inviato al browser del cliente;
- riga 67: l'oggetto URL esposto è [/getAllMedecins]. Il client deve utilizzare un metodo [GET] per effettuare la richiesta (method = RequestMethod.GET). Se questo URL fosse richiesto da 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 di caratteri (riga 67). L’intestazione HTTP [Content-type : application/json; charset=UTF-8] verrà inviata al client per indicargli che riceverà una stringa jSON (riga 67);
- riga 77: il parametro URL è impostato da {idMedecin}. Questo parametro viene recuperato con l’annotazione [@PathVariable] alla riga 79;
- riga 79: il parametro [long idMedecin] riceve il proprio valore dal parametro {idMedecin} di URL [@PathVariable("idMedecin")]. Il parametro in URL e quello del metodo possono avere nomi diversi. Va notato che [@PathVariable("idMedecin")] è di tipo String (tutto il URL è una String), mentre il parametro [long idMedecin] è di tipo [long]. La conversione del tipo viene effettuata automaticamente. Se la conversione non va a buon fine, viene restituito un codice di errore HTTP;
- riga 105: l’annotazione [@RequestBody] indica il corpo della richiesta. In una richiesta GET, non c'è quasi mai un corpo (ma è possibile inserirne uno). In una richiesta POST, il più delle volte c'è (ma è possibile non inserirlo). Per quanto riguarda URL e [ajouterRv], il client web invia nel proprio POST la seguente stringa jSON:
La sintassi [@RequestBody PostAjouterRv post] (riga 105) , unita al fatto che il metodo si aspetta il jSON [consumes = "application/json; charset=UTF-8"] alla riga 103, farà sì che la stringa jSON inviata dal client web venga deserializzata in un oggetto di tipo [PostAjouterRv]. Quest’ultimo è il seguente:
package rdvmedecins.web.models;
public class PostAjouterRv {
// dati del post
private String jour;
private long idClient;
private long idCreneau;
// getter e setter
...
}
Anche in questo caso, le modifiche di tipo necessarie avverranno automaticamente;
- nelle righe 107-109 si trova un meccanismo simile per URL e [/supprimerRv]. La stringa jSON inviata è la seguente:
e il tipo [PostSupprimerRv] è il seguente:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// dati del post
private long idRv;
// getter e setter
...
}
8.4.11.7. L'URL [/getAllMedecins]
Il codice URL e [/getAllMedecins] viene elaborato dal metodo seguente del controller [RdvMedecinsController]:
// elenco dei medici
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {
// la risposta
Response<List<Medecin>> response;
// stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// elenco dei medici
try {
response = new Response<>(0, null, application.getAllMedecins());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
}
// risposta
return jsonMapper.writeValueAsString(response);
}
- righe 9-10: si verifica se l'applicazione si è inizializzata correttamente (messages==null). In caso contrario, viene restituita una risposta con status=-1 e body=messages;
- riga 13: in caso contrario, si richiede l'elenco dei medici alla classe [ApplicationModel];
- riga 19: si invia la stringa jSON della risposta con il mappatore jSON [jsonMapper] poiché la classe [Medecin]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 poiché si limita a restituire un elenco presente nella cache. Tuttavia, manterremo questa gestione delle eccezioni nel caso in cui i medici non fossero più memorizzati nella cache;
Non abbiamo ancora illustrato il caso in cui l’applicazione non si sia inizializzata correttamente. Interrompiamo i metodi SGBD e MySQL5, avviamo il servizio web e quindi richiediamo i metodi URL e [/getAllMedecins]:

Si ottiene effettivamente un errore. In condizioni normali, si ottiene la seguente visualizzazione:
![]() |
8.4.11.8. L'URL [/getAllClients]
L'URL [/getAllClients] viene elaborato dal seguente metodo del controller [RdvMedecinsController]:
// elenco dei clienti
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {
// la risposta
Response<List<Client>> response;
// stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// elenco dei clienti
try {
response = new Response<>(0, null, application.getAllClients());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
// risposta
return jsonMapper.writeValueAsString(response);
}
È analogo al metodo [getAllMedecins] già esaminato. I risultati ottenuti sono i seguenti:
![]() |
8.4.11.9. L'URL [/getAllCreneaux/{idMedecin}]
Il metodo URL [/getAllCreneaux/{idMedecin}] viene elaborato dal seguente metodo del controller [RdvMedecinsController]:
// elenco degli orari disponibili di un medico
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
// la risposta
Response<List<Creneau>> response;
// stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// si recupera il medico
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
Medecin médecin = responseMedecin.getBody();
// fasce orarie del medico
try {
response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// risposta
return jsonMapperShortCreneau.writeValueAsString(response);
}
- riga 12: il medico identificato dal parametro [id] viene richiesto a un metodo locale:
private Response<Medecin> getMedecin(long id) {
// si recupera il medico
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// medico già presente?
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);
}
Da questo metodo si ritorna con un status in [0,1,2]. Torniamo al codice del metodo [getAllCreneaux]:
- righe 13-14: se status!=0, si costruisce una risposta con errore;
- riga 16: si recupera il medico;
- riga 19: si recuperano gli slot di quel medico;
- riga 25: si invia come risposta un oggetto [List<Creneau>]. Ricordiamo la definizione della classe [Creneau]:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// caratteristiche di una fascia oraria di RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// una fascia oraria è collegata a un medico
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// chiave esterna
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- riga 13: si cerca il medico in modalità [FetchType.LAZY];
Ricordiamo la richiesta 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 il join tra le tabelle [CRENEAUX] e [MEDECINS]. Pertanto, la query restituisce tutte le fasce orarie del medico, indicando in ciascuna di esse il nome del medico. Quando si serializzano questi appuntamenti in jSON, in ciascuno di essi compare la stringa jSON relativa al medico. Ciò è superfluo. Per controllare la serializzazione, sono necessarie due cose:
- avere accesso all’oggetto che viene serializzato;
- configurare l’oggetto da serializzare;
Il punto 1 viene verificato con l'iniezione del convertitore jSON appropriato per l'oggetto nel controller:
@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. Essa crea un filtro denominato [creneauFilter]. Con l'aiuto di questo filtro, potremo definire a livello di programmazione i campi che devono essere serializzati e quelli che non devono esserlo;
La serializzazione dell’oggetto [Creneau] avviene nella riga seguente del metodo [getAllCreneaux]:
// risposta
return jsonMapperShortCreneau.writeValueAsString(response);
Il mappatore jSON [jsonMapperShortCreneau] è stato definito nella classe [WebConfig] nel modo seguente:
@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] è la stringa jSON di tipo [Response<List<Creneau>].
I risultati ottenuti sono i seguenti:
![]() |
oppure questi, se la fascia oraria non esiste:
![]() |
Da questo esempio ricaviamo la seguente regola:
- i metodi del server web / jSON restituiscono un oggetto di tipo [Response<T>] che viene serializzato in jSON;
- se il tipo T ha uno o più filtri jSON, per serializzarlo si utilizzerà un mappatore con questi stessi filtri;
8.4.11.10. L'URL [/getRvMedecinJour/{idMedecin}/{jour}]
L'URL [/getRvMedecinJour/{idMedecin}/{jour}] viene elaborato dal seguente metodo del controller [RdvMedecinsController]:
// elenco degli appuntamenti di un medico
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// la risposta
Response<List<Rv>> response=null;
boolean erreur = false;
// stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// si verifica la data
Date jourAgenda = null;
if (!erreur) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<List<Rv>>(3, messages, null);
erreur = true;
}
}
Response<Medecin> responseMedecin = null;
if (!erreur) {
// si recupera il medico
responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
Medecin médecin = responseMedecin.getBody();
// elenco dei suoi appuntamenti
try {
response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// risposta
return jsonMapperLongRv.writeValueAsString(response);
}
- è necessario convertire la stringa jSON in un tipo [Response<List<Rv>>]. La classe [Rv] presenta un campo [Rv.creneau]. Se tale campo viene serializzato, si incontrerà 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 sia 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;
// caratteristiche di un appuntamento
@Temporal(TemporalType.DATE)
private Date jour;
// un appuntamento è collegato a un cliente
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// un appuntamento è collegato a una fascia oraria
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// chiavi esterne
@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 cliente viene ricercato con la modalità [FetchType.LAZY];
- riga 18: la fascia oraria viene ricercata con 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")
Vengono effettuati esplicitamente dei join per recuperare i campi [client] e [creneau]. Inoltre, a causa del join [cr.medecin.id=?1], avremo anche il medico. Il medico apparirà quindi nella stringa jSON di ogni appuntamento. Tuttavia, questa informazione duplicata è inoltre superflua. Abbiamo visto come risolvere questo problema utilizzando un filtro jSON sull’oggetto [Creneau]. A causa delle modalità [FetchType.LAZY] dei campi [client] e [creneau] della classe [Rv], scopriremo presto la necessità di applicare un filtro jSON alla classe [RV] del progetto [rdvmedecins-metier-dao]:
@Entity
@Table(name = "rv")
@JsonFilter("rvFilter")
public class Rv extends AbstractEntity {
...
Verificheremo la serializzazione dell’oggetto [Rv] con il filtro [rvFilter]. A quanto pare, in questo caso non è necessario applicare alcun filtro poiché ci servono tutti i campi dell’oggetto di tipo [Rv]. Tuttavia, poiché abbiamo specificato che la classe dispone di un filtro jSON, dobbiamo definirlo per ogni serializzazione di un oggetto di tipo [Rv], altrimenti si verifica un'eccezione. A tal fine, utilizziamo il seguente mappatore jSON definito nella classe [rdvMedecinsController]:
@Autowired
private ObjectMapper jsonMapperLongRv;
Questo mappatore è 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: si specifica che tutti i campi dell'oggetto [Rv] devono essere serializzati;
- riga 5: si specifica 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 un giorno 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}] viene elaborato con il seguente metodo del 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 {
// la risposta
Response<AgendaMedecinJour> response = null;
boolean erreur = false;
// Stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// si verifica la data
Date jourAgenda = null;
if (!erreur) {
// si verifica la data
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
erreur = true;
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(3, messages, null);
}
}
// si recupera il medico
Medecin médecin = null;
if (!erreur) {
// si recupera il medico
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
médecin = responseMedecin.getBody();
}
}
// si recupera la sua agenda
if (!erreur) {
try {
response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// risposta
return jsonMapperLongRv.writeValueAsString(response);
}
- righe 6, 49: si restituisce la stringa jSON di tipo [AgendaMedecinJour] incapsulata in un oggetto [Response];
Il tipo [AgendaMedecinJour] è il seguente:
public class AgendaMedecinJour implements Serializable {
// campi
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;
// campi
private Creneau creneau;
private Rv rv;
I campi [creneau] e [rv] dispongono di filtri jSON che devono essere configurati. È ciò che fa la riga 49 del metodo [getAgendaMedecinJour], che utilizza il mappatore jSON [jsonMapperLongRv] già incontrato:
@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 è errata:
![]() |
oppure questi, se il numero del medico non è valido:
![]() |
8.4.11.12. L'URL [/getMedecinById/{id}]
L'URL [/getMedecinById/{id}] viene elaborato con il seguente metodo del controllore [RdvMedecinsController]:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
// la risposta
Response<Medecin> response;
// stato dell'applicazione
if (messages != null) {
response = new Response<Medecin>(-1, messages, null);
} else {
response = getMedecin(id);
}
// risposta
return jsonMapper.writeValueAsString(response);
}
- righe 5, 13: il metodo converte la stringa jSON in un tipo [Medecin]. Questo tipo non presenta alcuna annotazione di filtro jSON. Pertanto, alla riga 14, si utilizza il mappatore jSON senza filtri;
Alla riga 10, il metodo [getMedecin] è il seguente:
private Response<Medecin> getMedecin(long id) {
// si recupera il medico
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// medico esistente?
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 ottenuti sono i seguenti:
![]() |
oppure questi, se il numero del medico non è corretto:
![]() |
8.4.11.13. L'URL [/getClientById/{id}]
L'URL [/getClientById/{id}] viene elaborata con il seguente metodo del 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 {
// la risposta
Response<Client> response;
// stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
response = getClient(id);
}
// risposta
return jsonMapper.writeValueAsString(response);
}
- righe 5, 13: il metodo converte la stringa jSON in un tipo [Client]. Questo tipo non presenta annotazioni relative ai filtri jSON. Pertanto, alla riga 13, si utilizza il mappatore jSON senza filtri;
Alla riga 11, il metodo [getClient] è il seguente:
private Response<Client> getClient(long id) {
// si recupera il cliente
Client client = null;
try {
client = application.getClientById(id);
} catch (RuntimeException e1) {
return new Response<Client>(1, Static.getErreursForException(e1), null);
}
// cliente esistente?
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 ottenuti sono i seguenti:
![]() |
oppure questi, se il numero del cliente non è corretto:
![]() |
8.4.11.14. L'URL [/getCreneauById/{id}]
L'URL [/getCreneauById/{id}] viene elaborata dal controller [RdvMedecinsController] con il seguente metodo:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
// la risposta
Response<Creneau> response;
// stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// si restituisce la fascia oraria
response = getCreneau(id);
}
// risposta
return jsonMapperShortCreneau.writeValueAsString(response);
}
- righe 5, 14: il metodo restituisce la stringa jSON di tipo [Response<Creneau>];
Riga 8, il metodo [getCreneau] è il seguente:
private Response<Creneau> getCreneau(long id) {
// si recupera la fascia oraria
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (RuntimeException e1) {
return new Response<Creneau>(1, Static.getErreursForException(e1), null);
}
// slot esistente?
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);
}
Ricordiamo il codice dell'entità [Creneau]:
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// caratteristiche di una fascia oraria di RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// una fascia oraria è associata a un medico
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// chiave esterna
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
- righe 14-16: poiché il campo [medecin] è in modalità [fetch = FetchType.LAZY], non viene recuperato quando si cerca una fascia oraria 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 ottenere il campo [medecin]. Tuttavia, con un’implementazione JPA / Hibernate, il metodo [fetch = FetchType.LAZY] del campo [medecin] ha restituito un oggetto [Creneau] il cui metodo [getMedecin] è programmato per recuperare il medico nel contesto JPA. Questo viene definito un oggetto [proxy]. Ricordiamo però l’architettura dell’applicazione web:
![]() |
Il controller si trova nel blocco [Contrôleurs / Actions]. Quando ci si trova in questo blocco, non esiste più il concetto di contesto JPA. Quest’ultimo viene creato durante le operazioni del livello [DAO]. Non sopravvive oltre. Pertanto, quando il controller tenta di accedere al contesto JPA, si verifica un'eccezione che indica che quest'ultimo è chiuso. Per evitare questa eccezione, è necessario impedire la serializzazione del campo [medecin] della classe [Rv]. È 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 della fascia oraria non è corretto:
![]() |
8.4.11.15. L'URL [/getRvById/{id}]
L'URL [/getRvById/{id}] viene elaborata dal controller [RdvMedecinsController] con il seguente metodo:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
// la risposta
Response<Rv> response;
// stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// si recupera l'appuntamento
response = getRv(id);
}
// risposta
return jsonMapperShortRv.writeValueAsString(response);
}
- righe 5, 14: il metodo restituisce la stringa jSON di tipo [Response<Rv>];
Riga 11, il metodo [getRv] è il seguente:
private Response<Rv> getRv(long id) {
// si recupera l'Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (RuntimeException e1) {
return new Response<Rv>(1, Static.getErreursForException(e1), null);
}
// Rv esistente?
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 con l'annotazione [fetch = FetchType.LAZY], ovvero i campi [creneau] e [client]. Questi campi non vengono quindi recuperati quando si cerca un [Rv] tramite la sua chiave primaria. È quindi necessario, per gli stessi motivi di prima, escluderli dalla serializzazione. È ciò che fa il 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 [/ajouterRv] viene elaborata dal metodo seguente del 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 {
// la risposta
Response<Rv> response = null;
boolean erreur = false;
// stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// si recuperano i valori inviati
String jour;
long idCreneau = -1;
long idClient = -1;
Date jourAgenda = null;
if (!erreur) {
// si recuperano i valori inviati
jour = post.getJour();
idCreneau = post.getIdCreneau();
idClient = post.getIdClient();
// si controlla la data
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(6, messages, null);
erreur = true;
}
}
// si recupera la fascia oraria
Response<Creneau> responseCréneau = null;
if (!erreur) {
// si recupera la fascia oraria
responseCréneau = getCreneau(idCreneau);
if (responseCréneau.getStatus() != 0) {
erreur = true;
response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
}
}
// si recupera il cliente
Response<Client> responseClient = null;
Creneau créneau = null;
if (!erreur) {
créneau = (Creneau) responseCréneau.getBody();
// si recupera il cliente
responseClient = getClient(idClient);
if (responseClient.getStatus() != 0) {
erreur = true;
response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
}
}
if (!erreur) {
Client client = responseClient.getBody();
// si aggiunge l'appuntamento
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);
}
}
// risposta
return jsonMapperLongRv.writeValueAsString(response);
}
- righe 5, 67: il metodo deve restituire la stringa jSON di tipo [Response<Rv>];
- riga 3: l'annotazione [@RequestBody PostAjouterRv post] recupera il corpo di POST e lo inserisce nel parametro [PostAjouterRv post]. Questo corpo proviene da jSON [consumes = "application/json; charset=UTF-8"] e verrà deserializzato automaticamente nel seguente tipo [PostAjouterRv]:
public class PostAjouterRv {
// dati del post
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: l’impostazione dei filtri jSON, [creneauFilter] e [rvFilter]. Il metodo converte la stringa jSON in un 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] da un oggetto [Medecin] ed è stato recuperato alle righe 36-44. È stato ricercato nel contesto JPA tramite la sua chiave primaria ed è stato recuperato senza la sua dipendenza [FetchType.LAZY]. Infine,
- l'oggetto [Rv] possiede tutte le sue dipendenze. Queste possono essere serializzate;
- l’oggetto [Creneau] non possiede la sua dipendenza [medecin]. È quindi necessario che quest’ultima non venga 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 simili a questi:
![]() |
- in [1], l’URL del POST;
- in [2], il POST;
- in [3], il valore pubblicato;
- in [4a], tale valore inserito proviene da jSON;
![]() |
- in [4b], il cliente 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. Vi si vede l'identificativo [id] dell'appuntamento aggiunto;
Si ottiene quanto segue con un numero di fascia oraria inesistente:
![]() |
8.4.11.17. L'URL [/supprimerRv]
L'URL [/supprimerRv] viene elaborato dal metodo seguente del 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 {
// la risposta
Response<Void> response = null;
boolean erreur = false;
// intestazioni CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// stato dell'applicazione
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// si recuperano i valori inviati
long idRv = post.getIdRv();
// si recupera il rv
if (!erreur) {
Response<Rv> responseRv = getRv(idRv);
if (responseRv.getStatus() != 0) {
response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
// eliminazione del rv
try {
application.supprimerRv(idRv);
response = new Response<Void>(0, null, null);
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// risposta
return jsonMapper.writeValueAsString(response);
}
- riga 5: il tipo [Void] è la classe corrispondente al tipo primitivo [void];
- righe 5, 34: il metodo restituisce la stringa jSON di tipo [Response<Void>], che non presenta filtri jSON. Pertanto, alla riga 34, si utilizza il mappatore jSON senza filtri;
- riga 3: il metodo ha come parametro il corpo del POST, ovvero il valore inviato. Questo viene ricevuto in formato jSON [consumes = "application/json; charset=UTF-8"] e deserializzato automaticamente nel seguente tipo [PostSupprimerRv]:
public class PostSupprimerRv {
// dati del post
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 numero di appuntamento inesistente, si ottiene quanto segue:
![]() |
Abbiamo terminato con il controller. Vediamo ora 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);
}
}
Alla riga 10, il metodo statico [SpringApplication.run] viene eseguito con come primo parametro la classe di configurazione del progetto [AppConfig]. Questo metodo provvederà all'autoconfigurazione del progetto, avvierà il server Tomcat integrato nelle dipendenze e vi distribuirà il controller [RdvMedecinsController].
I log sono gestiti dai seguenti file [2]:
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- agli encoder viene assegnato per impostazione predefinita il tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- controllo del livello dei log -->
<root level="info"> <!-- disattivato, info, debug, avviso -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- riga 9: il livello generale dei log è 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 consentono di impostare un livello di log specifico per alcuni elementi dell'applicazione:
- riga 1: si desiderano i log del livello [web];
- riga 2: non si desiderano i log del livello [JPA];
- riga 3: nessun banner 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: vengono individuati i URL esposti dal servizio web;
- riga 44: il server Tomcat è pronto e attende richieste sulla porta 8080;
Se si modifica il file [application.properties] nel modo seguente:
logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false
si ottengono i seguenti log:
Se inoltre si modifica il file [logback.xml] come segue:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- agli encoder viene assegnato per impostazione predefinita il tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- controllo del livello dei log -->
<root level="off"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
si ottengono i seguenti log:
Si nota quindi che è possibile esercitare un certo controllo sui log che compaiono nella console. Il livello [info] è spesso il livello di log corretto.
Ora disponiamo di un servizio web operativo consultabile tramite un client web. Passiamo ora alla messa in sicurezza di questo servizio: vogliamo che solo determinate persone possano gestire gli appuntamenti dei medici. A tal fine utilizzeremo il framework Spring Security, un ramo 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 è composto dai seguenti elementi:
- nella cartella [templates] si trovano le pagine HTML del progetto;
- [Application]: è la classe eseguibile del progetto;
- [MvcConfig]: è la classe di configurazione di 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 conoscere 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. Le 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. Si tratta del nome dell'utente connesso;
- 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;
L'ultima vista [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}"] fa sì 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}"] fa sì che il tag <div> venga generato solo se l'URL che visualizza la pagina di login contiene il parametro [logout] (http://context/login?logout);
- righe 11-23: un modulo HTML;
- riga 11: il modulo verrà inviato a 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 che alla riga 28 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 ridefinirne alcuni metodi;
- riga 10: ridefinizione di un metodo della classe padre;
- righe 11-16: il metodo [addViewControllers] consente di associare URL a viste HTML. Vengono effettuate le seguenti associazioni:
URL | vista |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
Il suffisso [html] e la cartella [templates] sono i valori predefiniti utilizzati da Thymeleaf. Possono essere modificati tramite configurazione. La cartella [templates] deve trovarsi nella radice del Classpath del progetto:
![]() |
Sopra [1], le cartelle [java] e [resources] sono entrambe cartelle sorgente (source folders). Ciò implica che il loro contenuto si troverà nella radice del Classpath del progetto. Pertanto, in [2], le cartelle [hello] e [templates] si troveranno alla 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] trasforma la classe [WebSecurityConfig] in una classe di configurazione di Spring Security;
- riga 11: la classe [WebSecurity] estende la classe [WebSecurityConfigurerAdapter] per ridefinirne alcuni metodi;
- riga 12: ridefinizione di un metodo della classe padre;
- righe 13-16: il metodo [configure(HttpSecurity http)] viene ridefinito per definire i diritti di accesso alle diverse classi URL dell’applicazione;
- riga 14: il metodo [http.authorizeRequests()] consente di associare i URL ai diritti di accesso. Vengono effettuate le seguenti associazioni:
URL | regola | codice |
accesso senza autenticazione | | |
accesso solo con autenticazione |
- riga 15: definisce il metodo di autenticazione. L'autenticazione avviene tramite un modulo accessibile a tutti: URL [/login] [http.formLogin().loginPage("/login").permitAll()]. Anche la disconnessione (logout) è accessibile a tutti;
- righe 19-21: ridefiniscono il metodo [configure(AuthenticationManagerBuilder auth)] che gestisce gli utenti;
- riga 20: l’autenticazione avviene tramite utenti predefiniti [auth.inMemoryAuthentication()]. Un utente è qui definito con il login [user], la password [password] e il ruolo [USER]. È possibile assegnare gli stessi diritti agli utenti che hanno lo stesso ruolo;
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] richiede a Spring Boot (riga 3) di effettuare la configurazione che lo sviluppatore non avrà effettuato esplicitamente;
- riga 9: rende la classe [Application] una classe di configurazione Spring;
- riga 10: richiede la scansione della cartella della classe [Application] per individuare i componenti Spring. Le due classi [MvcConfig] e [WebSecurityConfig] verranno così individuate poiché presentano l'annotazione [@Configuration];
- riga 13: il metodo [main] della classe eseguibile;
- riga 14: il metodo statico [SpringApplication.run] viene eseguito con come parametro la classe di configurazione [Application]. Abbiamo già incontrato questo processo e sappiamo che il server Tomcat incluso nelle dipendenze Maven del progetto verrà avviato e il progetto verrà distribuito su di esso. Abbiamo visto che quattro URL erano gestiti da [/, /home, /login, /hello] e che alcuni erano protetti da diritti di accesso.
8.4.12.6. Test dell’applicazione
Iniziamo richiedendo il URL [/], che è uno dei quattro URL accettati. È associato alla vista [/templates/home.html]:
![]() |
La URL richiesta, [/], è accessibile a tutti. Ecco perché l’abbiamo ottenuta. Il link [here] è il seguente:
Il file URL [/hello] verrà richiesto quando si cliccherà sul link. Questo è protetto:
URL | regola | codice |
accesso senza autenticazione | | |
accesso riservato agli utenti autenticati |
È necessario effettuare l'autenticazione per ottenerlo. Spring Security reindirizzerà quindi il browser del cliente alla pagina di autenticazione. In base alla configurazione visualizzata, si tratta della pagina URL [/login]. Questa è accessibile a tutti:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Otteniamo quindi [1]:
![]() |
Il codice sorgente della pagina ottenuta è il seguente:
- alla riga 7 compare un campo nascosto che non è presente nella pagina originale [login.html]. È stato aggiunto da Thymeleaf. Questo codice, denominato CSRF (Cross Site Request Forgery), ha lo scopo di eliminare una vulnerabilità di sicurezza. Questo token deve essere rinviato a Spring Security insieme all’autenticazione affinché quest’ultima venga accettata;
Ricordiamo che Spring Security riconosce solo l’utente user/password. Se inseriamo qualcos’altro in [2], otteniamo la stessa pagina con un messaggio di errore in [3]. Spring Security ha reindirizzato il browser verso 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 user/password [4]:
![]() |
- in [4], effettuiamo l’autenticazione;
- in [5], Spring Security ci reindirizza verso URL [/hello] poiché è URL che avevamo richiesto quando siamo stati reindirizzati alla pagina di login. L’identità dell’utente è stata visualizzata dalla seguente riga di [hello.html]:
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 [Sign Out], verrà eseguito un POST su URL [/logout]. Quest’ultimo, così come il URL e il [/login], è accessibile a tutti:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Nella nostra associazione URL / viste, non abbiamo definito nulla per URL e [/logout]. Cosa succederà? Proviamo:
![]() |
- in [6], clicchiamo sul pulsante [Sign Out];
- da [7], vediamo che siamo stati reindirizzati a URL [http://localhost:8080/login?logout]. È stato Spring Security a richiedere questo reindirizzamento. La presenza del parametro [logout] in URL ha fatto visualizzare la seguente riga nella vista:
<div th:if="${param.logout}">You have been logged out.</div>
8.4.12.7. Conclusion
Nell'esempio precedente, avremmo potuto scrivere prima l'applicazione web e poi renderla sicura. Spring Security non è invasivo. È possibile implementare la sicurezza di un'applicazione web già scritta. Inoltre, abbiamo scoperto quanto segue:
- è 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 l’aggiunta di un parametro «error» nel token URL;
- se l’autenticazione va a buon fine, si viene reindirizzati alla pagina richiesta al momento dell’autenticazione. Se si richiede direttamente la pagina di autenticazione senza passare da una pagina intermedia, Spring Security reindirizza alla pagina URL [/] (questo caso non è stato presentato);
- ci si disconnette richiedendo la pagina URL [/logout] con un POST. Spring Security ci reindirizza quindi alla pagina di autenticazione con il parametro logout nel URL;
Tutte queste conclusioni si basano sui comportamenti predefiniti di Spring Security. Tali comportamenti possono essere modificati tramite configurazione, ridefinendo alcuni metodi della classe [WebSecurityConfigurerAdapter].
Il tutorial precedente ci sarà di scarso aiuto nel prosieguo. Utilizzeremo infatti:
- un database per memorizzare gli utenti, le loro password e i loro ruoli;
- un'autenticazione tramite header HTTP;
Esistono pochi tutorial relativi a ciò che vogliamo realizzare in questa sede. La soluzione che verrà proposta è un insieme di codici trovati qua e là.
8.4.13. Implementazione della sicurezza sul servizio web di prenotazione appuntamenti
8.4.13.1. Il database
Il database [rdvmedecins] viene aggiornato per includere gli utenti, le loro password e i loro ruoli. Vengono aggiunte tre nuove tabelle:

Tabella [USERS]: gli utenti
- ID: chiave primaria;
- VERSION: colonna di versioning della riga;
- IDENTITY: un'identità descrittiva dell'utente;
- LOGIN: il nome utente;
- PASSWORD: la sua password;
Nella tabella USERS, le password non sono memorizzate in chiaro:
![]() |
L'algoritmo che crittografa le password è l'algoritmo BCRYPT.
Tabella [ROLES]: i ruoli
- ID: chiave primaria;
- VERSION: colonna di versioning della riga;
- NAME: nome del ruolo. Per impostazione predefinita, Spring Security si aspetta nomi del tipo ROLE_XX, ad esempio ROLE_ADMIN o ROLE_GUEST;
![]() |
Tabella [USERS_ROLES]: tabella di join USERS / ROLES
Un utente può avere più ruoli, un ruolo può raggruppare più utenti. Si ha una relazione molti-a-molti rappresentata dalla tabella [USERS_ROLES].
- ID: chiave primaria;
- VERSION: colonna di versioning della riga;
- USER_ID: identificativo di un utente;
- ROLE_ID: identificativo di un ruolo;
![]() |
Poiché stiamo modificando il database, è necessario modificare tutti i livelli del progetto [métier, DAO, JPA]:
![]() |
8.4.13.2. Il nuovo progetto STS derivato da [métier, DAO, JPA]
Il progetto [rdvmedecins-metier-dao] si evolve come segue:
![]() |
- in [1]: il nuovo progetto;
- in [2]: le modifiche apportate per motivi di 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 in un unico pacchetto.
8.4.13.3. Le nuove entità [JPA]
![]() |
Il livello JPA definisce tre nuove entità:
![]() |
La classe [User] è l'immagine della 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;
// caratteristiche
private String identity;
private String login;
private String password;
// costruttore
public User() {
}
public User(String identity, String login, String password) {
this.identity = identity;
this.login = login;
this.password = password;
}
// identità
@Override
public String toString() {
return String.format("User[%s,%s,%s]", identity, login, password);
}
// getter e setter
....
}
- riga 9: la classe estende la classe [AbstractEntity] già utilizzata per le altre entità;
- righe 13-15: non viene specificato alcun nome per le colonne poiché hanno lo stesso nome dei campi ad esse associati;
La classe [Role] è il riflesso della 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;
// proprietà
private String name;
// costruttori
public Role() {
}
public Role(String name) {
this.name = name;
}
// identità
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// getter e setter
...
}
La classe [UserRole] è l'immagine della 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;
// un UserRole fa riferimento a un User
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// un UserRole fa riferimento a un ruolo
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// getter e setter
...
}
- righe 15-17: definiscono la chiave esterna dalla tabella [USERS_ROLES] alla tabella [USERS];
- righe 19-21: definiscono la chiave esterna della tabella [USERS_ROLES] verso la tabella [ROLES];
8.4.13.4. Modifiche al livello [DAO]
![]() |
Il livello [DAO] viene arricchito con tre nuovi [Repository]:
![]() |
L'interfaccia [UserRepository] gestisce gli accessi 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> {
// elenco dei ruoli di un utente identificato dal proprio ID
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// elenco dei ruoli di un utente identificato tramite login e password
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// ricerca di un utente tramite il suo nome utente
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)] consente di ottenere tutti i ruoli di un utente identificato tramite il suo [id]
- righe 16-17: lo stesso, ma per un utente identificato tramite login e password;
- riga 20: per trovare un utente tramite il suo nome utente;
L'interfaccia [RoleRepository] gestisce gli accessi alle entità [Role]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// ricerca di un ruolo tramite il suo nome
Role findRoleByName(String name);
}
- riga 5: l'interfaccia [RoleRepository] estende l'interfaccia [CrudRepository];
- riga 8: è possibile cercare un ruolo tramite il suo nome;
L'interfaccia [userRoleRepository] gestisce gli accessi alle entità [UserRole]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- riga 5: l'interfaccia [UserRoleRepository] si limita a estendere l'interfaccia [CrudRepository] senza aggiungere nuovi metodi;
8.4.13.5. Le classi di gestione degli utenti e dei ruoli
![]() |
Spring Security richiede la creazione di una classe che implementi la seguente interfaccia [UsersDetail]:
![]() |
Tale interfaccia è qui implementata 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;
// proprietà
private User user;
private UserRepository userRepository;
// costruttori
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interfaccia
@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;
}
// getter e setter
...
}
- riga 10: la classe [AppUserDetails] implementa l'interfaccia [UserDetails];
- righe 15-16: la classe incapsula un utente (riga 15) e il repository che consente di ottenere i dettagli di tale 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 derivati. Qui utilizziamo il tipo derivato [SimpleGrantedAuthority] (riga 32) che incapsula il nome di uno dei ruoli dell’utente della riga 15;
- righe 31-33: si percorre l’elenco dei ruoli dell’utente della riga 15 per costruire un elenco di elementi di tipo [SimpleGrantedAuthority];
- righe 38-40: implementano il metodo [getPassword] dell’interfaccia [UserDetails]. Viene restituita la password dell’utente della riga 15;
- righe 38-40: implementano il metodo [getUserName] dell'interfaccia [UserDetails]. Viene restituito il nome 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 {
// ricerca dell'utente tramite login
User user = userRepository.findUserByLogin(login);
// Trovato?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// vengono restituiti i dettagli dell'utente
return new AppUserDetails(user, userRepository);
}
}
- riga 9: la classe sarà un componente Spring, quindi disponibile nel proprio 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 nome utente;
- riga 18: si cerca l'utente in base al suo nome utente;
- righe 20-22: se non viene trovato, viene generata un'eccezione;
- riga 24: viene creato e visualizzato un oggetto [AppUserDetails]. È effettivamente 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) {
// sintassi: login password roleName
// sono necessari tre parametri
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// si recuperano i parametri
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// contesto Spring
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// il ruolo esiste già?
Role role = roleRepository.findRoleByName(roleName);
// se non esiste, lo si crea
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// L'utente esiste già?
User user = userRepository.findUserByLogin(login);
// se non esiste, lo creiamo
if (user == null) {
// si esegue l'hash della password con bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// si salva l'utente
user = userRepository.save(new User(login, login, crypt));
// si crea il collegamento con il ruolo
userRoleRepository.save(new UserRole(user, role));
} else {
// l'utente esiste già: possiede il ruolo richiesto?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// se non viene trovato, si crea la relazione con il ruolo
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// chiusura del contesto Spring
context.close();
}
}
- riga 17: la classe richiede tre argomenti che definiscono un utente: il suo login, la sua password e il suo ruolo;
- righe 25-27: i tre parametri vengono recuperati;
- riga 29: il contesto Spring viene costruito a partire dalla classe di configurazione [DomainAndPersistenceConfig]. Questa classe era già presente nel progetto iniziale. Deve essere modificata come segue:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- riga 1: occorre indicare che ora nel pacchetto [rdvmedecins.security] sono presenti componenti [Repository];
- riga 4: occorre indicare che ora nel pacchetto [rdvmedecins.security] sono presenti entità JPA;
Torniamo al codice per la creazione di un utente:
- righe 30-32: si recuperano i riferimenti dei tre [Repository] che possono esserci utili per creare l'utente;
- riga 34: si verifica se il ruolo esiste già;
- righe 36-38: se non esiste, lo si crea nel database. Avrà un nome del tipo [ROLE_XX];
- riga 40: si verifica se il login esiste già;
- righe 42-49: se il nome utente non esiste, lo creiamo nel database;
- riga 44: si crittografa la password. Qui si utilizza la classe [BCrypt] di Spring Security (riga 4). Sono quindi necessari i file di 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: nel caso in cui l’account esista già, si verifica se tra i suoi ruoli è già presente quello che si desidera assegnargli;
- righe 59-61: se il ruolo cercato non è stato trovato, si crea una riga nella tabella [USERS_ROLES] per collegare l'utente al suo ruolo;
- non ci si è protetti da eventuali eccezioni. Si tratta di una classe di supporto per creare rapidamente un utente con un ruolo.
Quando si esegue la classe con gli argomenti [x x guest], si ottengono in linea di massima i seguenti risultati:
Tabella [USERS]
![]() |
Tabella [ROLES]
![]() |
Tabella [USERS_ROLES]
![]() |
Consideriamo ora la seconda classe [UsersTest], che è un test di 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() {
// si recupera l'utente [admin]
User user = userRepository.findUserByLogin("admin");
// si verifica che la sua password sia [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// si verifica il ruolo di admin / admin
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// si recupera l'utente [admin]
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// si verifica che la sua password sia [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// si verifica il ruolo di admin / admin
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// metodo di utilità - visualizza gli elementi di una collezione
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- righe 27-34: test visivo. Vengono visualizzati tutti gli utenti con i rispettivi ruoli;
- righe 36-46: si verifica che l’utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando il repository [UserRepository];
- riga 41: [admin] è la password in chiaro. Nella base dati, è crittografata secondo l’algoritmo BCrypt. Il metodo [BCrypt.checkpw] consente di verificare che la password in chiaro, una volta crittografata, sia effettivamente uguale a quella presente nella base dati;
- righe 48-59: si verifica che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando il servizio [appUserDetailsService];
L'esecuzione dei test ha esito positivo con i seguenti log:
8.4.13.7. Conclusione intermedia
L'aggiunta delle classi necessarie a Spring Security è stata possibile con poche modifiche al progetto originale. Ricordiamole:
- aggiunta di una dipendenza da Spring Security nel file [pom.xml];
- creazione di tre tabelle aggiuntive nel database;
- creazione delle entità JPA e dei componenti Spring nel pacchetto [rdvmedecins.security];
Questo caso particolarmente favorevole deriva dal fatto che le tre tabelle aggiunte al database sono indipendenti da quelle esistenti. Si sarebbe potuto persino inserirle in un database separato. Ciò è stato possibile perché si è deciso che un utente avesse un’esistenza indipendente dai medici e dai clienti. Se questi ultimi fossero stati utenti potenziali, sarebbe stato necessario creare collegamenti tra la tabella [USERS] e le tabelle [MEDECINS] e [CLIENTS]. Ciò avrebbe quindi avuto un impatto significativo sul progetto esistente.
8.4.13.8. Il progetto STS del livello [web]
![]() |
Il progetto [rdvmedecins-webjson] si evolve come segue da [1]:
![]() |
Le modifiche principali vanno apportate nel pacchetto [rdvmedecins.web.config], dove è necessario configurare Spring Security. Ce ne sono altre, 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 estenda la classe [WebSecurityConfigurerAdapter];
- riga 13: definire un metodo [configure(HttpSecurity http)] che definisce i diritti di accesso alle diverse 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 {
// l'autenticazione viene effettuata dal bean [appUserDetailsService]
// la password è crittografata tramite l'algoritmo di hash BCrypt
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// applicazione sicura?
if (application.isSecured()) {
// la password viene trasmessa tramite l'intestazione Authorization: Basic xxxx
http.httpBasic();
// il metodo HTTP OPTIONS deve essere autorizzato per tutti
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// solo il ruolo ADMIN può utilizzare l'applicazione
http.authorizeRequests() //
.antMatchers("/", "/**") // tutte le URL
.hasRole("ADMIN");
// nessuna sessione
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 consente l'accesso agli utenti dell'applicazione;
- righe 21-22: viene iniettata la classe [ApplicationModel], che funge da cache per l'applicazione web. Si decide qui di utilizzarla anche per configurare l’applicazione web in un unico punto. È questa classe che definisce il valore booleano [isSecured] della riga 36. Questo valore booleano protegge (true) o meno (false) l’applicazione web;
- righe 25-29: il metodo [configure(HttpSecurity http)] definisce gli utenti e i loro ruoli. Riceve come parametro un tipo [AuthenticationManagerBuilder]. Questo parametro viene arricchito con due informazioni (riga 28):
- un riferimento al servizio [appUserDetailsService] della riga 20 che consente l’accesso agli utenti registrati. Si noti qui che il fatto che siano registrati in un database non risulta esplicito. Potrebbero quindi trovarsi in una cache, essere forniti da un servizio web, ...
- 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 ai URL del servizio web;
- riga 34: abbiamo visto nel progetto introduttivo che, per impostazione predefinita, Spring Security gestiva un token CSRF (Cross Site Request Forgery) che l’utente che voleva autenticarsi doveva rinviare al server. Qui questo meccanismo è disattivato. Ciò, insieme al valore booleano (isSecured=false), consente di utilizzare l’applicazione web senza sicurezza;
- riga 38: si attiva la modalità di autenticazione tramite l'intestazione HTTP. Il client dovrà inviare la seguente intestazione HTTP:
dove «code» è la codifica della stringa «login:password» tramite l’algoritmo Base64. Ad esempio, la codifica Base64 della stringa admin:admin è YWRtaW46YWRtaW4=. Pertanto, l'utente con nome utente [admin] e password [admin] invierà la seguente intestazione HTTP per autenticarsi:
- righe 40-42: indicano che tutti i URL del servizio web sono accessibili agli utenti con il ruolo [ROLE_ADMIN]. Ciò significa che un utente che non possiede tale 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 volte successive, non gli verranno richiesti i dati di accesso. In questo caso, è stata scelta una modalità senza sessione. Ogni richiesta dovrà essere accompagnata dai dati di sicurezza;
La classe [AppConfig], che configura l’intera applicazione, viene modificata 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: si aggiunge la classe di configurazione [SecurityConfig];
Infine, la classe [ApplicationModel] viene arricchita con un valore booleano:
@Component
public class ApplicationModel implements IMetier {
...
// dati di configurazione
private boolean secured = false;
public boolean isSecured() {
return secured;
}
- riga 6: si imposta il valore booleano [secured] su [true / false] a seconda che si desideri o meno attivare la protezione.
8.4.13.9. Test del servizio web
Testeremo il servizio web con il client Chrome [Advanced Rest Client]. Dovremo specificare l'intestazione di autenticazione HTTP:
dove [code] è il codice Base64 della stringa [login:password]. 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) {
// sono richiesti due argomenti: login e password
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// si recuperano i due argomenti
String chaîne = String.format("%s:%s", args[0], args[1]);
// si codifica la stringa
byte[] data = Base64.encode(chaîne.getBytes());
// si visualizza la sua codifica Base64
System.out.println(new String(data));
}
}
Se eseguiamo questo programma con i due argomenti [admin admin]:
![]() |
si ottiene il seguente risultato:
Ora che sappiamo come generare l'intestazione di autenticazione HTTP, avviamo il servizio web, ora protetto:
@Component
public class ApplicationModel implements IMetier {
...
private boolean secured = true;
Quindi, con il client Chrome [Advanced Rest Client], richiediamo l’elenco di tutti i medici:
![]() |
- in [1], richiediamo l'URL dei medici;
- in [2], con un metodo GET;
- in [3], forniamo l'intestazione HTTP dell'autenticazione. Il codice [YWRtaW46YWRtaW4=] è la codifica Base64 della stringa [admin:admin];
- in [4], inviamo il comando 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;
Si ottiene effettivamente l'elenco dei medici:
![]() |
Proviamo ora a effettuare 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 Unauthorized]. In questo caso, l’utente si è autenticato correttamente ma non dispone dei diritti sufficienti per accedere a URL;
Un servizio web protetto è ora operativo. Lo completeremo in modo che autorizzi le richieste interdominio. Questa esigenza è emersa nel documento [Tutoriel AngularJS / Spring 4] e, sebbene qui non sussista, provvederemo comunque a soddisfarla.
8.4.14. Implementazione delle richieste tra domini
Esaminiamo il problema delle richieste tra domini. Nel documento [Tutoriel AngularJS / Spring 4] viene sviluppata 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]. Ebbene, ciò è vietato dal browser che esegue l’applicazione Angular perché costituisce una falla di sicurezza. L’applicazione può interrogare solo il server da cui proviene, ovvero il server [1];
In realtà, non è esatto affermare che il browser impedisca all’applicazione Angular di interrogare il server [2]. In realtà, l’applicazione lo interroga per chiedergli se autorizza un client che non proviene dal suo stesso dominio a interrogarlo. Questa tecnica di condivisione è denominata CORS (Cross-Origin Resource Sharing). Il server [2] dà il proprio consenso inviando specifici header HTTP.
Per illustrare i problemi che si possono incontrare, creeremo un’applicazione client/server in cui:
- il server sarà il nostro server web / jSON;
- il client sarà una semplice pagina HTML dotata di un codice JavaScript che effettuerà richieste al server web / jSON;
8.4.14.1. Il progetto del 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 /> <!-- ricerca il genitore nel 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: si tratta di un progetto Spring Boot;
- righe 29-32: si utilizza la dipendenza [spring-boot-starter-web] che include un server Tomcat e Spring MVC;
La pagina HTML è la seguente:
![]() |
Viene generata dal codice seguente:
<!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">
<!-- metodo 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 />
<!-- valore inserito -->
<br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
<!-- pulsante di conferma -->
<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: si importa la libreria jQuery;
- riga 7: si importa un codice che scriveremo noi;
Il codice [client.js] è il seguente:
// dati globali
var url;
var posted;
var response;
var method;
function requestServer() {
// si recuperano le informazioni del modulo
var urlValue = url.val();
var postedValue = posted.val();
method = document.forms[0].elements['method'].value;
// si effettua una chiamata Ajax manualmente
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// si effettua una chiamata Ajax manualmente
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + url,
type : 'GET',
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// risultato testo
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// errore di sistema
response.text(jqXHR.responseText);
}
})
}
function doPost(url, posted) {
// si effettua manualmente una chiamata Ajax
$.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) {
// risultato testo
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// errore di sistema
response.text(jqXHR.responseText);
}
})
}
// durante il caricamento del documento
$(document).ready(function() {
// si recuperano i riferimenti dei componenti della pagina
url = $("#url");
posted = $("#posted");
response = $("#response");
});
Lasciamo che sia il lettore a comprendere questo codice. Tutto è già stato incontrato prima o poi. Alcune righe meritano tuttavia una spiegazione:
- riga 11:
- [document] indica il documento caricato dal browser, ovvero il cosiddetto DOM (Document Object Model),
- [document.forms[0]] indica il primo modulo del documento; un documento può contenerne diversi. In questo caso ce n’è solo uno,
- [document.forms[0].elements['method']] indica l'elemento del modulo che possiede 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 che possiede l'attributo [name='method']. Si sa che il valore inviato è il valore dell’attributo [value] del pulsante di opzione selezionato. In questo caso, sarà quindi una delle stringhe ['get', 'post'];
- righe 23-25: ci si rivolge a un server che richiede un'intestazione HTTP [Authorization: Basic code]. Creiamo questa intestazione per l'utente [admin / admin], l'unico autorizzato a interrogare il server;
- riga 26: l’utente inserirà dei URL del tipo [/getAllMedecins, /supprimerRv, ...]. È quindi necessario completare questi URL;
- riga 28: il server restituisce un jSON che è una forma di testo. Si indica il tipo [text/plain] come tipo di risultato per visualizzarlo così come è stato ricevuto;
- riga 33: visualizzazione della risposta testuale del server;
- riga 39: visualizzazione di un eventuale messaggio di errore in formato testo;
- riga 52: per indicare che il client invia jSON;
Nell'applicazione client/server realizzata:
- il client è un'applicazione web disponibile all'indirizzo URL [http://localhost:8081]. È l'applicazione che stiamo realizzando;
- il server è un'applicazione web disponibile all'indirizzo URL [http://localhost:8080]. È il nostro server web / jSON;
Poiché il client non si collega dalla stessa porta del server, si presenta il problema delle richieste interdominio. [http://localhost:8080] e [http://localhost:8081] sono due domini diversi.
L'applicazione Spring Boot è un'applicazione da 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);
}
// pagine statiche
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
}
// configurazione dispatcherServlet
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
// server Tomcat integrato
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8081);
}
}
- riga 14: la classe [Client] è una classe di configurazione Spring;
- riga 15: si configura un'applicazione Spring MVC. Questa annotazione comporta una serie di configurazioni automatiche;
- riga 16: per ridefinire alcuni valori predefiniti del framework Spring MVC, è necessario estendere la classe [WebMvcConfigurerAdapter];
- righe 23-26: il metodo [addResourceHandlers] consente di specificare le cartelle in cui si trovano le risorse statiche (html, css, js, ...) dell'applicazione. In questo caso, si indica la cartella [static] situata nel Classpath del progetto:
![]() |
- righe 29-37: configurazione del bean [dispatcherServlet] che indica il servlet Spring MVC;
- righe 40-43: il server Tomcat integrato funzionerà sulla porta 8081;
8.4.14.2. URL [/getAllMedecins]
Avviamo:
- il server web/json sulla porta 8080;
- il client di questo server sulla porta 8081;
quindi richiediamo il URL [http://localhost:8081/client.html] [1]:
![]() |
- in [2], eseguiamo un GET su URL [http://localhost:8080/getAllMedecins];
Non riceviamo alcuna risposta dal server. Osservando la console di sviluppo (Ctrl-Maiusc-I) si nota un errore:
![]() |
- in [1], ci troviamo nella scheda [Network];
- In [2], si vede che la richiesta HTTP che è stata effettuata non è [GET], bensì [OPTIONS]. Nel caso di una richiesta interdominio, il browser verifica con il server che siano soddisfatte determinate condizioni inviandogli una richiesta HTTP [OPTIONS]. In questo caso, le richieste sono quelle indicate dai simboli [5-6];
- in [5], il browser chiede se la destinazione URL sia raggiungibile tramite 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 della richiesta [Access-Control-Request-Headers] attende una risposta con un'intestazione HTTP [Access-Control-Allow-Headers] che indichi che le intestazioni richieste sono accettate;
- si verifica un errore in [3]. Cliccando 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], si può constatare che il server non ha effettivamente inviato tale intestazione. Di conseguenza, il browser ha rifiutato di eseguire la richiesta HTTP GET inizialmente richiesta;
Dobbiamo modificare il server web / jSON. Effettuiamo una prima modifica in [ApplicationModel], che è uno degli elementi di configurazione del servizio web:
![]() |
@Component
public class ApplicationModel implements IMetier {
...
// dati di configurazione
private boolean corsAllowed = true;
private boolean secured = true;
...
public boolean isCorsAllowed() {
return corsAllowed;
}
- riga 6: creiamo un valore booleano che indica se si accettano o meno i client esterni al dominio del server;
- righe 10-12: il metodo di accesso a questa informazione;
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;
// invio delle opzioni al client
public void sendOptions(String origin, HttpServletResponse response) {
// CORS consentito?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// si imposta l'intestazione CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// si autorizzano determinati header
response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
// si autorizza il GET
response.addHeader("Access-Control-Allow-Methods", "GET");
}
// elenco dei medici
@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 URL [/getAllMedecins] quando viene richiesta con il comando HTTP [OPTIONS];
- riga 34: il metodo [getAllMedecins] accetta come parametri:
- l'oggetto [@RequestHeader(value = "Origin", required = false)] che recupererà l'intestazione HTTP [Origin] della richiesta. Tale intestazione è stata inviata dal mittente della richiesta:
Si specifica che l’intestazione HTTP [Origin] è facoltativa [required = false]. In questo caso, se l'intestazione è assente, il parametro [String origin] assumerà il valore null. Poiché [required = true] è il valore predefinito, se l'intestazione è assente viene generata un'eccezione. Si è voluto evitare questo caso;
- riga 34:
- l'oggetto [HttpServletResponse response] che verrà inviato al cliente che ha effettuato la richiesta;
Questi due parametri vengono inseriti da Spring;
- riga 35: si delega l'elaborazione della richiesta al metodo delle righe 19-30;
- righe 15-16: viene iniettato l’oggetto [ApplicationModel];
- righe 21-23: se l'applicazione è configurata per accettare le richieste interdominio e se il mittente ha inviato l'intestazione HTTP [Origin] e se tale origine inizia con [http://localhost], allora si accetta la richiesta inter-dominio, altrimenti la si rifiuta;
- riga 25: se il client si trova nel dominio [http://localhost:port], si invia l'intestazione HTTP:
il che significa che il server accetta l’origine del client;
- riga 25: abbiamo segnalato due intestazioni HTTP particolari nella richiesta HTTP [OPTIONS]:
All'intestazione HTTP [Access-Control-Request-X], il server risponde con un'intestazione HTTP [Access-Control-Allow-X] in cui indica ciò che è consentito. Le righe 23-26 si limitano a riprendere la richiesta del client per indicare che è stata accettata;
Ora siamo pronti per nuovi test. Lanciamo la nuova versione del servizio web e scopriamo che il problema persiste. Non è cambiato nulla. Se alla riga 35 sopra inseriamo un output in console, questo non viene mai visualizzato, dimostrando così che il metodo [getAllMedecins] della riga 34 non viene mai chiamato.
Dopo alcune ricerche, scopriamo che Spring MVC gestisce autonomamente i comandi HTTP e [OPTIONS] con un'elaborazione 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 {
// configurazione di dispatcherservlet per le intestazioni CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
// mappatura jSON
...
- righe 10-11: il bean [dispatcherServlet] serve a definire il servlet che gestisce le richieste dei client. In questo caso è di tipo [DispatcherServlet], il servlet del framework Spring MVC;
- riga 12: si crea un'istanza di tipo [DispatcherServlet];
- riga 13: si richiede alla servlet di inoltrare all'applicazione i comandi HTTP [OPTIONS];
- riga 14: si rende la servlet così configurata;
Ripetiamo i test con questa nuova configurazione. Otteniamo il seguente risultato:
![]() |
- in [1], si nota che ci sono due richieste HTTP verso URL e [http://localhost:8080/getAllMedecins];
- in [2], la richiesta [OPTIONS];
- in [3], le tre intestazioni HTTP che abbiamo appena configurato nella risposta del server;
Esaminiamo ora la seconda richiesta:
![]() |
- in [1], la richiesta esaminata;
- in [2], ovvero la richiesta GET. Grazie alla prima richiesta [OPTIONS], il browser ha ricevuto le informazioni richieste. Ora esegue la richiesta [GET] richiesta inizialmente;
- 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 [3] del server è normale [HTTP/1.1 200 OK]. Dovremmo quindi avere il documento richiesto. È possibile che il server abbia effettivamente inviato il documento, ma che sia il browser a impedirne l'utilizzo perché richiede che anche per la richiesta GET la risposta contenga l'intestazione HTTP [Access-Control-Allow-Origin:http://localhost:8081].
Modifichiamo il controller [RdvMedecinsController] nel modo seguente:
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// elenco dei medici
@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 {
// la risposta
Response<List<Medecin>> response;
// intestazioni CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// stato dell'applicazione
...
- righe 1-2: viene inserito il controller [RdvMedecinsCorsController];
- righe 7-8: si inietta nei parametri del metodo [getAllMedecins] l’oggetto HttpServletResponse, che incapsula la risposta che verrà inviata al client, e l’intestazione HTTP [Origin];
- riga 12: si richiama il metodo [sendOptions] del controller [RdvMedecinsCorsController], lo stesso che è stato richiamato per elaborare la richiesta HTTP [OPTIONS]. Invierà quindi le stesse intestazioni HTTP utilizzate per quella richiesta;
Dopo questa modifica, i risultati sono i seguenti:
![]() |
Abbiamo ottenuto correttamente l’elenco dei medici.
8.4.14.3. Gli altri URL [GET]
Mostriamo ora gli altri URL interrogati tramite un GET. Nei controller, il codice delle azioni che li gestiscono segue il modello delle azioni che hanno precedentemente gestito l’URL [/getAllMedecins]. Il lettore può verificare il codice negli esempi forniti con questo documento. Ecco un esempio:
in [RdvMedecinsCorsController]
// elenco degli appuntamenti di un medico
@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]
// elenco degli appuntamenti di un medico
@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 {
// la risposta
Response<List<Rv>> response = null;
boolean erreur = false;
// intestazioni CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// stato dell'applicazione
...
Ecco ora alcune schermate di esecuzione:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
8.4.14.4. I URL [POST]
Esaminiamo il seguente caso:
![]() |
- si effettua un trasferimento da POST [1] verso URL [2];
- in [3], il valore inviato. Si tratta di una stringa jSON;
- in totale, si cerca di eliminare l'appuntamento con il codice [id] 100;
Per il momento non modifichiamo alcun codice. Il risultato ottenuto è quindi il seguente:
![]() |
- in [1], come per le richieste [GET], il browser effettua una richiesta [OPTIONS];
- da [2], richiede un'autorizzazione di accesso per una richiesta [POST]. In precedenza era [GET];
- in [3], richiede l’autorizzazione a inviare le intestazioni HTTP e [accept, authorization, content-type]. In precedenza erano presenti solo le prime due intestazioni;
Modifichiamo il metodo [RdvMedecinsCorsController.sendOptions] nel modo seguente:
public void sendOptions(String origin, HttpServletResponse response) {
// Cors consentito?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// si imposta l'intestazione CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// si autorizzano alcune intestazioni
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// si autorizza il GET
response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
- riga 9: abbiamo aggiunto l'intestazione HTTP [Content-Type] (le maiuscole e le minuscole non fanno differenza);
- riga 11: è stato aggiunto il metodo HTTP [POST];
In questo modo i metodi [POST] vengono trattati allo stesso modo delle richieste [GET]. Ecco l'esempio di URL [/supprimerRv]:
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 {
// la risposta
Response<Void> response = null;
boolean erreur = false;
// intestazioni CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// stato dell'applicazione
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 ottenuto è il seguente:
![]() |
Per URL e [/ajouterRv], si ottiene il seguente risultato:
![]() |
8.4.14.5. Conclusion
La nostra applicazione ora supporta le richieste interdominio. Queste possono essere autorizzate o meno tramite configurazione nella classe [ApplicationModel]:
// dati di configurazione
private boolean corsAllowed = false;
8.5. Client programmato del servizio web / jSON
Torniamo all’architettura generale dell’applicazione che vogliamo scrivere:
![]() |
La parte superiore dello schema è stata già implementata. Si tratta del server web / jSON. Passiamo ora alla 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 da console
Il progetto STS del client da console sarà il seguente:
![]() |
8.5.2. Configurazione Maven
Il file [pom.xml] del 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 /> <!-- ricerca del genitore dal repository -->
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- libreria jSON utilizzata da Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- componente utilizzato da 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 console del server web / jSON si basa su un componente denominato [RestTemplate] fornito dalla dipendenza [spring-web];
- righe 29-36: la serializzazione/deserializzazione degli oggetti jSON richiede una libreria jSON. Utilizziamo una variante della libreria Jackson impiegata da Spring Web;
- righe 38-41: al livello più basso, il componente [RestTemplate] comunica con il server tramite i socket TCP/IP. Vogliamo impostare il valore di [timeout], ovvero il tempo massimo di attesa per una risposta dal server. Il componente [RestTemplate] non ci permette di impostarlo. Per farlo, passeremo al costruttore [RestTemplate] un componente di basso livello fornito dalla dipendenza [org.apache.httpcomponents.httpclient]. È questa dipendenza che ci consentirà di impostare il [timeout] della comunicazione;
8.5.3. Il pacchetto [rdvmedecins.client.entities]
![]() |
Il pacchetto [rdvmedecins.client.entities] raggruppa tutte le entità che il servizio web / jSON invia tramite i suoi vari URL. Non le descriveremo nuovamente in dettaglio. Ci limiteremo a dire che le entità JPA e [Client, Creneau, Medecin, Rv, Personne] sono state private di tutte le loro annotazioni JPA e delle loro annotazioni jSON. Ecco, ad esempio, la classe [Rv]:
package rdvmedecins.client.entities;
import java.util.Date;
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// giorno dell'appuntamento
private Date jour;
// un appuntamento è associato a un cliente
private Client client;
// un appuntamento è collegato a una fascia oraria
private Creneau creneau;
// chiavi esterne
private long idClient;
private long idCreneau;
// Costruttore predefinito
public Rv() {
}
// con parametri
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);
}
// getter e setter
...
}
8.5.4. Il pacchetto [rdvmedecins.client.requests]
![]() |
Il pacchetto [rdvmedecins.client.requests] raggruppa le due classi il cui valore jSON è associato a URL, [/ajouterRv] e [supprimerRv]. Sono identiche a quelle presenti sul lato server.
8.5.5. Il pacchetto [rdvmedecins.client.responses]
![]() |
[Response] è il tipo di tutte le risposte del servizio web / jSON. Si tratta di un tipo generico:
package rdvmedecins.client.responses;
import java.util.List;
public class Response<T> {
// ----------------- proprietà
// stato dell'operazione
private int status;
// eventuali messaggi di errore
private List<String> messages;
// il corpo della risposta
private T body;
// costruttori
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getter e setter
...
}
- 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 costituisce 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() {
// creazione del componente RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// risultato
return restTemplate;
}
// mappatori 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à esaminato alla ricerca di componenti Spring. Al suo interno si troverà il componente [Dao];
- 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 / jSON;
- riga 21: si potrebbe scrivere [RestTemplate restTemplate = new RestTemplate() ;]. Nella maggior parte dei casi è sufficiente. Ma in questo caso vogliamo impostare i [timeout] del client. A tal fine, si inietta nel componente [RestTemplate] un componente di basso livello di tipo [HttpComponentsClientHttpRequestFactory] (riga 20) che ci consentirà di impostare questi [timeout]. La dipendenza Maven necessaria è stata presentata;
- righe 28-57: definiscono i mappatori jSON. Si tratta dei mappatori jSON utilizzati sul lato server (cfr. paragrafo 8.4.11.3) per serializzare il tipo T della risposta [Response<T>]. Questi stessi convertitori verranno 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 i 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 {
// URL del servizio web
public void setUrlServiceWebJson(String url);
// timeout
public void setTimeout(int timeout);
// autenticazione
public void authenticate(User user);
// elenco dei clienti
public List<Client> getAllClients(User user);
// elenco dei medici
public List<Medecin> getAllMedecins(User user);
// elenco delle fasce orarie di un medico
public List<Creneau> getAllCreneaux(User user, long idMedecin);
// trova un cliente identificato dal suo ID
public Client getClientById(User user, long id);
// trovare un cliente identificato dal suo ID
public Medecin getMedecinById(User user, long id);
// trovare un appuntamento identificato dal suo ID
public Rv getRvById(User user, long id);
// trovare una fascia oraria identificata dal proprio ID
public Creneau getCreneauById(User user, long id);
// aggiungere un RV
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
// eliminare un RV
public void supprimerRv(User user, long idRv);
// elenco degli appuntamenti di un medico in un determinato giorno
public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
// agenda
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
}
- riga 14: il metodo che consente di impostare la radice del servizio web / jSON, ad esempio [http://localhost:8080];
- riga 17: il metodo che consente di impostare i [timeout] lato client. Si desidera controllare questo parametro poiché alcuni client HTTP a volte impiegano molto tempo ad attendere una risposta che non arriverà;
- riga 20: il metodo che consente di identificare un utente [login, passwd]. Genera un'eccezione se l'utente non viene riconosciuto;
- righe 22-53: a ogni URL esposta dal servizio web / jSON è associato un metodo dell'interfaccia la cui firma deriva dalla firma del metodo lato server che gestisce la URL esposta. Prendiamo ad esempio il seguente URL lato 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: si nota che [idMedecin] e [jour] sono i parametri di URL. Questi saranno i parametri di input del metodo associato a questo URL lato client;
- riga 2: si nota che il metodo lato server restituisce un tipo [Response<String>]. Questo tipo [String] è il tipo del valore jSON di tipo [AgendaMedecinJour]. Il tipo del risultato del metodo associato a questo URL sul lato client sarà [AgendaMedecinJour];
Sul lato client, si dichiara il seguente metodo:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
Questa firma è valida quando il server invia una risposta [int status, List<String> messages, String body] con [status==0]. In questo caso abbiamo [messages==null && body!=null]. Non è valida quando si ha [status!=0]. In questo caso si ottiene [messages!=null && body==null]. È necessario segnalare in qualche modo che si è verificato un errore. A tal fine, verrà generata 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;
// codice di errore
private int status;
// elenco dei messaggi di errore
private List<String> messages;
public RdvMedecinsException() {
}
public RdvMedecinsException(int code, List<String> messages) {
super();
this.status = code;
this.messages = messages;
}
// getter e setter
...
}
- righe 9 e 11: l’eccezione riprenderà i valori dei campi [status, messages] dell’oggetto [Response<T>] inviato dal server;
- riga 5: la classe [RdvMedecinsException] estende la classe [RuntimeException]. Si tratta quindi di un'eccezione non controllata, ovvero non è obbligatorio gestirla con un try/catch né dichiararla nella firma 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 {
// dati
private String login;
private String passwd;
// costruttori
public User() {
}
public User(String login, String passwd) {
this.login = login;
this.passwd = passwd;
}
// getter e setter
...
}
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 conosciamo l’interfaccia del livello [DAO], possiamo presentare l’applicazione da 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 {
// serializzatore jSON
static private ObjectMapper mapper = new ObjectMapper();
// timeout delle connessioni in millisecondi
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// si recupera un riferimento sul livello [DAO]
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// si imposta l'URL del servizio web / json
dao.setUrlServiceWebJson("http://localhost:8080");
// si impostano i timeout in millisecondi
dao.setTimeout(TIMEOUT);
// Autenticazione
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);
}
// elenco dei clienti
message = "/getAllClients";
try {
showResponse(message, dao.getAllClients(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// elenco dei medici
message = "/getAllMedecins";
try {
showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// elenco degli orari disponibili del medico 2
message = "/getAllCreneaux/2";
try {
showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// cliente n. 1
message = "/getClientById/1";
try {
showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// medico n. 2
message = "/getMedecinById/2";
try {
showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// fascia oraria n. 3
message = "/getCreneauById/3";
try {
showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// appuntamento n. 4
message = "/getRvById/4";
try {
showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// aggiunta di un appuntamento
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);
}
// elenco degli appuntamenti del medico 1 dell'8 gennaio 2015
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// agenda del medico 1 dell'8 gennaio 2015
message = "/getAgendaMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// eliminazione dell'appuntamento aggiunto
message = String.format("/supprimerRv [idRv=%s]", idRv);
try {
dao.supprimerRv(new User("admin", "admin"), idRv);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// elenco degli appuntamenti del medico 1 dell'8 gennaio 2015
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// chiusura contesto
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 di un'applicazione Spring. Passiamo al suo costruttore, la classe [AppConfig] che configura l'applicazione;
- riga 26: si recupera un riferimento al livello [DAO];
- righe 27-30: lo si configura;
- righe 32-169: si testano 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 associare i risultati al codice. Quest’ultimo mostra come richiamare ciascun metodo del livello [DAO]. Basti notare 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: si aggiunge un appuntamento al medico n. 1;
- righe 32-33: si visualizza questo appuntamento. È l'unico della giornata;
- righe 34-35: lo si vede anche nell’agenda del medico;
- righe 36-37: l’appuntamento è scomparso. Il codice lo ha nel frattempo cancellato;
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">
<!-- agli encoder viene assegnato per impostazione predefinita il tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- controllo del livello dei log -->
<root level="info"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
8.5.10. Implementazione del livello [DAO]
Ora non ci resta che presentare il cuore del livello [DAO], ovvero l'implementazione della sua interfaccia [IDao]. Lo faremo in modo graduale.
![]() |
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 {
// dati
@Autowired
protected RestTemplate restTemplate;
protected String urlServiceWebJson;
// URL servizio web / jSON
public void setUrlServiceWebJson(String url) {
this.urlServiceWebJson = url;
}
public void setTimeout(int timeout) {
// impostazione del timeout delle richieste del client web
HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
.getRequestFactory();
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
}
private String getBase64(User user) {
// si codificano in base 64 l'utente e la sua password - richiede
// 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())));
}
// richiesta generica
protected String getResponse(User user, String url, String jsonPost) {
...
}
}
- riga 20: la classe è astratta, il che ci impedisce di designarla come componente Spring. Sarà la sua classe figlia a essere designata come tale;
- righe 23-24: iniettiamo il bean [restTemplate] che abbiamo definito nella classe di configurazione [AppConfig];
- riga 25: URL è la radice del servizio web /jSON;
- righe 32-38: impostano il timeout del client in attesa di una risposta dal server;
- riga 34: recuperiamo il componente [HttpComponentsClientHttpRequestFactory] che avevamo iniettato nel bean [restTemplate] al momento della sua creazione (cfr. [AppConfig]);
- riga 36: impostiamo il tempo massimo di attesa del client quando stabilisce una connessione con il server;
- riga 37: impostiamo il tempo massimo di attesa del client quando attende una risposta a una delle sue richieste;
L'implementazione dei metodi di comunicazione con il server verrà fattorizzata nel seguente metodo generico:
// richiesta generica
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. Si tratta della parte finale dell’URL, mentre la prima parte è fornita dal campo [urlServiceWebJson] della classe;
- [String jsonPost]: la stringa jSON da inviare. Se questo valore è presente, verrà richiesto l’URL con un POST, altrimenti con un GET;
Continuiamo:
// richiesta generica
protected String getResponse(User user, String url, String jsonPost) {
// URL: URL da contattare
// jsonPost: il valore jSON da inviare
try {
// esecuzione della richiesta
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);
}
// si esegue la richiesta
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 numero significativo di metodi di scambio con il server. Si sarebbe potuto scegliere un metodo diverso da [exchange]. Il secondo parametro della chiamata definisce il tipo di risposta attesa, in questo caso una stringa jSON. Il primo parametro è la richiesta di tipo [RequestEntity] (riga 7). Il risultato del metodo [exchange] è di tipo [ResponseEntity<String>]. Il tipo [ResponseEntity] incapsula la risposta completa del server, le intestazioni HTTP e il documento inviato da quest’ultimo. Allo stesso modo, il tipo [RequestEntity] incapsula l’intera richiesta del client, comprese le intestazioni HTTP e l’eventuale valore inviato tramite POST;
- riga 23: è 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 di tipo [RequestEntity]. Essa varia a seconda che si utilizzi un GET o un POST per effettuare la richiesta;
- riga 9: la richiesta per un GET. La classe [RequestEntity] offre metodi statici per creare le richieste GET, POST, HEAD,... Il metodo [RequestEntity.get] consente di creare una richiesta GET concatenando i diversi metodi che la compongono:
- il metodo [RequestEntity.get] accetta come parametro il URL di destinazione 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 questa concatenazione di metodi è un tipo [HeadersBuilder];
- righe 10-12: nel caso in cui il parametro [User user] non sia null, si include l’intestazione HTTP [Authorization] nella richiesta;
- riga 13: il metodo [HeadersBuilder.build] utilizza queste diverse informazioni per costruire il tipo [RequestEntity] della richiesta;
- riga 15: la richiesta per un POST. Il metodo [RequestEntity.post] consente di creare una richiesta POST concatenando i diversi metodi che la costruiscono:
- il metodo [RequestEntity.post] accetta come parametro il URL di destinazione sotto forma di un'istanza URI,
- il metodo [header] consente di definire le intestazioni HTTP che si desidera utilizzare, in questo caso quella relativa all’autorizzazione,
- il metodo [header] che segue include nella richiesta l’intestazione [Content-Type: application/json] per indicarle che il valore inviato le arriverà sotto forma di una stringa jSON;
- il metodo [accept] consente di indicare che accettiamo il tipo [application/json] che il server invierà;
- righe 17-19: nel caso in cui il parametro [User user] non sia null, si include l'intestazione HTTP [Authorization] nella richiesta;
- riga 20: il metodo [BodyBuilder.body] imposta il valore registrato. Questo è il secondo parametro del metodo generico [getResponse] (riga 2);
- righe 25-28: se si verifica un errore qualsiasi, viene generata un'eccezione di tipo [RdvMedecinsException];
Il metodo [getMessagesForException] delle righe 26 e 28 è il seguente:
// elenco dei messaggi di errore di un'eccezione
protected static List<String> getMessagesForException(Exception exception) {
// si recupera l'elenco dei messaggi di errore dell'eccezione
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// si recupera il messaggio solo se !=null e non è vuoto
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// causa successiva
cause = cause.getCause();
}
return erreurs;
}
Il metodo privato [getBase64] fornisce il codice Base64 della stringa 'login:passwd' per l'intestazione di autenticazione HTTP:
private String getBase64(User user) {
// codifica in base 64 l'utente e la sua password - richiede 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] nel modo seguente:
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 {
// mappatori 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]. Si sarebbe potuto continuare a utilizzare l'annotazione [@Component] utilizzata fino a quel momento;
- 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) {
// la risposta
Response<AgendaMedecinJour> response;
// l'agenda
String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
try {
// l'agenda AgendaMedecinJour
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
});
} catch (IOException e) {
throw new RdvMedecinsException(401, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(402, getMessagesForException(e));
}
// analisi della risposta
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: la destinazione URL;
- 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 di tipo [RdvMedecinsException]. Se generata, tale eccezione verrà propagata al metodo che ha chiamato il metodo [getAgendaMedecinJour] sopra indicato;
- riga 8: ilURL [/getAgendaMedecinJour] invia un tipo [Response<AgendaMedecinJour>] che è stato serializzato in jSON sul lato server dal mappatore jSON [jsonMapperLongRv]. Lo stesso mappatore viene utilizzato per deserializzare la stringa jSON ricevuta;
- righe 10-13: se si verifica un errore alla riga 9, viene generata un'eccezione di tipo [RdvMedecinsException];
- righe 16-21: si analizza la risposta inviata dal server;
- righe 17-18: se il server ha segnalato un errore, viene generata un'eccezione con le informazioni trasmesse dal server;
- righe 19-21: in caso contrario, viene restituito l’agenda del medico;
La richiesta POST esaminata sarà la seguente:
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
// la risposta
Response<Rv> response;
try {
// l'appuntamento
String jsonResponse = getResponse(user, "/ajouterRv",
jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
// l'appuntamento Appuntamento
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));
}
// analisi della risposta
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: si passa il valore jSON di tipo [PostAjouter] costruito con le informazioni ricevute come parametri dal metodo. Si utilizza un mappatore jSON senza filtri;
- riga 9: lato server, è il mappatore jSON [jsonMapperLongRv] che ha serializzato la risposta del server. Lato client, si utilizza lo stesso mappatore per deserializzarla;
- riga 6: il URL [/ajouterRv] restituisce il valore jSON di tipo [Response<Rv>];
- righe 4-11: qui, il metodo [getResponse] è stato inserito in un try/catch perché la serializzazione del valore inviato può generare un'eccezione. Il metodo [getResponse] potrebbe generare un'eccezione [RdvMedecinsException]. In tal caso, ci si limita a rilanciarla (righe 11-12);
Il codice che segue (righe 13-24) è analogo a quello appena esaminato. L’unica differenza rispetto a un’operazione GET è quindi il secondo parametro del metodo [getResponse], che deve essere il valore jSON del valore da inserire.
Gli altri metodi seguono lo stesso schema.
8.5.11. Anomalia
Effettuando vari test si riscontra 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 {
// serializzatore jSON
static private ObjectMapper mapper = new ObjectMapper();
// timeout delle connessioni in millisecondi
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// si recupera un riferimento sul livello [DAO]
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// si imposta l'URL del servizio web / json
dao.setUrlServiceWebJson("http://localhost:8080");
// si impostano i timeout in millisecondi
dao.setTimeout(TIMEOUT);
// Autenticazione
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);
}
// Autenticazione
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);
}
// Autenticazione
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);
}
// Chiusura del contesto
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: si autentica l’utente [admin, admin];
- righe 40-47: si autentica l'utente [admin, x], che quindi ha una password errata;
- righe 49-56: si autentica l'utente [user, user], che è un utente esistente ma non autorizzato;
Ecco i risultati:
- riga 2: contrariamente alle aspettative, l'utente [admin, x] è stato accettato;
Se si commentano le righe 33-38 del codice, si ottiene il seguente risultato:
che è il risultato atteso. È come se, una volta che l’utente [admin, admin] si fosse autenticato con successo per la prima volta, la sua password non fosse più necessaria per le volte successive. È proprio così. Spring Security utilizza per impostazione predefinita una sessione che fa sì che, una volta che un utente si è autenticato, non debba più farlo nelle richieste successive. È possibile modificare la configurazione di [Spring Security] nel server web / jSON affinché ciò non avvenga più:
![]() |
Il file [SecurityConfig] deve essere modificato come segue:
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// nessuna sessione
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
- la riga 5 richiede che non vi sia alcuna sessione di sicurezza;
Questo ha risolto il problema dell'anomalia.
8.6. Codice del server Spring / Thymeleaf
8.6.1. Introduzione
Torniamo all’architettura dell’applicazione client/server da realizzare:
![]() |
- è stato realizzato il server web [Web2] / jSON;
- è 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]. Anch’esso è stateless;
- in [2]: il codice JavaScript incorporato nella vista caricata all’avvio dell’applicazione è strutturato a livelli:
- il livello [présentation] gestisce le interazioni con l’utente,
- il livello [DAO] gestisce l’accesso ai dati tramite il server [Web2];
- il client [2] memorizzerà alcune viste nella cache per alleggerire il carico del server;
Costruiremo il server web / jSON [Web1] implementato con Spring MVC / Thymeleaf in diverse fasi:
- scoperta del framework CSS Bootstrap;
- scrittura delle viste;
- scrittura del controller;
Successivamente, e separatamente, realizzeremo il client JS del server [Web1]. Per dimostrare chiaramente che questo client gode di una certa indipendenza rispetto al server [Web1], lo realizzeremo con lo strumento [Webstorm] anziché con STS.
Di seguito, alcuni dettagli saranno tralasciati perché potrebbero distrarci dall’aspetto fondamentale, ovvero l’organizzazione del codice. Il lettore interessato potrà trovare il codice completo sul sito di questo documento.
8.6.2. Il progetto STS
![]() |
- in [1], i codici 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 {
// ----------------- configurazione del livello [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;
}
// Configurazione di DispatcherServlet per le intestazioni CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
Abbiamo già incontrato, in un momento o nell’altro, tutti gli elementi di questa configurazione. Ricordiamo semplicemente che le righe 42-47 sono necessarie quando si desidera poter interrogare il server con richieste interdominio (CORS). Questo sarà il caso in questa occasione.
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";
// radice del servizio web / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// timeout in millisecondi
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
}
- riga 11: [AppConfig] importa la configurazione del livello [DAO] e del livello [web];
- righe 15-16: gli identificativi che consentiranno all’applicazione di effettuare un accesso all’avvio dell’applicazione per memorizzare nella cache medici e clienti;
- riga 18: l'URL del servizio web / jSON [Web1];
- riga 20: il timeout delle chiamate HTTP dell'applicazione;
- riga 22: un valore booleano per autorizzare o meno le chiamate tra domini;
Infine, in [application.properties], il server Tomcat è configurato per funzionare sulla porta 8081:
![]() |
server.port=8081
8.6.3. Le funzionalità dell'applicazione
Sono state descritte nel paragrafo 8.2. Le riportiamo ora. Tramite un browser, si richiede l’URL [http://localhost:8081/boot.html]:
![]() |
- [1], la pagina di accesso all’applicazione;
- in [2] e [3], rispettivamente l’ID e la password di chi desidera utilizzare l’applicazione. Ci sono due utenti: admin/admin (login/password) con un ruolo (ADMIN) e user/user con un ruolo (USER). Solo il ruolo ADMIN ha il diritto di utilizzare l'applicazione. Il ruolo USER serve solo a mostrare 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: il francese (impostazione predefinita) e l’inglese;
- in [6], l'URL del server [rdvmedecins-springthymeleaf-server];
![]() |
- in [1], si effettua l'accesso;
![]() |
- una volta effettuato l'accesso, è possibile scegliere il medico con cui si desidera fissare un appuntamento [2] e il giorno dell'appuntamento [3]. Non appena vengono inseriti il medico e il giorno, viene visualizzato automaticamente il calendario:
![]() |
- una volta visualizzato il calendario del medico, è possibile prenotare una fascia oraria [5];
![]() |
- in [6], si seleziona il paziente per l’appuntamento e si conferma la scelta in [7];
![]() |
Una volta confermato l'appuntamento, si torna automaticamente all'agenda dove il nuovo appuntamento è ora registrato. Questo appuntamento potrà essere successivamente cancellato con [8].
Le funzionalità principali sono state descritte. Sono semplici. Concludiamo con la gestione della lingua:
![]() |
- in [1], si passa dal francese all’inglese;
![]() |
- in [2], la visualizzazione passa all’inglese, compreso il calendario;
8.6.4. Fase 1: introduzione al framework CSS Bootstrap
![]() |
Nel client web sopra riportato, le pagine HTML utilizzeranno il framework CSS Bootstrap [http://getbootstrap.com/] che presentiamo ora.
8.6.4.1. Il progetto degli esempi
Il formato degli esempi sarà il seguente:
![]() |
- in [1]: il progetto nella sua interezza;
- in [2]: i codici 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] è quello di 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 /> <!-- ricerca del genitore dal 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 controller [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 integrato:
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">
<!-- Jumbotron di Bootstrap -->
<div th:include="jumbotron"></div>
<!-- contenuto -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- errore -->
<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 locale CSS;
- riga 13: visualizza [1];
- righe 19-21: visualizzano [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, ad esempio [alert-info, alert-warning,...];
Il jumbotron [1] viene generato dalla vista [jumbotron.xml] seguente:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Jumbotron Bootstrap -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1>
Les Médecins
<br />
associés
</h1>
</div>
</div>
</div>
</section>
- riga 4: l'area ha la classe CSS [jumbotron];
- riga 5: la classe [row] definisce una riga a 12 colonne;
- riga 6: la classe [col-md-2] definisce un'area a due colonne nella riga;
- riga 7: in queste due colonne si inserisce un'immagine;
- righe 9-15: nelle altre 10 colonne si inserisce il testo;
8.6.4.3. Esempio n. 2: la barra di navigazione
L'azione [/bs-02] visualizza la vista [bs-02.xml] seguente:
![]() |
La novità è la barra di navigazione [1] con il suo modulo di inserimento dati e i suoi 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" />
<!-- script JS -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/js/bs-02.js"></script>
</head>
<body id="body">
<div class="container">
<!-- barra di navigazione -->
<div th:include="navbar1"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- contenuto -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- informazioni -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 10: si importa jQuery;
- riga 11: uno script locale JS;
- riga 16: la barra di navigazione;
La barra di navigazione è generata dalla vista [navbar1.xml] 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" />
<!-- modulo di identificazione -->
<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] definirà lo stile della barra di navigazione. La classe [navbar-inverse] le assegna lo sfondo nero. La classe [navbar-fixed-top] farà in modo 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 tipicamente di una serie di classi che non capisco. Utilizzo il componente così com’è;
- righe 14-26: definiscono un'area "responsive" della barra di comando. Su uno smartphone, quest'area scompare in un'area di menu;
- riga 15: un'immagine attualmente nascosta;
- righe 17-25: la classe [navbar-form] definisce lo stile di un modulo della barra di comando. La classe [navbar-right] lo posiziona a destra di quest’ultimo;
- righe 21-23: i due campi di immissione del modulo della riga 17 [2]. Si trovano all’interno di una classe [form-group] che definisce gli elementi di un modulo e ciascuna di esse 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 fa clic sul pulsante [Connexion], 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 con elenco
L'azione [/bs-03] visualizza la vista [bs-03.xml] seguente:
![]() |
- La novità è il menu a tendina [1], noto anche come "dropdown";
Il codice della 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>
<!-- script locale -->
<script type="text/javascript" src="resources/js/bs-03.js"></script>
</head>
<body id="body">
<div class="container">
<!-- barra di navigazione -->
<div th:include="navbar2"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- contenuto -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- informazioni -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 11: il pulsante a lista 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" />
<!-- modulo di identificazione -->
<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>
<!-- lingue -->
<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>
<!-- pagina iniziale -->
<script th:inline="javascript">
/*<![CDATA[*/
// inizializzazione della pagina
initNavBar2();
/*]]>*/
</script>
</section>
- righe 25-40: definiscono il pulsante con elenco;
- riga 27: la classe [btn-danger] gli conferisce il colore rosso;
- righe 32-39: gli elementi dell'elenco. Si tratta di link, ciascuno associato a una funzione JS;
- righe 46-51: uno script JS eseguito dopo il caricamento del documento;
Lo script JS [bs-03.js] è il seguente:
function initNavBar2() {
// menu a tendina delle lingue
$('.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 [dropdown]. [$('.dropdown-toggle')] individua l'elemento con la classe [dropdown-toggle]. Si tratta del pulsante con elenco (riga 28 della vista). A esso viene applicata la funzione JS [dropdown()], definita nel file JS [bootstrap.js]. Solo dopo questa operazione il pulsante si comporta come un pulsante con elenco;
- righe 10-21: la funzione eseguita quando si seleziona una lingua;
Ecco un esempio:

8.6.4.5. Esempio n. 4: un menu
L'azione [/bs-04] visualizza la vista [bs-04.xml] seguente:
![]() |
È 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>
<!-- script locale -->
<script type="text/javascript" src="resources/js/bs-04.js"></script>
</head>
<body id="body">
<div class="container">
<!-- barra di navigazione -->
<div th:include="navbar3"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- contenuto -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- informazioni -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 18: si inserisce 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>
<!-- pulsanti a destra -->
<div class="navbar-form navbar-right" role="form">
<!-- disconnessione -->
<button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- lingue -->
<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>
<!-- pagina iniziale -->
<script th:inline="javascript">
/*<![CDATA[*/
// inizializzazione della pagina
initNavBar3();
/*]]>*/
</script>
</section>
- righe 16-29: creano il menu con quattro opzioni, ciascuna delle quali è 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() {
// menu a tendina delle lingue
$('.dropdown-toggle').dropdown();
// l'immagine animata
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) {
// i link del menu
var lnkAfficherAgenda = $("#lnkAfficherAgenda");
var lnkAccueil = $("#lnkAccueil");
var lnkValiderRv = $("#lnkValiderRv");
var lnkRetourAgenda = $("#lnkRetourAgenda");
// li si inserisce in un dizionario
var options = {
"lnkAccueil" : lnkAccueil,
"lnkAfficherAgenda" : lnkAfficherAgenda,
"lnkValiderRv" : lnkValiderRv,
"lnkRetourAgenda" : lnkRetourAgenda
}
// si nascondono tutti i link
for ( var key in options) {
options[key].hide();
}
// si visualizzano quelli richiesti
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 con l'elenco delle lingue;
- righe 6-7: l'immagine animata è nascosta;
- righe 26-48: una funzione [setMenu] che permette di indicare quali opzioni devono essere visibili;
Andiamo nella console di sviluppo (Ctrl-Maiusc-I) e inseriamo il seguente codice [1]:
![]() |
Poi torniamo al browser. Il menu è cambiato [2]:
8.6.4.6. Esempio n. 5: un menu a tendina
L’azione [/bs-05] visualizza la seguente vista [bs-05.xml]:
![]() |
La novità è in [1]. Qui utilizziamo un componente fornito al di fuori di Bootstrap, [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].
Il codice della 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>
<!-- script locale -->
<script type="text/javascript" src="resources/js/bs-05.js"></script>
</head>
<body id="body">
<div class="container">
<!-- barra di navigazione -->
<div th:include="navbar3"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- contenuto -->
<div id="content" th:include="choixmedecin">
</div>
<!-- informazioni -->
<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>
<!-- script locale -->
<script th:inline="javascript">
/*<![CDATA[*/
// si inizializza la pagina
initChoixMedecin();
/*]]>*/
</script>
</section>
- righe 7-12: si tratta di un classico tag [select], 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() {
// il menu a tendina dei medici
$('#idMedecin').selectpicker();
// il 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 menu a tendina 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 JS viene eseguita quando si fa clic sull’opzione di menu [Agenda];
- riga 3: si recupera il valore dell'opzione selezionata nell'elenco a discesa: [$('#idMedecin option:selected')] individua innanzitutto il componente [id=idMedecin] e poi, all'interno di tale 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 vista [bs-06.xml] seguente:

La selezione di un medico o di una data attiva una funzione JS che visualizza sia il medico che la data selezionati. Ecco un esempio:
![]() |
Grazie al pulsante «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>
<!-- script locale -->
<script type="text/javascript" src="resources/js/bs-06.js"></script>
</head>
<body id="body">
<div class="container">
<!-- barra di navigazione -->
<div th:include="navbar3"></div>
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron"></div>
<!-- contenuto -->
<div id="content" th:include="choixmedecinjour">
</div>
<!-- informazioni -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 8: il file CSS del componente [bootstrap-datepicker];
- riga 16: il file JS del 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 di una libreria denominata [moment] che consente di accedere 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>
<!-- script locale -->
<script th:inline="javascript">
/*<![CDATA[*/
// si inizializza la pagina
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. Si eliminerà quindi il contenuto del componente [id="calendar_container"] e vi si inserirà 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() {
// calendario
var calendar_container = $("#calendar_container");
calendar_infos = {
"container" : calendar_container,
"html" : calendar_container.html(),
"today" : moment().format('YYYY-MM-DD'),
"langue" : "fr"
}
// creazione calendario
updateCalendar();
// il menu a tendina dei medici
$('#idMedecin').selectpicker();
$('#idMedecin').change(function(e) {
afficherAgenda();
})
// il menu
setMenu([]);
}
- riga 2: il calendario è gestito da diverse funzioni JS. La variabile [calendar_infos] raccoglierà le informazioni relative al calendario. È una variabile globale, in modo da essere visibile alle diverse funzioni;
- riga 6: si identifica il contenitore del calendario;
- righe 7-12: le informazioni memorizzate relative al calendario;
- riga 8: un riferimento al suo contenitore,
- riga 9: il codice HTML del calendario. Con queste due informazioni, è possibile eliminare il calendario e rigenerarlo,
- riga 10: la data odierna nel formato [aaaa-mm-jj],
- 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 cambierà, verrà eseguito il metodo [afficherAgenda];
- riga 21: nessun menu nella barra di navigazione;
La funzione [updateCalendar] è la seguente:
function updateCalendar(renew) {
if (renew) {
// aggiornamento del calendario attuale
calendar_infos.container.html(calendar_infos.html);
}
// inizializzazione del calendario
var calendar = $("#calendar");
var settings = {
format : "yyyy-mm-dd",
startDate : calendar_infos.today,
language : calendar_infos.langue,
};
calendar.datepicker(settings);
// selezione della data corrente
if (calendar_infos.date) {
calendar.datepicker('setDate', calendar_infos.date)
}
// eventi
calendar.datepicker().on('hide', function(e) {
// visualizzazione del giorno selezionato
displayJour();
});
calendar.datepicker().on('changeDate', function(e) {
// si annota la nuova data
calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
// visualizzazione informazioni agenda
afficherAgenda();
// visualizzazione del giorno selezionato
displayJour();
});
// visualizzazione del giorno selezionato
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: si fa riferimento al calendario;
- righe 8-12: i suoi parametri di inizializzazione;
- riga 9: il formato delle date gestite da [aaaa-mm-jj],
- riga 10: la prima data selezionabile nel calendario. In questo caso, la data odierna. Le date precedenti non potranno essere selezionate,
- riga 11: la lingua del calendario. Ce ne saranno due: ['en'] e ['fr'];
- riga 13: il calendario è configurato;
- righe 15-17: se è stata inizializzata la data [calendar_infos], allora si imposta tale data come data corrente del calendario;
- righe 19-22: ogni volta che il calendario si chiuderà, verrà visualizzata la data selezionata;
- righe 23-30: ogni volta che si verifica un cambiamento di data nel calendario:
- riga 25: si registra la data selezionata in [calendar_infos],
- riga 27: si visualizzano le informazioni sull'agenda,
- riga 29: viene visualizzato il giorno selezionato;
- riga 32: visualizzazione del giorno selezionato, se presente;
Il metodo [displayJour] che visualizza il giorno selezionato è il seguente:
// visualizza il giorno selezionato
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 (all'inizio il calendario non ha una data selezionata);
- riga 4: si individua il componente in cui verrà inserita la data;
- riga 5: questa data può essere scritta in inglese o in francese. Si imposta la lingua della libreria [moment];
- riga 6: si visualizza la data selezionata nella lingua scelta e nel formato lungo;
- riga 7: questa data viene visualizzata;
Ecco due esempi:
![]() | ![]() |
Quando si cambia medico o data, viene eseguito il metodo [afficherAgenda]:
function afficherAgenda() {
// vengono visualizzati il medico e la data
var idMedecin = $('#idMedecin option:selected').val();
if (calendar_infos.date) {
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin + " et le jour " + calendar_infos.date);
}
}
8.6.4.8. Esempio n. 7: una tabella HTML "responsive"
Nota: «responsive» è un termine inglese 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 novità è rappresentata dalla tabella HTML [1]. Questa tabella è gestita dalla libreria JS [footable]: [https://github.com/fooplugins/FooTable].
Se si riduce la dimensione della finestra del browser, si ottiene quanto segue:
![]() |
- la tabella HTML si è adattata alle dimensioni dello schermo;
- in [1], per visualizzare il link [Réserver], è necessario cliccare sul simbolo [+];
- in [2], ciò che si vede quando si clicca sul simbolo [+];
La schermata [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>
<!-- script locale -->
<script type="text/javascript" src="resources/js/bs-07.js"></script>
</head>
<body id="body">
<div class="container">
<!-- barra di navigazione -->
<div th:include="navbar3" />
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron" />
<!-- contenuto -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda" />
<!-- informazioni -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- riga 10: il CSS della libreria [footable];
- riga 19: il JS della libreria [footable];
- riga 31: la tabella HTML di un'agenda;
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>
<!-- pagina iniziale -->
<script th:inline="javascript">
/*<![CDATA[*/
// si inizializza la pagina
initAgenda();
/*]]>*/
</script>
</body>
</html>
- riga 4: inserisce la tabella in una riga [row] e un riquadro colorato [alert alert-danger];
- riga 5: la tabella occuperà 6 colonne [col-md-6];
- riga 6: la tabella HTML è formattata con Bootstrap [class='table'];
- riga 9: l’attributo [data-toggle] indica la colonna che ospita il simbolo [+/-] che espande/comprime la riga;
- riga 15: l'attributo [data-hide='phone'] indica 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: si associa una funzione JS al link [Réserver];
- riga 46: si associa una funzione JS al link [Supprimer];
- righe 56-61: inizializzazione della pagina;
Alcune delle classi CSS utilizzate sopra provengono dal file CSS [bootstrapDemo.css]:
@CHARSET "UTF-8";
#intervalli th {
text-align: center;
}
#intervalli 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] trovato sul sito della biblioteca.
Nel file JS [bs-07.js], la pagina viene inizializzata come segue:
function initAgenda() {
// la tabella degli intervalli orari
$("#creneaux").footable();
}
Questo è tutto. [$("#creneaux")] fa riferimento alla tabella HTML che si desidera rendere «responsive». Inoltre, si trovano le funzioni JS associate ai due link [Réserver] e [Supprimer]:
function reserver(idCreneau) {
showInfo("Réservation du créneau n° " + idCreneau);
}
function supprimer(idRv) {
showInfo("Suppression du rv n° " + idRv);
}
8.6.4.9. Esempio n. 8: una finestra modale
L’azione [/bs-08] visualizza la vista [bs-08.xml] seguente:

Mentre in precedenza, cliccando sul link [Réserver] veniva visualizzata un'informazione nella finestra informativa, in questo caso verrà visualizzata una finestra modale per selezionare un cliente per il RV:

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>
<!-- script locale -->
<script type="text/javascript" src="resources/js/bs-08.js"></script>
</head>
<body id="body">
<div class="container">
<!-- barra di navigazione -->
<div th:include="navbar3" />
<!-- Jumbotron Bootstrap -->
<div th:include="jumbotron" />
<!-- contenuto -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda-modal" />
<div th:include="resa" />
<!-- informazioni -->
<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 collegamento [Réserver]:
<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>
La funzione [showDialogResa] ha il compito di visualizzare la finestra modale di selezione di un cliente;
- riga 33: la vista [resa.xml] è la finestra modale di 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">Titolo della finestra modale</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 -->
<!-- pagina iniziale -->
<script th:inline="javascript">
/*<![CDATA[*/
// si inizializza la pagina
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 [Annuler] gestito dalla funzione JS [cancelDialogResa];
- riga 33: un pulsante [Valider] gestito dalla funzione JS [validateResa];
- righe 39-44: lo script di inizializzazione della finestra modale;
Il risultato è la seguente visualizzazione:
![]() |
Da notare che la finestra modale non viene visualizzata per impostazione predefinita. Per questo motivo, non è visibile all’avvio dell’applicazione, sebbene il suo codice HTML sia presente nel documento.
Il file JS [bs-08.js] è il seguente:
var idCreneau;
var idClient;
var resa;
function showDialogResa(idCreneau) {
// si memorizza l'ID della fascia oraria
this.idCreneau = idCreneau;
// viene visualizzata la finestra di dialogo di prenotazione
var resa = $("#resa");
resa.modal('show');
// log
showInfo("Réservation du créneau n° " + idCreneau);
}
function cancelDialogResa() {
// si nasconde la finestra di dialogo
resa.modal('hide');
}
// conferma della prenotazione
function validateResa() {
// si recuperano le informazioni
var idClient = $('#idClient option:selected').val();
// si nasconde la finestra di dialogo
resa.modal('hide');
// informazioni
showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
function initResa() {
// il menu a tendina dei clienti
$('#idClient').selectpicker();
// finestra modale
resa = $("#resa");
resa.modal({});
}
- righe 30-36: la funzione di inizializzazione della finestra modale;
- riga 32: la finestra modale contiene un menu a tendina che deve essere inizializzato;
- righe 34-35: inizializzazione della finestra modale stessa;
- righe 5-13: la funzione JS associata al link [Réserver];
- riga 7: si memorizza il parametro della funzione nella variabile globale della riga 1;
- righe 9-10: la finestra modale viene resa visibile;
- riga 12: si registra un'informazione nella finestra di informazioni;
- righe 15-18: gestione del pulsante [Annuler]. Ci si limita a nascondere la finestra modale (riga 17);
- righe 21-31: la funzione JS associata al pulsante [Valider];
- riga 23: si recupera l'attributo [value] del cliente selezionato;
- riga 25: si nasconde la finestra di dialogo;
- riga 27: si registrano le due informazioni: il numero della fascia oraria prenotata e il nome del cliente;
8.6.5. Fase 2: scrittura delle viste
Descriveremo ora le viste fornite dal server [Web1] e i relativi modelli.
![]() |
8.6.5.1. La vista [navbar-start]
Visualizza la barra di navigazione della pagina iniziale:

Il codice di [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" />
<!-- modulo di identificazione -->
<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>
<!-- lingue -->
<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>
<!-- pagina iniziale -->
<script th:inline="javascript">
/*<![CDATA[*/
// inizializzazione della pagina
initNavBarStart();
/*]]>*/
</script>
</section>
Questa vista non ha un modello. Dispone dei seguenti gestori di eventi:
evt | gestore |
clic sul pulsante di connessione | |
clic sul link [Français] | |
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">
<!-- Jumbotron di Bootstrap -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1 th:utext="#{application.header}" />
</div>
</div>
</div>
</section>
La vista [jumbotron] non ha né modelli né eventi.
8.6.5.3. La vista [login]
È 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é modelli né eventi.
8.6.5.4. La vista [navbar-run]
Questa è la barra di navigazione visualizzata quando l'accesso è andato a buon fine:

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" />
<!-- pulsanti a destra -->
<form class="navbar-form navbar-right" role="form">
<!-- disconnessione -->
<button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- lingue -->
<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>
<!-- inizializzazione pagina -->
<script th:inline="javascript">
/*<![CDATA[*/
// si sta inizializzando la pagina
initNavBarRun();
/*]]>*/
</script>
</section>
Questa vista non ha un modello. Dispone dei seguenti gestori di eventi:
evt | gestore |
clic sul pulsante di disconnessione | |
clic sul link [Français] | |
clic sul link [English] |
8.6.5.5. La vista [accueil]
Questa è la vista visualizzata immediatamente sotto la barra di navigazione [navbar-run]:

Il suo codice [accueil.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>
<!-- script locale -->
<script th:inline="javascript">
/*<![CDATA[*/
// si inizializza la pagina
initChoixMedecinJour();
/*]]>*/
</script>
</html>
Il suo modello è il seguente:
- [rdvmedecins.medecinItems] (riga 8): l'elenco dei medici;
Nella sua forma attuale, la vista non sembra disporre di un gestore di eventi. In realtà questi sono definiti nella funzione [initChoixMedecinJour]. Questa funzione è stata presentata al paragrafo 8.6.4.7, a pagina 466 e in particolare a pagina 469. Vi si trovano i seguenti gestori di eventi:
evt | gestore |
scelta di un medico | |
scelta di una data |
8.6.5.6. La vista [agenda]
La vista [agenda] mostra una giornata dell'agenda di un medico:

Il suo 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>
<!-- prenotazione -->
<section th:include="resa" />
</th:block>
<!-- inizializzazione della pagina -->
<script th:inline="javascript">
/*<![CDATA[*/
// si inizializza la pagina
initAgenda();
/*]]>*/
</script>
</body>
</html>
Il modello di questa vista contiene un solo elemento:
- [agenda] (riga 4): un modello piuttosto complesso creato appositamente per la visualizzazione dell'agenda;
Dispone dei seguenti gestori di eventi:
evt | gestore |
clic sul pulsante [Supprimer] | |
clic sul link [Réserver] |
La vista [resa] della riga 47 è quella visualizzata quando l'utente clicca su un link [Réserver]:

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">Titolo modale</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 -->
<!-- inizializza la pagina -->
<script th:inline="javascript">
/*<![CDATA[*/
// si inizializza la pagina
initResa();
/*]]>*/
</script>
</body>
</html>
Il suo modello contiene un solo elemento:
- [clientItems] (riga 24): l'elenco dei clienti;
Dispone dei seguenti gestori di eventi:
evt | gestore |
clic sul pulsante [Annuler] | |
clic sul pulsante [Valider] |
8.6.5.7. La vista [erreurs]
Questa è la vista che viene visualizzata se l'azione richiesta dall'utente non è andata a buon fine:

Il codice [erreurs.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 modello contiene un solo elemento:
- [erreurs] (riga 8): l'elenco degli errori da visualizzare;
La vista non dispone di un gestore di eventi.
8.6.5.8. Résumé
La tabella seguente riporta le viste e i relativi modelli:
vista | modello | gestori di eventi |
navbar-start | ||
jumbotron | ||
login | ||
navbar-run | ||
home | ||
agenda | ||
Prenotazione | ||
errori |
8.6.6. Fase 3: scrittura delle azioni
Torniamo all'architettura del servizio web [Web1]:
![]() |
Vedremo ora quali URL sono esposte da [Web1] e la loro implementazione:
8.6.6.1. Le URL esposte dal servizio [Web1]
Sono le seguenti:
- una URL per ciascuna delle viste precedenti o una loro combinazione;
- una URL per aggiungere una RV;
- una URL per eliminare un RV;
Tutte restituiscono una risposta del tipo [Reponse] come segue:
public class Reponse {
// ----------------- proprietà
// stato dell'operazione
private int status;
// la barra di navigazione
private String navbar;
// il jumbotron
private String jumbotron;
// il corpo della pagina
private String content;
// l'agenda
private String agenda;
...
}
- riga 5: uno stato della risposta: 1 (OK), 2 (errore);
- riga 7: il flusso HTML delle viste [navbar-start] o [navbar-run] a seconda dei casi;
- riga 9: il flusso HTML della vista [jumbotron];
- riga 13: il flusso HTML della vista [agenda];
- riga 9: il flusso HTML delle viste [accueil], [erreurs], [login] a seconda dei casi;
Le URL riportate sono le seguenti
inserisce la vista [navbar-start] in [Reponse.navbar] | |
inserisce la vista [navbar-run] in [Reponse.navbar] | |
inserisce la vista [accueil] in [Reponse.content] | |
inserisce la vista [jumbotron] in [Reponse.jumbotron] | |
inserisce la vista [agenda] in [Reponse.agenda] | |
inserisce la vista [login] in [Reponse.content] | |
| |
inserisce la vista [navbar-run] in [Reponse.navbar], la vista [jumbotron] in [Reponse.jumbotron], la vista [accueil] nella vista [Reponse.content], la vista [agenda] nella vista [Reponse.agenda] | |
aggiunge l'appuntamento selezionato e inserisce la nuova agenda in [Reponse.agenda] | |
elimina l'appuntamento selezionato e inserisce il nuovo calendario in [Reponse.agenda] |
8.6.6.2. Il singleton [ApplicationModel]
![]() |
La classe [ApplicationModel] viene istanziata in un unico esemplare 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 affinché 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 {
// il livello [DAO]
@Autowired
private IDao dao;
// la configurazione
@Autowired
private AppConfig appConfig;
// dati provenienti dal livello [DAO]
private List<ClientItem> clientItems;
private List<MedecinItem> medecinItems;
// dati di configurazione
private String userInit;
private String mdpUserInit;
private boolean corsAllowed;
// eccezione
private RdvMedecinsException rdvMedecinsException;
// produttore
public ApplicationModel() {
}
@PostConstruct
public void init() {
// configurazione
userInit = appConfig.getUSER_INIT();
mdpUserInit = appConfig.getMDP_USER_INIT();
dao.setTimeout(appConfig.getTIMEOUT());
dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
corsAllowed = appConfig.isCORS_ALLOWED();
// si memorizzano nella cache gli elenchi a discesa dei medici e dei clienti
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) {
// si creano gli elementi degli elenchi a discesa
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));
}
}
}
// getter e setter
...
// implementazione dell'interfaccia [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: iniezione del riferimento all’implementazione del livello [DAO]. È poi questo riferimento che viene 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: si memorizzano nella cache le informazioni che alimenteranno gli elenchi a discesa dei medici e dei clienti. Si parte quindi dal presupposto che, se un medico o un cliente cambia, l’applicazione debba essere riavviata. L’idea qui è quella di mostrare 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 {
// elemento di un elenco
private Long id;
private String texte;
// costruttore
public PersonneItem() {
}
public PersonneItem(Personne personne) {
id = personne.getId();
texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
}
// getter e setter
...
}
- riga 8: il campo [id] sarà il valore dell'attributo [value] di un'opzione dell'elenco a discesa;
- riga 9: il campo [texte] 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. In essa sono stati raggruppati i metodi di utilità della classe [RdvMedecinsController], che non sono fondamentali tranne uno. È possibile classificarli in tre gruppi:
- i metodi di utilità;
- i metodi che rendono le viste fuse con i relativi modelli;
- il metodo di inizializzazione di un'azione
| due metodi di utilità che forniscono un elenco di messaggi di errore. Li abbiamo già incontrati e utilizzati; |
| genera la vista [accueil] senza modello |
| restituisce la vista [agenda] e il relativo modello |
| restituisce la vista [login] senza modello |
| restituisce la risposta al cliente quando l'azione richiesta si è conclusa con un errore |
| Il metodo di inizializzazione di tutte le azioni del controller [RdvMedecinsController] |
Esaminiamo due di questi metodi.
Il metodo [getPartialViewAgenda] genera la vista più complessa, quella dell’agenda. Il suo codice è il seguente:
// flusso [agenda]
protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
// contesti
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
// si costruisce il modello della pagina [agenda]
ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
// l'agenda con il relativo modello
thymeleafContext.setVariable("agenda", modelAgenda);
thymeleafContext.setVariable("clientItems", application.getClientItems());
return engine.process("agenda", thymeleafContext);
}
- righe 9-10: i due elementi del modello dell'agenda:
- riga 9: l'agenda visualizzata.
- riga 10: l’elenco dei clienti visualizzato quando l’utente fissa un appuntamento;
Il metodo [setModelforAgenda] della riga 7 è il seguente:
// modello della pagina [Agenda]
private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
// il titolo della pagina
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);
// gli orari disponibili per la prenotazione
ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
int i = 0;
for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
// fascia oraria del medico
Creneau créneau = creneauMedecinJour.getCreneau();
ViewModelCreneau modelCréneau = new ViewModelCreneau();
modelCréneaux[i] = modelCréneau;
// ID
modelCréneau.setId(créneau.getId());
// fascia oraria
modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
créneau.getHfin(), créneau.getMfin()));
Rv rv = creneauMedecinJour.getRv();
// cliente e ordine
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);
}
// fascia oraria successiva
i++;
}
// si restituisce il modello dell'agenda
ViewModelAgenda modelAgenda = new ViewModelAgenda();
modelAgenda.setTitre(titre);
modelAgenda.setCreneaux(modelCréneaux);
return modelAgenda;
}
- riga 6: l'agenda ha un titolo:

oppure:

Si nota che il formato della data dipende dalla lingua. Questo formato viene recuperato dai file dei messaggi (riga 4).
- righe 11-40: per ogni fascia oraria, occorre visualizzare la vista:
![]()
oppure la vista:
![]()
- righe 19-20: visualizzano la fascia oraria;
- righe 25-28: nel caso in cui la fascia oraria sia libera. In tal caso, occorre visualizzare il pulsante [Réserver];
- righe 31-36: il caso in cui la fascia oraria sia occupata. In questo caso occorre visualizzare sia il cliente che il pulsante [Supprimer];
L’altro metodo su cui forniamo ulteriori spiegazioni è il metodo [getActionContext]. Viene chiamato all’inizio di ciascuna delle azioni di [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 eventuale elenco di 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 chiamata tra domini;
- [request]: la richiesta HTTP in fase di elaborazione, ciò che da tempo viene definita un'azione;
- [response]: la risposta che verrà fornita a tale richiesta;
- [result]: ogni azione di [RdvMedecinsController] riceve un valore inviato, di cui si verifica la validità. [result] è il risultato di tale verifica;
- [rdvMedecinsController]: il controller contenitore delle azioni;
Il metodo [getActionContext] è implementato come segue:
// contesto di un'azione
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
// lingua?
if (lang == null) {
lang = "fr";
}
// locale
Locale locale = null;
if (lang.trim().toLowerCase().equals("fr")) {
// francese
locale = new Locale("fr", "FR");
} else {
// tutto il resto in inglese
locale = new Locale("en", "US");
}
// intestazioni CORS
rdvMedecinsCorsController.sendOptions(origin, response);
// ActionContext
ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
// errori di inizializzazione
RdvMedecinsException e = application.getRdvMedecinsException();
if (e != null) {
actionContext.setErreurs(e.getMessages());
return actionContext;
}
// errori di POST?
if (result != null && result.hasErrors()) {
actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
return actionContext;
}
// nessun errore
return actionContext;
}
- righe 3-15: a partire dal parametro [lang], si imposta la locale dell’azione;
- riga 17: si inviano le intestazioni HTTP necessarie per le richieste interdominio. Non entriamo nei dettagli. La tecnica utilizzata è quella descritta nel paragrafo 8.4.14;
- riga 19: creazione di un oggetto [ActionContext] senza errori;
- riga 21: abbiamo visto al paragrafo 8.6.6.2 che il singleton [ApplicationModel] accedeva al database per recuperare sia i clienti che i medici. Tale accesso può fallire. Si memorizza quindi l’eccezione che si verifica. Alla riga 21, recuperiamo questa eccezione;
- righe 22-25: se si è verificata un’eccezione all’avvio dell’applicazione, non è possibile eseguire alcuna azione. Si restituisce quindi, per qualsiasi azione, un oggetto [ActionContext] contenente i messaggi di errore dell’eccezione;
- righe 27-20: si analizza il parametro [result] per verificare se il valore inviato fosse valido o meno. Se non era valido, si restituisce un oggetto [ActionContext] con i messaggi di errore appropriati;
- riga 32: caso senza errori;
Esaminiamo ora le azioni del controller [RdvMedecinsController]
8.6.6.4. L'azione [/getNavBarStart]
L’azione [/getNavBarStart] restituisce 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 tipo [Reponse] seguente:
public class Reponse {
// ----------------- proprietà
// stato dell'operazione
private int status;
// la barra di navigazione
private String navbar;
// il jumbotron
private String jumbotron;
// il corpo della pagina
private String content;
// l'agenda
private String agenda;
...
}
e presenta i seguenti parametri:
- [PostLang postlang]: il valore inviato è il seguente:
public class PostLang {
// data
@NotNull
private String lang;
...
}
La classe [PostLang] è la classe padre di tutti i valori inviati. Infatti, il cliente 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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// errori?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// si restituisce la vista [navbar-start]
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 degli errori, questi vengono inviati nella risposta al cliente (riga 12) con lo stato 2:
- righe 15-18: si invia la vista [navbar-start] con lo stato 1:
Di seguito descriviamo solo le novità.
8.6.6.5. L'azione [/getNavbarRun]
L'azione [/getNavBarRun] genera 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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// errori?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// si restituisce la vista [navbar-run]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
return reponse;
}
L'azione può restituire due tipi di risposta:
- la risposta con 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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// errori?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// si restituisce la vista [jumbotron]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
return reponse;
}
L'azione può restituire due tipi di risposta:
- la risposta con errore (righe 10-13):
- la risposta con la vista [jumbotron]:
8.6.6.7. L'azione [/getLogin]
L'azione [/getLogin] restituisce 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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// errori?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// viene restituita la vista [login]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
reponse.setContent(getPartialViewLogin(thymeleafContext));
return reponse;
}
L'azione può restituire due tipi di risposta:
- la risposta con errore (righe 9-11):
- la risposta con la vista [login]:
8.6.6.8. L'azione [/getAccueil]
L'azione [/getAccueil] restituisce la vista [accueil]. 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)
- alla riga 3, il valore inserito è di tipo [PostUser] come segue:
public class PostUser extends PostLang {
// dati
@NotNull
private User user;
...
}
- riga 1: la classe [PostUser] estende la classe [PostLang] e quindi include una lingua;
- riga 4: l'utente che desidera visualizzare 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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// errori?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// la vista [accueil] è protetta
try{
// utente
User user = postUser.getUser();
// si stanno verificando le credenziali [userName, password]
application.authenticate(user);
}catch(RdvMedecinsException e){
// viene restituito un errore
return getViewErreurs(thymeleafContext, e.getMessages());
}
// viene restituita la vista [accueil]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
- righe 15-22: si noti che la pagina [accueil] è protetta e che quindi l'utente deve essere autenticato;
L'azione può restituire due tipi di risposta:
- la risposta con errore (righe 11 e 21):
- la risposta con la vista [accueil] (righe 24-27):
8.6.6.9. L'azione [/getNavbarRunJumbotronAccueil]
L'azione [/getNavbarRunJumbotronAccueil] genera le viste [navbar-run, jumbotron, accueil]. 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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// errori?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// la vista [accueil] è protetta
try {
// utente
User user = postUser.getUser();
// si stanno verificando le credenziali [userName, password]
application.authenticate(user);
} catch (RdvMedecinsException e) {
// viene restituito un errore
return getViewErreurs(thymeleafContext, e.getMessages());
}
// si invia la risposta
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 risposta:
- la risposta con errore (righe 13, 23):
- la risposta con le viste [navbar-run, jumbotron, accueil] (righe 26-31):
8.6.6.10. L'azione [/getAgenda]
L'azione [/getAgenda] restituisce 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 inserito è di tipo [PostGetAgenda] come segue:
public class PostGetAgenda extends PostUser {
// dati
@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: il numero del medico di cui si desidera l'agenda;
- riga 8: il giorno dell'agenda desiderato;
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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result, rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
Locale locale = actionContext.getLocale();
// errori?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// si verifica la validità del POST
if (result != null) {
new PostGetAgendaValidator().validate(postGetAgenda, result);
if (result.hasErrors()) {
// si restituisce la vista [erreurs]
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
...
}
- fino alla riga 14, il codice è ormai classico;
- righe 16-21: si esegue un controllo aggiuntivo sul valore inviato. La data deve essere successiva o uguale a quella odierna. Per verificarlo si utilizza 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) {
// il giorno scelto per l'appuntamento
Date jour = null;
if (post instanceof PostGetAgenda) {
jour = ((PostGetAgenda) post).getJour();
} else {
if (post instanceof PostValiderRv) {
jour = ((PostValiderRv) post).getJour();
}
}
// si convertono le date nel formato yyyy-MM-dd
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String strJour = sdf.format(jour);
String strToday = sdf.format(new Date());
// il giorno scelto non deve essere precedente alla data odierna
if (strJour.compareTo(strToday) < 0) {
errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
}
}
}
- riga 19: il validatore opera su 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) {
...
// azione
try {
// agenda del medico
AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
// risposta
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException e1) {
// si restituisce la vista [erreurs]
return getViewErreurs(thymeleafContext, e1.getMessages());
} catch (Exception e2) {
// si torna alla vista [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- righe 9-10: con i parametri inviati via POST, si richiede l’agenda del medico;
- righe 12-13: si restituisce l'agenda:
- righe 17, 21: si restituisce una risposta con errori:
8.6.6.11. L'azione [/getNavbarRunJumbotronAccueilAgenda]
L'azione [/getNavbarRunJumbotronAccueilAgenda] restituisce le viste [navbar-run, jumbotron, accueil, agenda]. 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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// errori?
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;
}
// si invia la risposta
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: si sfrutta l'esistenza dell'azione [/getAgenda] per richiamarla. Successivamente si esamina il status della risposta (riga 16). Se si rileva un errore, non si procede oltre e si restituisce la risposta;
- riga 20: si inviano le viste richieste:
8.6.6.12. L'azione [/supprimerRv]
L'azione [/supprimerRv] 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 è del tipo [PostSupprimerRv] come segue:
public class PostSupprimerRv extends PostUser {
// dati
@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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// errori?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// valori inviati
User user = postSupprimerRv.getUser();
long idRv = postSupprimerRv.getIdRv();
// si elimina l'appuntamento
AgendaMedecinJour agenda = null;
try {
// lo si recupera
Rv rv = application.getRvById(user, idRv);
Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
long idMedecin = creneau.getIdMedecin();
Date jour = rv.getJour();
// si elimina l'appuntamento associato
application.supprimerRv(user, idRv);
// si rigenera l'agenda del medico
agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
// si restituisce la nuova agenda
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// si restituisce la vista [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// si restituisce la vista [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- riga 22: si recupera l’appuntamento da eliminare. Se non esiste, si genera un’eccezione;
- righe 23-25: a partire da questo appuntamento, si individuano il medico e il giorno in questione. Queste informazioni sono necessarie per rigenerare l’agenda del medico;
- riga 27: l’appuntamento viene eliminato;
- riga 29: si richiede il nuovo calendario del medico. Questo è importante. Oltre alla fascia oraria appena liberata, altri utenti dell’applicazione potrebbero aver apportato modifiche al calendario. È importante restituire all’utente la versione più recente dello stesso;
- righe 31-34: si restituisce l'agenda:
8.6.6.13. L'azione [/validerRv]
L'azione [/validerRv] aggiunge un appuntamento nell'agenda 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 inserito è del tipo [PostValiderRv] come segue:
public class PostValiderRv extends PostUser {
// dati
@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: il numero del cliente per il quale è stata effettuata la prenotazione;
- riga 10: il giorno dell'appuntamento;
L'implementazione dell'azione è la seguente:
// conferma di un appuntamento
@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) {
// contesti dell'azione
ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebApplicationContext springContext = actionContext.getSpringContext();
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// errori?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// si verifica la validità della data dell'appuntamento
if (result != null) {
new PostGetAgendaValidator().validate(postValiderRv, result);
if (result.hasErrors()) {
// si restituisce la vista [erreurs]
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
// valori inviati
User user = postValiderRv.getUser();
long idClient = postValiderRv.getIdClient();
long idCreneau = postValiderRv.getIdCreneau();
Date jour = postValiderRv.getJour();
// azione
try {
// si recuperano le informazioni sulla fascia oraria
Creneau créneau = application.getCreneauById(user, idCreneau);
long idMedecin = créneau.getIdMedecin();
// si aggiunge l'appuntamento
application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
// si aggiorna l'agenda
AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
new SimpleDateFormat("yyyy-MM-dd").format(jour));
// si restituisce la nuova agenda
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// si restituisce la vista [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// si restituisce la vista [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
}
Il codice è analogo a quello dell'azione [/supprimerRv].
8.6.7. Fase 4: test del server Spring/Thymeleaf
Ora testeremo le diverse azioni precedenti con il plugin Chrome [Advanced Rest Client] (cfr. paragrafo 9.6).
8.6.7.1. Configurazione dei test
Tutte le azioni richiedono un valore inviato tramite POST. Invieremo alcune varianti della stringa jSON seguente:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Questo valore inviato contiene informazioni superflue per la maggior parte delle azioni. Tuttavia, tali informazioni vengono ignorate dalle azioni che le ricevono e non causano errori. Questo valore inviato ha il vantaggio di coprire i diversi valori da inviare.
8.6.7.2. L'azione [/getNavbarStart]
![]() |
- in [1], l'azione testata;
- in [2], il valore inviato;
- in [3], il valore inserito è 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 (campi evidenziati).
Ora commettiamo un errore. Modifichiamo il valore dell’attributo [lang] da quello inviato a null. Riceviamo 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 il seguente valore inviato:
{"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 il seguente valore inviato:
{"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.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:
![]() |
Riproviamo 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 ottenuto è il seguente:
![]() |
Riproviamo con un utente esistente ma 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 ottenuto è 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:
![]() |
Riproviamo con una data precedente a oggi:
![]() |
Riproviamo 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 ottenuto è 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 ottenuto è il seguente:
![]() |
Lo stesso vale per un utente sconosciuto:
![]() |
8.6.7.9. L'azione [/getNavbarRunJumbotronAccueilAgenda]
Richiediamo l'azione [getNavbarRunJumbotronAccueilAgenda] 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:
![]() |
Inseriamo un medico che non esiste:
![]() |
8.6.7.10. L'azione [/supprimerRv]
Richiediamo l'azione [supprimerRv] 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 Rv n. 93 non esiste. Il risultato ottenuto è il seguente:
![]() |
Con un appuntamento esistente:
![]() |
È possibile verificare nel database che l’appuntamento sia stato effettivamente eliminato. Viene restituita la nuova agenda.
8.6.7.11. L'azione [/validerRv]
Chiediamo l'azione [validerRv] con il seguente valore registrato:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Il risultato ottenuto è il seguente:
![]() |
È possibile verificare nel database che l'appuntamento sia stato effettivamente creato. Il nuovo calendario è stato inviato.
Si esegue la stessa operazione con un numero di fascia oraria inesistente:
![]() |
Si ripete la stessa operazione con un numero di cliente inesistente:
![]() |
8.6.8. Fase 5: Scrittura del client JavaScript
Torniamo all’architettura del server [Web1]:
![]() |
Il client [2] del server [Web1] è un client JavaScript di tipo APU (applicazione a pagina singola):
- il client richiede la pagina di avvio a un server web (non necessariamente [Web1]);
- richiede le pagine successive al server [Web1] tramite chiamate Ajax;
Per realizzare questo client, utilizzeremo lo strumento [Webstorm] (cfr. paragrafo 9.8). Questo strumento mi è sembrato più pratico rispetto a STS. Il suo principale vantaggio è che offre il completamento automatico durante la digitazione del codice, oltre ad alcune opzioni di refactoring. Ciò consente di evitare numerosi errori.
8.6.8.1. Il progetto JS
Il progetto JS presenta la seguente struttura:
![]() |
- in [1], il client JS nel suo complesso. [boot.html] è la pagina iniziale. Sarà l’unica pagina caricata dal browser;
- in [2], i fogli di stile dei componenti Bootstrap;
- in [3], le poche immagini utilizzate dall'applicazione;
![]() |
- in [4], gli script JS. È qui che si concentra il nostro lavoro;
- in [5], le librerie JS utilizzate: principalmente jQuery e quelle dei componenti Bootstrap;
8.6.8.2. L'architettura del codice
Il codice è stato suddiviso in tre livelli:
![]() |
- il livello [présentation] raggruppa le funzioni di inizializzazione della pagina [boot.xml] e quelle dei vari componenti Bootstrap. È implementato dal file [ui.js];
- il livello [événements] raggruppa tutti i gestori di eventi del livello [présentation]. È implementato dal file [evts.js];
- il livello [DAO] invia le richieste HTTP al server [Web1]. È implementato dal file [dao.js];
8.6.8.3. Il livello [présentation]
![]() |
Il livello [présentation] è implementato dal seguente file [ui.js]:
//il livello [présentation]
var ui = {
// variabili globali;
"agenda": "",
"resa": "",
"langue": "",
"urlService": "http://localhost:8081",
"page": "login",
"jourAgenda": "",
"idMedecin": "",
"user": {},
"login": {},
"exceptionTitle": {},
"calendar_infos": {},
"erreur": "",
"idCreneau": "",
"done": "",
// componenti della vista
"body": "",
"navbar": "",
"jumbotron": "",
"content": "",
"exception": "",
"exception_text": "",
"exception_title": "",
"loading": ""
};
// il livello degli eventi
var evts = {};
// il livello [dao]
var dao = {};
// ------------ documento pronto
$(document).ready(function () {
// inizializzazione del documento
console.log("document.ready");
// componenti della pagina
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");
// si memorizza la pagina di accesso per poterla riprodurre
ui.login.lang = ui.langue;
ui.login.navbar = ui.navbar.html();
ui.login.jumbotron = ui.jumbotron.html();
ui.login.content = ui.content.html();
// URL del servizio
$("#urlService").val(ui.urlService);
});
// ------------------------ funzioni di inizializzazione dei componenti Bootstrap
ui.initNavBarStart = function () {
...
};
ui.initNavBarRun = function () {
...
};
ui.initChoixMedecinJour = function () {
...
};
ui.updateCalendar = function (renew) {
...
};
// visualizza il giorno selezionato
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 [présentation] (righe 2-27),
- [evts] per il livello di gestione degli eventi (riga 29),
- [dao] per il livello [DAO] (riga 31);
Questa suddivisione dei livelli in tre oggetti consente di 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 stesso.
- righe 38-44: si memorizzano i campi che saranno sempre presenti indipendentemente dalle viste visualizzate. Ciò evita di effettuare ricerche jQuery ripetitive e inutili;
- righe 46-49: si memorizza localmente la pagina di avvio per poterla ripristinare quando l’utente si disconnette e non ha cambiato lingua;
- righe 54-83: funzioni di inizializzazione dei componenti Bootstrap. Sono state tutte illustrate nell’analisi dedicata a tali componenti al paragrafo 8.6.4;
8.6.8.4. Le funzioni di utilità del livello [événements]
![]() |
I gestori di eventi sono stati inseriti nel file [evts.js]. Diverse funzioni vengono utilizzate regolarmente dai gestori di eventi. Le presentiamo ora:
// inizio dell'attesa
evts.beginWaiting = function () {
// inizio attesa
ui.loading = $("#loading");
ui.loading.show();
ui.exception.hide();
ui.erreur.hide();
evts.travailEnCours = true;
};
// fine attesa
evts.stopWaiting = function () {
// fine attesa
evts.travailEnCours = false;
ui.loading = $("#loading");
ui.loading.hide();
};
// visualizzazione del risultato
evts.showResult = function (result) {
// visualizzazione dei dati ricevuti
var data = result.data;
// si analizza lo stato
switch (result.status) {
case 1:
// errore?
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:
// visualizzazione errore
evts.showException(data);
break;
}
};
// ------------ funzioni varie
evts.showException = function (data) {
// visualizzazione errore
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 asincrona [DAO];
- righe 4-5: viene visualizzata l’immagine animata di attesa;
- righe 6-7: si nasconde l'area di visualizzazione degli errori e delle eccezioni (non sono la stessa cosa);
- riga 8: si segnala che è in corso un'operazione asincrona;
- riga 12: la funzione [evts.stopwaiting] viene chiamata dopo che un'azione asincrona [DAO] ha restituito il proprio risultato;
- riga 14: si nota che l'operazione asincrona è terminata;
- riga 15: si nasconde l’immagine animata di attesa;
- riga 20: la funzione [evts.showResult] visualizza il risultato [result] di un'azione asincrona [DAO]. Il risultato è un oggetto JS della forma seguente {'status':status,'data':data,'sendMeBack':sendMeBack}.
- righe 47-50: utilizzate se [result.status==2]. Ciò accade quando il server [Web1] invia una risposta con un'intestazione di errore HTTP (ad esempio 403 forbidden). In questo caso, [data] è la stringa jSON inviata dal server per segnalare 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] abbia inviato una risposta di errore {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':errori};
- righe 28-29: viene visualizzata la vista [erreurs];
- righe 31-33: eventuale visualizzazione della barra di navigazione;
- righe 34-36: eventuale visualizzazione del jumbotron;
- righe 37-39: eventuale visualizzazione del campo [data.content]. Rappresenta, a seconda dei casi, una delle viste [accueil, agenda];
- righe 40-43: se l'agenda è stata rigenerata, si recuperano alcuni riferimenti ai suoi componenti in modo da non doverli ricercare ogni volta che se ne avrà bisogno;
- riga 54: la funzione [evts.showException] ha lo scopo di visualizzare 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 attualmente impostata;
Il file [evts.js] contiene più di 300 righe di codice che non intendo commentare tutte. Mi limiterò a citare alcuni esempi per illustrare lo spirito di questo livello.
8.6.8.5. Accesso di un utente

La connessione di un utente è gestita dalla seguente funzione:
// ------------------------ connessione
evts.connecter = function () {
// si recuperano i valori da inviare
var login = $("#login").val().trim();
var passwd = $("#passwd").val().trim();
// si imposta l'URL del server
ui.urlService = $("#urlService").val().trim();
dao.setUrlService(ui.urlService);
// parametri della richiesta
var post = {
"user": {
"login": login,
"passwd": passwd
},
"lang": ui.langue
};
var sendMeBack = {
"user": {
"login": login,
"passwd": passwd
},
"caller": evts.connecterDone
};
// si esegue la richiesta
evts.execute([{
"name": "accueil-sans-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- righe 4-5: si recuperano il nome utente e la password dell’utente;
- righe 7-8: si recupera l'URL dal 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] è un oggetto che viene passato alla funzione [DAO] che verrà chiamata e che quest'ultima dovrà restituire alla funzione della riga 22. Qui l’oggetto [sendMeBack] incapsula l’utente che sta tentando di connettersi;
- righe 25-29: la funzione [evts.execute] è in grado di eseguire una sequenza di azioni asincrone. In questo caso, viene passato un elenco costituito da una sola azione. I campi di quest’ultima 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 proprio risultato;
Prima di approfondire la funzione [evts.execute], esaminiamo la funzione [evts.connecterDone] alla riga 22. È la funzione alla quale la funzione asincrona [DAO], una volta chiamata, deve restituire il proprio risultato:
evts.connecterDone = function (result) {
// visualizzazione del risultato
evts.showResult(result);
// Connessione riuscita?
if (result.status == 1 && result.data.status == 1) {
// pagina
ui.page = "accueil-sans-agenda";
// registrazione dell'utente
ui.user = result.sendMeBack.user;
}
};
- riga 3: viene visualizzato il risultato restituito dal server [Web1];
- riga 5: se questo risultato non contiene errori, si memorizzano il tipo della nuova pagina (riga 7) e l'utente autenticato (riga 9);
La funzione [evts.execute] esegue una serie di azioni asincrone:
// esecuzione di una sequenza di azioni
evts.execute = function (actions) {
// lavoro in corso?
if (evts.travailEnCours) {
// non si sta facendo nulla
return;
}
// in attesa
evts.beginWaiting();
// esecuzione delle azioni
dao.doActions(actions, evts.stopWaiting);
};
- riga 2: il parametro [actions] è un elenco di azioni asincrone da eseguire;
- righe 4-7: l'esecuzione è consentita solo se non ce n'è già un'altra in corso;
- riga 9: si avvia l'attesa;
- riga 11: si richiede al livello [DAO] di eseguire la sequenza di azioni. Il secondo parametro è il nome della funzione da eseguire quando tutte le azioni della sequenza avranno restituito il proprio risultato;
Non entreremo ora nei dettagli della funzione [dao.doActions]. Esamineremo un altro evento.
8.6.8.6. Cambio di lingua

Il cambio di lingua è gestito dalla seguente funzione:
// ------------------------ cambio di lingua
evts.setLang = function (lang) {
// cambio di lingua?
if (lang == ui.langue) {
// non si esegue alcuna operazione
return;
}
// nuova lingua
ui.langue = lang;
// quale pagina va tradotta?
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 si esegue alcuna operazione;
- riga 9: si memorizza la nuova lingua;
- righe 12-20: in caso di cambio di lingua, è necessario rigenerare la pagina attualmente visualizzata dal browser. Sono possibili tre pagine:
- quella denominata [login], in cui la pagina visualizzata è quella di autenticazione,
- quella denominata [accueil-sans-agenda], che è la pagina visualizzata subito dopo un'autenticazione riuscita,
- quella denominata [accueil-avec-agenda], che è la pagina visualizzata non appena viene visualizzata la prima agenda. Successivamente, rimane sempre in primo piano fino alla disconnessione dell’utente;
Esamineremo il caso della pagina [accueil-avec-agenda]. 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]
È la seguente funzione:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// parametri della richiesta
var post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
var sendMeBack = {
"caller": evts.getAccueilAvecAgendaDone
};
// richiesta
evts.execute([{
"name": "accueil-avec-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- righe 4-9: il valore da inviare incapsula l’utente connesso, la lingua desiderata, il numero del medico di cui si desidera l’agenda, il giorno dell’agenda desiderata;
- righe 10-12: l'oggetto [sendMeBack] è l'oggetto che verrà restituito alla funzione della riga 11. In questo caso, non contiene alcuna informazione;
- righe 14-18: esecuzione di una sequenza di azioni asincrone, denominata [accueil-avec-agenda] (riga 15);
- riga 11: la funzione eseguita quando l’azione asincrona [accueil-avec-agenda] avrà restituito il proprio risultato;
La funzione [evts.getAccueilAvecAgendaDone] alla riga 11 visualizza il risultato della funzione asincrona denominata [accueil-avec-agenda]:
evts.getAccueilAvecAgendaDone = function (result) {
// visualizzazione del risultato
evts.showResult(result);
// nuova pagina?
if (result.status == 1 && result.data.status == 1) {
ui.page = "accueil-avec-agenda";
}
};
- riga 1: [result] è il risultato della funzione asincrona denominata [accueil-avec-agenda];
- riga 3: viene visualizzato questo risultato;
- riga 5: se il risultato è corretto, si registra la nuova pagina (riga 6);
8.6.8.8. La funzione [ getAccueilAvecAgenda-parallel]
Si tratta della seguente funzione:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// azioni [navbar-run, jumbotron, accueil, agenda] 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,
'giorno': ui.jourAgenda,
"caller": evts.getAgendaDone
};
// esecuzione azioni in //
evts.execute([navbarRun, jumbotron, accueil, agenda])
};
- riga 51: questa volta si eseguono 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 quando l’azione asincrona [navbarRun] avrà restituito il risultato;
- righe 15-23: definizione dell'azione [jumbotron] che recupera la vista [jumbotron];
- riga 22: la funzione da eseguire quando l'azione asincrona [jumbotron] avrà restituito il risultato;
- righe 25-34: definizione dell'azione [accueil] che recupera la vista [accueil];
- riga 33: la funzione da eseguire quando l'azione asincrona [accueil] avrà restituito il risultato;
- righe 36-49: definizione dell'azione [agenda] che recupera la vista [jumbotron];
- riga 48: la funzione da eseguire quando l'azione asincrona [agenda] avrà restituito il risultato;
8.6.8.9. La funzione [ getAccueilAvecAgenda-sequence]
Si tratta della seguente funzione:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// azioni [navbar-run, jumbotron, accueil, agenda] in ordine
// agenda
var agenda = {
"name" : "agenda"
};
agenda.post = {
"user" : ui.user,
"lang" : ui.langue,
"idMedecin" : ui.idMedecin,
"jour" : ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin': ui.idMedecin,
'giorno: 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
};
// esecuzione sequenziale delle azioni
evts.execute([ navbarRun ])
};
- riga 54: si esegue l'azione [navbarRun]. Una volta terminata, si passa alla successiva: [jumbotron], riga 51. Anche questa azione viene quindi eseguita a sua volta. Una volta terminata, si passa alla successiva: [accueil], riga 40. Anche questa viene eseguita a sua volta. Una volta completata, si passa a quella successiva: [agenda], riga 29. Anche questa viene eseguita a sua volta. Una volta completata, ci si ferma poiché l’azione [agenda] non ha un’azione successiva.
8.6.8.10. Il livello [DAO]
![]() |
Il file [dao.js] raggruppa tutte le funzioni del livello [DAO]. Le presenteremo una alla volta:
// URL esposte dal 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"
};
// --------------- interfaccia
// URL del server
dao.setUrlService = function (urlService) {
dao.urlService = urlService;
};
- righe 16-18: la funzione che consente di impostare 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;
// ------------------ gestione generica delle azioni
// esecuzione di una sequenza di azioni asincrone
dao.doActions = function (actions, done) {
// elaborazione delle azioni
dao.actionsCount = actions.length;
dao.actionIndex = 0;
for (var i = 0; i < dao.actionsCount; i++) {
// richiesta asincrona DAO
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 quando tutte le azioni hanno restituito il proprio risultato;
- righe 7-12: le azioni asincrone vengono eseguite in parallelo. Tuttavia, nel caso in cui una di esse abbia un'azione successiva, quest'ultima viene eseguita al termine dell'azione che la precede;
- riga 9: si crea l’oggetto [Deferred] nello stato [pending];
- riga 10: quando questo oggetto passerà allo 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 parametro;
La funzione [dao.actionDone], che viene eseguita al termine di ogni azione asincrona, è la seguente:
// è stato ricevuto un risultato
dao.actionDone = function (result) {
// chiama?
var sendMeBack = result.sendMeBack;
if (sendMeBack && sendMeBack.caller) {
sendMeBack.caller(result);
}
// successivo?
if (sendMeBack && sendMeBack.next) {
// richiesta asincrona DAO
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
}
// finito?
dao.actionIndex++;
if (dao.actionIndex == dao.actionsCount) {
// fatto?
if (sendMeBack && sendMeBack.done) {
sendMeBack.done(result);
}
}
};
- riga 2: la funzione [dao.actionDone] riceve il risultato [result] da una delle azioni asincrone dell'elenco delle azioni da eseguire;
- righe 4-7: se l'azione asincrona completata aveva specificato una funzione a cui restituire il risultato, tale funzione viene chiamata;
- righe 9-14: se l'azione asincrona completata ha un'azione successiva, allora tale azione viene a sua volta eseguita;
- riga 16: un'azione è terminata. Si incrementa il contatore delle azioni completate. 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 il proprio risultato, allora tale funzione viene ora eseguita;
Il metodo [dao.doAction] esegue un'azione asincrona:
// esecuzione di un'azione
dao.doAction = function (deferred, action, done) {
// funzione "done" da incorporare nell'azione
if (action.sendMeBack) {
action.sendMeBack.done = done;
} else {
action.sendMeBack = {
"done": done
};
}
// esecuzione dell'azione
dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
- righe 4-10: come appena visto, la funzione che elaborerà il risultato dell'azione asincrona da eseguire deve avere accesso alla funzione [done]. A tal fine, si inserisce quest’ultima nell’oggetto [sendMeBack], oggetto che farà parte del risultato dell’operazione asincrona;
- riga 12: si esegue la funzione [dao.executePost] che effettua una chiamata HTTP al server [Web1]. L'URL di destinazione è l'URL associata al nome dell'azione da eseguire;
La funzione [dao.executePost] esegue una chiamata a HTTP:
// richiesta HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
// si effettua manualmente una chiamata Ajax
$.ajax({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
url: dao.urlService + url,
type: 'POST',
data: JSON3.stringify(post),
dataType: 'json',
success: function (data) {
// si restituisce il risultato
deferred.resolve({
"status": 1,
"data": data,
"sendMeBack": sendMeBack
});
},
error: function (jqXHR, textStatus, errorThrown) {
var data;
if (jqXHR.responseText) {
data = jqXHR.responseText;
} else {
data = textStatus;
}
// si restituisce l'errore
deferred.resolve({
"status": 2,
"data": data,
"sendMeBack": sendMeBack
});
}
});
};
Abbiamo già incontrato e commentato questa funzione. Si noti semplicemente, alla riga 9, che la funzione URL ha come obiettivo la concatenazione della funzione URL del server [Web1] con la funzione URL associata al nome dell'azione.
8.6.8.11. La pagina di avvio
![]() |

La pagina di avvio [boot.html] mostra la schermata sopra riportata. È l'unica pagina caricata direttamente dal browser. Le altre vengono recuperate tramite richieste 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"/>
<!-- Stili personalizzati per questo modello -->
<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>
<!-- script utente -->
<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"/>
<!-- Modulo di identificazione -->
<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>
<!-- lingue -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:evts.setLang('fr')">Français</a></li>
<li><a href="javascript:evts.setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Jumbotron Bootstrap -->
<div id="jumbotron">
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>
Cabinet médical<br/>Les Médecins associés
</h1>
</div>
</div>
</div>
</div>
<!-- messaggi di errore -->
<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>
<!-- contenuto -->
<div id="content">
<div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
</div>
</div>
<!-- pagina iniziale -->
<script>
// inizializzazione della pagina
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 dedicato a Bootstrap (paragrafo 8.6.4);
- righe 99-105: inizializzazione di alcuni elementi del livello [présentation];
- riga 27: viene utilizzato lo script [getAccueilAvecAgenda-sequence.js]. Modificando lo script in questa riga si ottengono tre comportamenti diversi per visualizzare la pagina [accueil-avec-agenda]:
- [getAccueilAvecAgenda-one.js] ottiene la pagina con una sola chiamata a HTTP,
- [getAccueilAvecAgenda-parallel.js] recupera la pagina con quattro chiamate simultanee a HTTP,
- [getAccueilAvecAgenda-sequence.js] recupera la pagina con quattro chiamate successive a HTTP;
8.6.8.12. Tests
Esistono diversi modi per eseguire i test. In questo caso utilizzeremo lo strumento [Webstorm]:
![]() |
- in [1] si apre un progetto. È sufficiente selezionare la cartella [2] contenente la struttura statica (HTML, CSS, JS) del sito da testare;
![]() |
- in [3], il sito statico;
- in [4-5], si carica la pagina [boot.html];
![]() |
- in [5], si vede che un server incorporato in [Webstorm] ha fornito la pagina [boot.html] dalla porta [63342]. È un aspetto importante da comprendere, poiché significa che gli script della pagina [boot.html] effettueranno chiamate interdominio al server [Web1], che a sua volta opera su [localhost:8081]. Il browser che ha caricato [boot.html] sa di averla caricata da [localhost:63342]. Non accetterà quindi che questa pagina effettui chiamate al sito [localhost:8081] perché non si tratta della stessa porta. Attuerà quindi le chiamate interdominio descritte al paragrafo 8.4.14. Per questo motivo, l’applicazione [Web1] deve essere configurata per accettare tali chiamate interdominio. 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";
// radice servizio web / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// timeout in millisecondi
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
Lasciamo al lettore il compito di eseguire i test sul client JS. Esso deve essere in grado di riprodurre le funzionalità descritte al paragrafo 8.6.3.
Una volta che il client JS è stato dichiarato corretto, è possibile distribuirlo nella cartella del server [Web1] per evitare di dover autorizzare le richieste interdominio:
![]() |
Come illustrato sopra, abbiamo copiato il sito testato nella cartella [src / main / resources / static]. Successivamente è possibile richiedere URL e [http://localhost:8081/boot.html]:

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


Se si accede alla console di sviluppo (Ctrl-Maiusc-I), viene visualizzata la causa dell’errore:

Si tratta di un errore di richiesta interdominio non autorizzata.
8.6.8.13. Conclusion
Abbiamo realizzato la seguente architettura JS:
![]() |
- i livelli sono ben distinti;
- abbiamo un'applicazione di tipo APU (applicazione a pagina singola). È proprio questa caratteristica che ora ci consentirà di generare un'applicazione nativa per diversi dispositivi mobili (Android, IoS, Windows Phone);
- abbiamo creato un modello in grado di eseguire azioni asincrone in parallelo, in sequenza o con una combinazione delle due;
8.6.9. Fase 6: generazione di un’applicazione nativa per Android
Lo strumento [Phonegap] [http://phonegap.com/] consente di produrre un eseguibile per dispositivi mobili (Android, IoS, Windows 8, ...) a partire da un'applicazione HTML / JS / CSS. Esistono diversi modi per raggiungere questo obiettivo. Noi utilizziamo il metodo più semplice: uno strumento disponibile online sul sito di Phonegap [http://build.phonegap.com/apps]. Questo strumento provvederà a caricare il file zip del sito statico da convertire. La pagina di avvio deve chiamarsi [index.html]. Rinominiamo quindi la pagina [boot.html] in [index.html]:
![]() |
quindi comprimiamo la cartella, in questo caso [rdvmedecins-client-js-03]. Successivamente accediamo al sito di Phonegap [http://build.phonegap.com/apps]:
![]() |
- prima di [1], potrebbe essere necessario creare un account;
- in [1], si avvia;
- in [2], si sceglie un piano gratuito che consente solo un’applicazione Phonegap;
![]() |
- in [3], si scarica l'applicazione compressa [4];
![]() |
- in [5], si assegna un nome all'applicazione;
- in [6], si procede alla compilazione. Questa operazione può richiedere 1 minuto. Attendere fino a quando le icone delle diverse piattaforme mobili indicano che la compilazione è terminata;
![]() |
- sono stati generati solo i file binari per Android ([7]) e Windows ([8]);
- fare clic su [7] per scaricare il file binario Android;
![]() |
- in [9] il file binario [apk] scaricato;
Avviare un emulatore [GenyMotion] per un tablet Android (vedere paragrafo 9.9):
![]() |
Nell'immagine sopra, si avvia un emulatore di tablet con Android API 19. Una volta avviato l'emulatore,
- sbloccarlo trascinando il cursore di sblocco (se presente) sul 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 sostituire URL con [1]. A tal fine, in una finestra di comando, digitate il comando [ipconfig] (riga 1 qui sotto) che visualizzerà i diversi indirizzi IP del vostro 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. . . :
...
Annotate l’indirizzo Wi-Fi IP (righe 6-9) oppure l’indirizzo sulla rete locale IP (righe 11-17). Quindi utilizzare questo indirizzo IP nel campo URL del server web:
![]() |
Fatto ciò, accedete al servizio web:
![]() |
Provate l'applicazione sull'emulatore. Dovrebbe funzionare. A livello di server, è possibile autorizzare o meno le intestazioni CORS nella classe [ApplicationModel]:
// CORS
private final boolean CORS_ALLOWED=false;
Questo non ha alcuna importanza per l'applicazione Android, poiché non viene eseguita in un browser. L'esigenza delle intestazioni CORS deriva infatti dal browser e non dal server.
8.6.10. Conclusioni del caso di studio
Abbiamo sviluppato la seguente architettura:
![]() |
Si tratta di un'architettura a tre livelli complessa. L'obiettivo era riutilizzare il livello [Web2], che era il livello server dell'applicazione [AngularJS-Spring MVC] del documento [Tutoriel AngularJS / Spring 4], per passare daURL e [http://tahe.developpez.com/angularjs-spring4/]. È solo per questo motivo che si è optato per un’architettura a tre livelli. Laddove 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 ci sarà una perdita di prestazioni.
L’applicazione qui analizzata è stata sviluppata nel corso del tempo in tre documenti diversi:
- da [Introduction aux frameworks JSF2, Primefaces et Primefaces mobile] a URL e [http://tahe.developpez.com/java/primefaces/]. Il caso di studio era stato quindi sviluppato con i framework JSF2 / Primefaces. Primefaces è una libreria di componenti AJAX che evita di scrivere codice JavaScript. L’applicazione sviluppata all’epoca era meno complessa di quella qui analizzata. Presentava una versione web classica per computer e una versione mobile per telefoni;
- da [Tutoriel AngularJS / Spring 4] a URL e [http://tahe.developpez.com/angularjs-spring4/]. L’applicazione sviluppata all’epoca presentava le stesse caratteristiche di quella analizzata nel presente documento. L’applicazione era stata inoltre portata su Android;
- il presente documento;
Da questo lavoro emergono, a mio avviso, i seguenti punti:
- l’applicazione [Primefaces] è stata di gran lunga la più semplice da scrivere e la sua versione web mobile si è rivelata performante. Non richiede conoscenze di JavaScript. Non è possibile portarla in modo nativo sui dispositivi mobili, ma è davvero necessario? Sembra difficile modificare lo stile dell’applicazione. Si lavora infatti con i fogli di stile di Primefaces. Questo potrebbe rappresentare uno svantaggio;
- l’applicazione [AngularJS-Spring MVC] è stata complessa da scrivere. Il framework [AngularJS] mi è sembrato piuttosto difficile da comprendere quando si vuole padroneggiarlo. L’architettura [client Angular] / [service web / jSON implémenté par Spring MVC] è particolarmente pulita ed efficiente. Questa architettura è replicabile per qualsiasi applicazione web. È l’architettura che mi sembra più promettente perché mette in gioco competenze diverse sia sul lato client che sul lato server (JS+HTML+CSS sul lato client, Java o altro sul lato server), il che consente di sviluppare il client e il server in parallelo;
- per l’applicazione sviluppata in questo documento con un’architettura a tre livelli [client jQuery] / [serveur Web1 / Spring MVC / Thymeleaf] / [serveur Web2 / Spring MVC], è possibile che alcuni trovino la tecnologia [jQuery+Spring MVC+Thymelaf] più semplice da comprendere rispetto a quella di [AngularJS]. Il livello [DAO] del client JavaScript che abbiamo scritto è riutilizzabile in altre applicazioni;

























































































































































































































































































