Skip to content

8. Caso di studio

8.1. Introduzione

Proponiamo di realizzare un'applicazione web per la pianificazione degli appuntamenti in uno studio medico. Questo problema è stato affrontato nel documento "AngularJS / Spring 4 Tutorial" all'URL [http://tahe.developpez.com/angularjs-spring4/]. L'architettura di questa applicazione era la seguente:

  • In [1], un server web fornisce pagine statiche a un browser. Queste pagine contengono un'applicazione AngularJS basata sul modello MVC (Model–View–Controller). Il modello qui comprende sia le viste che il dominio, rappresentato in questo caso dal livello [Services];
  • l'utente interagisce con le viste presentate nel browser. Le sue azioni a volte richiederanno l'invio di una query al server Spring 4 [2]. Il server elaborerà la richiesta e restituirà una risposta JSON (JavaScript Object Notation) [3]. Questa risposta verrà utilizzata per aggiornare la vista presentata all'utente.

Proponiamo di prendere questa applicazione e implementarla end-to-end utilizzando Spring MVC. L'architettura diventa quindi la seguente:

Il browser si collegherà a un'applicazione [Web 1] implementata con Spring MVC, che recupererà i propri dati da un servizio web [Web 2] anch'esso implementato con Spring MVC.

8.2. Funzionalità dell'applicazione

I lettori sono invitati a esplorare le funzionalità dell'applicazione provandola. Carichiamo i progetti Maven dalla cartella [case-study] in STS:

Per prima cosa, creeremo il database MySQL 5 [dbrdvmedecins] utilizzando lo strumento [Wamp Server] (vedere la sezione 9.5):

  • In [1], selezionare lo strumento [phpMyAdmin] da WampServer;
  • In [2], selezionare l'opzione [Importa];
  • In [3], selezionare il file [database/dbrdvmedecins.sql];
  • in [4], eseguirlo;
  • in [5], il database viene creato.

Successivamente, dobbiamo avviare il server collegato al database. Si tratta del progetto [rdvmedecins-webjson-server]

Il server sarà disponibile all'URL [http://localhost:8080]. Questo può essere modificato nel file [application.properties] del progetto:

  

server.port=8080

Le credenziali di accesso al database sono memorizzate nella classe [DomainAndPersistenceConfig] del progetto [rdvmedecins-metier-dao]:

  

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

Se accedi al database MySQL utilizzando credenziali diverse, è qui che devi apportare le modifiche.

Successivamente, proprio come con il server precedente, avviamo il server [rdvmedecins-springthymeleaf-server]:

 

Questo server è disponibile per impostazione predefinita all'URL [http://localhost:8081]. Anche in questo caso, è possibile configurarlo nel file [application.properties] del progetto:


server.port=8081

Inoltre, questo server deve conoscere l'URL del server connesso al database. Questa configurazione si trova nella classe [AppConfig] sopra riportata:


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

Se il primo server è stato avviato su una porta diversa da 8080, è necessario modificare la riga 5.

Quindi, utilizzando un browser, richiedere l'URL [http://localhost:8081/boot.html]:

  • in [1], la pagina di accesso dell'applicazione;
  • nei campi [2] e [3], il nome utente e la password dell'utente che desidera utilizzare l'applicazione. Sono presenti due utenti: admin/admin (nome utente/password) con il ruolo (ADMIN) e user/user con il ruolo (USER). Solo il ruolo ADMIN dispone dell'autorizzazione per utilizzare l'applicazione. Il ruolo USER è presente solo per illustrare la risposta del server in questo caso d'uso;
  • in [4], il pulsante che consente di connettersi al server;
  • in [5], la lingua dell'applicazione. Sono disponibili due opzioni: francese (impostazione predefinita) e inglese;
  • in [6], l'URL del server [rdvmedecins-springthymeleaf-server];
  • in [1], effettui l'accesso;
  • una volta effettuato l'accesso, puoi scegliere il medico che desideri consultare [2] e la data dell'appuntamento [3]. Non appena hai selezionato il medico e la data, viene visualizzato automaticamente il calendario:
  • una volta visualizzato il calendario del medico, è possibile prenotare una fascia oraria [5];
  • In [6], selezionare il paziente per l'appuntamento e confermare la selezione in [7];

Una volta confermato l'appuntamento, si torna automaticamente al calendario, dove il nuovo appuntamento è ora elencato. Questo appuntamento può essere cancellato in un secondo momento [8].

Le funzionalità principali sono state descritte. Sono semplici. Concludiamo con le impostazioni della lingua:

Image

  • in [1], si passa dal francese all'inglese;
  • In [2], la visualizzazione passa all'inglese, compreso il calendario;

8.3. Il database

Il database, di seguito denominato [dbrdvmedecins], è un database MySQL5 con le seguenti tabelle:

  

Gli appuntamenti sono gestiti dalle seguenti tabelle:

  • [doctors]: contiene l'elenco dei medici dello studio;
  • [clients]: contiene l'elenco dei pazienti dello studio;
  • [slots]: contiene le fasce orarie di ciascun medico;
  • [rv]: contiene l'elenco degli appuntamenti dei medici.

Le tabelle [ruoli], [utenti] e [utenti_ruoli] sono relative all'autenticazione. Per ora non ce ne occuperemo. Le relazioni tra le tabelle che gestiscono gli appuntamenti sono le seguenti:

 
  • una fascia oraria appartiene a un medico – un medico ha 0 o più fasce orarie;
  • un appuntamento mette in contatto un cliente e un medico tramite la fascia oraria del medico;
  • un cliente ha 0 o più appuntamenti;
  • una fascia oraria è associata a 0 o più appuntamenti (in giorni diversi).

8.3.1. La tabella [DOCTORS]

Contiene informazioni sui medici gestiti dall'applicazione [RdvMedecins].

  • ID: numero che identifica il medico — chiave primaria della tabella
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • LAST_NAME: il cognome del medico
  • FIRST_NAME: nome del medico
  • TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)

8.3.2. La tabella [CLIENTS]

I clienti dei vari medici sono memorizzati nella tabella [CLIENTI]:

  • ID: numero identificativo del cliente - chiave primaria della tabella
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • COGNOME: il cognome del cliente
  • NOME: il nome del cliente
  • TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)

8.3.3. La tabella [SLOTS]

Elenca le fasce orarie in cui sono disponibili gli appuntamenti:

  • ID: numero identificativo della fascia oraria - chiave primaria della tabella (riga 8)
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • DOCTOR_ID: numero ID che identifica il medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna DOCTORS(ID).
  • START_TIME: ora di inizio della fascia oraria
  • MSTART: minuto di inizio della fascia oraria
  • HFIN: ora di fine della fascia oraria
  • MFIN: minuti di fine della fascia oraria

La seconda riga della tabella [SLOTS] (vedi [1] sopra) indica, ad esempio, che la fascia oraria n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (dott.ssa Marie PELISSIER).

8.3.4. La tabella [RV]

Elenca gli appuntamenti prenotati per ciascun medico:

  • ID: identificatore univoco dell'appuntamento – chiave primaria
  • DAY: giorno dell'appuntamento
  • SLOT_ID: fascia oraria dell'appuntamento – chiave esterna sul campo [ID] della tabella [SLOTS] – determina sia la fascia oraria che il medico coinvolto.
  • CLIENT_ID: ID del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]

Questa tabella ha un vincolo di unicità sui valori delle colonne unite (GIORNO, SLOT_ID):

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

Se una riga nella tabella [RV] ha il valore (DAY1, SLOT_ID1) per le colonne (DAY, SLOT_ID), questo valore non può comparire in nessun altro punto. In caso contrario, ciò significherebbe che sono stati prenotati due appuntamenti contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database genera un'eccezione SQLException quando ciò si verifica.

La riga con ID pari a 3 (vedi [1] sopra) indica che è stato prenotato un appuntamento per lo slot n. 20 e il cliente n. 4 il 23/08/2006. La tabella [SLOTS] ci dice che lo slot n. 20 corrisponde alla fascia oraria 16:20 – 16:40 e appartiene al medico n. 1 (Sig.ra Marie PELISSIER). La tabella [CLIENTS] ci dice che il cliente n. 4 è la sig.ra Brigitte BISTROU.

8.3.5. Creazione del database

Per creare il database [dbrdvmedecins], viene fornito uno script [dbrdvmedecins.sql] insieme agli esempi in questo documento [1-3]:

Utilizziamo lo strumento [PhpMyAdmin] di WampServer:

  • In [1], selezionare lo strumento [phpMyAdmin] di WampServer;
  • in [2], selezionare l'opzione [Importa];
  • in [3], selezionare il file [database/dbrdvmedecins.sql];
  • in [4], eseguirlo;
  • in [5], il database viene creato.

8.4. Il servizio Web / JSON

Nell'architettura sopra descritta, ci occuperemo ora della realizzazione del servizio web / JSON basato sul framework Spring MVC. Lo scriveremo in diverse fasi:

  • in primo luogo, i livelli [business] e [DAO] (Data Access Object). Qui utilizzeremo Spring Data;
  • Successivamente, il servizio web JSON senza autenticazione. Qui useremo Spring MVC;
  • quindi aggiungeremo il componente di autenticazione utilizzando Spring Security.

Quella che segue è una riproduzione del documento [http://tahe.developpez.com/angularjs-spring4/] con alcune modifiche.

8.4.1. Introduzione a Spring Data

Implementeremo il livello [DAO] del progetto utilizzando Spring Data, un componente dell'ecosistema Spring.

Il sito web di Spring offre numerosi tutorial per iniziare a utilizzare Spring [http://spring.io/guides]. Ne useremo uno per introdurre Spring Data. A tal fine, utilizzeremo Spring Tool Suite (STS).

  • In [1], importiamo uno dei tutorial da [spring.io/guides];
  • In [2], selezioniamo il tutorial [Accessing Data JPA], che illustra come accedere a un database utilizzando Spring Data;
  • In [3], selezioniamo un progetto configurato da Maven;
  • in [4], il tutorial è disponibile in due forme: [initial], che è una versione vuota da compilare seguendo il tutorial, oppure [complete], che è la versione finale del tutorial. Scegliamo quest'ultima;
  • In [5], è possibile scegliere di visualizzare il tutorial in un browser;
  • In [6], il progetto finale.

8.4.1.1. La configurazione Maven del progetto

Le dipendenze Maven del progetto sono configurate nel file [pom.xml]:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <!-- use UTF-8 for everything -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • righe 5–9: definiscono un progetto Maven padre. Questo progetto definisce la maggior parte delle dipendenze del progetto. Potrebbero essere sufficienti, nel qual caso non vengono aggiunte dipendenze aggiuntive, oppure potrebbero non esserlo, nel qual caso vengono aggiunte le dipendenze mancanti;
  • righe 12–15: definiscono una dipendenza da [spring-boot-starter-data-jpa]. Questo artefatto contiene le classi Spring Data;
  • Righe 16–19: definiscono una dipendenza dal DBMS H2, che consente di creare e gestire database in memoria.

Diamo un'occhiata alle classi fornite da queste dipendenze:

Ce ne sono molte:

  • alcune appartengono all'ecosistema Spring (quelle che iniziano con spring);
  • altri fanno parte dell'ecosistema Hibernate (Hibernate, JBoss), e qui utilizziamo l'implementazione JPA;
  • altri sono librerie di test (JUnit, Hamcrest);
  • altri sono librerie di logging (log4j, logback, slf4j);

Le terremo tutte. Per un'applicazione di produzione, dovrebbero essere mantenute solo quelle necessarie.

Alla riga 26 del file [pom.xml], troviamo la riga:


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

Questa riga è collegata alle seguenti righe:


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

Righe 6–9: Il [spring-boot-maven-plugin] consente di generare il JAR eseguibile dell'applicazione. La riga 26 del file [pom.xml] specifica quindi la classe eseguibile di questo JAR.

8.4.1.2. Il livello [JPA]

L'accesso al database viene gestito tramite un livello [JPA], la Java Persistence API:

  

L'applicazione è semplice e gestisce le entità [Cliente]. La classe [Cliente] fa parte del livello [JPA] ed è la seguente:


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

Un cliente ha un ID [id], un nome [firstName] e un cognome [lastName]. Ogni istanza [Customer] rappresenta una riga in una tabella del database.

  • riga 8: annotazione JPA che garantisce che la persistenza delle istanze [Customer] (Creazione, Lettura, Aggiornamento, Eliminazione) sarà gestita da un'implementazione JPA. In base alle dipendenze Maven, possiamo vedere che viene utilizzata l'implementazione JPA/Hibernate;
  • Righe 11–12: annotazioni JPA che associano il campo [id] alla chiave primaria della tabella [Customer]. La riga 12 indica che l'implementazione JPA utilizzerà il metodo di generazione della chiave primaria specifico del DBMS in uso, in questo caso H2;

Non ci sono altre annotazioni JPA. Verranno quindi utilizzati i valori predefiniti:

  • la tabella [Customer] prenderà il nome dalla classe, ovvero [Customer];
  • le colonne di questa tabella avranno i nomi dei campi della classe: [id, firstName, lastName], tenendo presente che nei nomi delle colonne della tabella non viene fatta distinzione tra maiuscole e minuscole;

Si noti che l'implementazione JPA utilizzata non viene mai menzionata.

8.4.1.3. Il livello [DAO]

  

La classe [CustomerRepository] implementa il livello [DAO]. Il suo codice è il seguente:


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

Si tratta quindi di un'interfaccia e non di una classe (riga 7). Essa estende l'interfaccia [CrudRepository], un'interfaccia Spring Data (riga 5). Questa interfaccia è parametrizzata da due tipi: il primo è il tipo degli elementi gestiti, in questo caso il tipo [Customer]; il secondo è il tipo della chiave primaria degli elementi gestiti, in questo caso un tipo [Long]. L'interfaccia [CrudRepository] è la seguente:


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

Questa interfaccia definisce le operazioni CRUD (Create – Read – Update – Delete) che possono essere eseguite su un tipo JPA T:

  • Riga 8: Il metodo save consente di salvare un'entità T nel database. Salva l'entità utilizzando la chiave primaria assegnatale dal DBMS. Consente inoltre di aggiornare un'entità T identificata dal suo ID di chiave primaria. La scelta tra queste due azioni dipende dal valore dell'ID della chiave primaria: se è nullo, viene eseguita l'operazione di salvataggio; altrimenti, viene eseguita l'operazione di aggiornamento;
  • riga 10: come sopra, ma per un elenco di entità;
  • riga 12: il metodo findOne recupera un'entità T identificata dal suo ID chiave primaria;
  • riga 22: il metodo delete consente di eliminare un'entità T identificata dal suo ID chiave primaria;
  • righe 24–28: varianti del metodo [delete];
  • riga 16: il metodo [findAll] recupera tutte le entità T persistenti;
  • riga 18: come sopra, ma limitato alle entità per le quali è stato fornito un elenco di identificatori;

Torniamo all'interfaccia [CustomerRepository]:


package hello;
 
import java.util.List;
 
import org.springframework.data.repository.CrudRepository;
 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
 
    List<Customer> findByLastName(String lastName);
}
  • La riga 9 consente di recuperare un [Cliente] in base al suo [cognome];

E questo è tutto per il livello [DAO]. Non esiste una classe di implementazione per l'interfaccia precedente. Viene generata in fase di esecuzione da [Spring Data]. I metodi dell'interfaccia [CrudRepository] vengono implementati automaticamente. Per i metodi aggiunti all'interfaccia [CustomerRepository], dipende. Torniamo alla definizione di [Customer]:


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

Il metodo alla riga 9 viene implementato automaticamente da [Spring Data] poiché fa riferimento al campo [lastName] (riga 3) di [Customer]. Quando incontra un metodo [findBySomething] nell'interfaccia da implementare, Spring Data lo implementa utilizzando la seguente query JPQL (Java Persistence Query Language):

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

Pertanto, il tipo T deve avere un campo denominato [something]. Di conseguenza, il metodo

List<Customer> findByLastName(String lastName);

sarà implementato con un codice simile al seguente:

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

dove [em] si riferisce al contesto di persistenza JPA. Ciò è possibile solo se la classe [Customer] dispone di un campo denominato [lastName], cosa che effettivamente avviene.

In conclusione, nei casi semplici, Spring Data ci permette di implementare il livello [DAO] con una semplice interfaccia.

8.4.1.4. Il livello [console]

  

La classe [Application] è la seguente:


package hello;
 
import java.util.List;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
 
@Configuration
@EnableAutoConfiguration
public class Application {
 
    public static void main(String[] args) {
 
        ConfigurableApplicationContext context = SpringApplication.run(Application.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
 
        // save a couple of customers
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));
 
        // fetch all customers
        Iterable<Customer> customers = repository.findAll();
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : customers) {
            System.out.println(customer);
        }
        System.out.println();
 
        // fetch an individual customer by ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();
 
        // fetch customers by last name
        List<Customer> bauers = repository.findByLastName("Bauer");
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : bauers) {
            System.out.println(bauer);
        }
 
        context.close();
    }
 
}
  • Riga 10: indica che la classe viene utilizzata per configurare Spring. Le versioni recenti di Spring possono infatti essere configurate in Java anziché in XML. Entrambi i metodi possono essere utilizzati contemporaneamente. Nel codice di una classe annotata con [Configuration], normalmente si trovano i bean Spring, ovvero le definizioni delle classi da istanziare. Qui non sono definiti bean. È importante notare che quando si lavora con un DBMS, devono essere definiti vari bean Spring:
    • un [EntityManagerFactory] che definisce l'implementazione JPA da utilizzare,
    • un [DataSource] che definisce la fonte dati da utilizzare,
    • un [TransactionManager] che definisce il gestore delle transazioni da utilizzare;

Qui, nessuno di questi bean è definito.

  • Riga 11: L'annotazione [EnableAutoConfiguration] è un'annotazione del progetto [Spring Boot] (righe 5–6). Questa annotazione indica a Spring Boot, tramite la classe [SpringApplication] (riga 16), di configurare l'applicazione in base alle librerie presenti nel suo classpath. Poiché le librerie Hibernate si trovano nel classpath, il bean [entityManagerFactory] verrà implementato con Hibernate. Poiché la libreria DBMS H2 si trova nel classpath, il bean [dataSource] verrà implementato con H2. Nel bean [dataSource] dobbiamo anche definire il nome utente e la password. Qui, Spring Boot utilizzerà l'amministratore H2 predefinito, che non ha password. Poiché la libreria [spring-tx] si trova nel classpath, verrà utilizzato il gestore delle transazioni di Spring.

Inoltre, la directory contenente la classe [Application] verrà scansionata alla ricerca di bean riconosciuti implicitamente da Spring o definiti esplicitamente dalle annotazioni Spring. Pertanto, verranno ispezionate le classi [Customer] e [CustomerRepository]. Poiché la prima ha l'annotazione [@Entity], verrà catalogata come entità da gestire da Hibernate. Poiché la seconda estende l'interfaccia [CrudRepository], verrà registrata come bean Spring.

Esaminiamo le righe 16–17 del codice:


ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
  • Riga 16: Viene eseguito il metodo statico [run] della classe [SpringApplication] nel progetto Spring Boot. Il suo parametro è la classe che presenta un'annotazione [Configuration] o [EnableAutoConfiguration]. A questo punto avrà luogo tutto ciò che è stato spiegato in precedenza. Il risultato è un contesto applicativo Spring, ovvero un insieme di bean gestiti da Spring;
  • Riga 17: Richiediamo un bean che implementi l'interfaccia [CustomerRepository] da questo contesto Spring. Qui, recuperiamo la classe generata da Spring Data per implementare questa interfaccia.

Le operazioni seguenti utilizzano semplicemente i metodi del bean che implementa l'interfaccia [CustomerRepository]. Si noti alla riga 50 che il contesto viene chiuso. L'output della console è il seguente:

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

2014-12-19 11:13:46.612  INFO 10932 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 10932 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\etude-de-cas\gs-accessing-data-jpa-complete)
2014-12-19 11:13:46.658  INFO 10932 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@279ad2e3: startup date [Fri Dec 19 11:13:46 CET 2014]; root of context hierarchy
2014-12-19 11:13:48.234  INFO 10932 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2014-12-19 11:13:48.258  INFO 10932 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2014-12-19 11:13:48.337  INFO 10932 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.7.Final}
2014-12-19 11:13:48.339  INFO 10932 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2014-12-19 11:13:48.341  INFO 10932 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2014-12-19 11:13:48.620  INFO 10932 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2014-12-19 11:13:48.689  INFO 10932 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2014-12-19 11:13:48.853  INFO 10932 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2014-12-19 11:13:49.143  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2014-12-19 11:13:49.151  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2014-12-19 11:13:49.692  INFO 10932 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-12-19 11:13:49.709  INFO 10932 --- [           main] hello.Application                        : Started Application in 3.461 seconds (JVM running for 4.435)
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

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

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2014-12-19 11:13:49.931  INFO 10932 --- [           main] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@279ad2e3: startup date [Fri Dec 19 11:13:46 CET 2014]; root of context hierarchy
2014-12-19 11:13:49.933  INFO 10932 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2014-12-19 11:13:49.934  INFO 10932 --- [           main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2014-12-19 11:13:49.935  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2014-12-19 11:13:49.938  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
  • righe 1-8: il logo del progetto Spring Boot;
  • riga 9: viene eseguita la classe [hello.Application];
  • riga 10: [AnnotationConfigApplicationContext] è una classe che implementa l'interfaccia [ApplicationContext] di Spring. Si tratta di un contenitore di bean;
  • riga 11: il bean [entityManagerFactory] è implementato utilizzando la classe [LocalContainerEntityManagerFactory], una classe Spring;
  • riga 15: compare [Hibernate]. Si tratta dell'implementazione JPA che è stata scelta;
  • riga 19: un dialetto Hibernate è la variante SQL da utilizzare con il DBMS. Qui, il dialetto [H2Dialect] indica che Hibernate funzionerà con il DBMS H2;
  • righe 21–22: viene creato il database. Viene creata la tabella [CUSTOMER]. Ciò significa che Hibernate è stato configurato per generare tabelle dalle definizioni JPA, in questo caso la definizione JPA della classe [Customer];
  • righe 27–31: vengono inseriti i cinque clienti;
  • righe 33–35: risultato del metodo [findOne] dell'interfaccia;
  • righe 37–40: risultati del metodo [findByLastName];
  • Righe 41 e seguenti: log dalla chiusura del contesto Spring.

8.4.1.5. Configurazione manuale del progetto Spring Data

Duplichiamo il progetto precedente nel progetto [gs-accessing-data-jpa-2]:

  

In questo nuovo progetto non faremo affidamento sulla configurazione automatica fornita da Spring Boot. Lo configureremo manualmente. Ciò può essere utile se le configurazioni predefinite non soddisfano le nostre esigenze.

Per prima cosa, specificheremo le dipendenze necessarie nel file [pom.xml]:


...
    <dependencies>
        <!-- Spring Core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring transactions -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring ORM -->        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.7.1.RELEASE</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>1.1.10.RELEASE</version>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.4.Final</version>
        </dependency>
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.178</version>
        </dependency>
        <!-- Commons DBCP -->
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
            <version>1.6</version>
        </dependency>
    </dependencies>
...
 
</project>
  • righe 2–18: librerie principali di Spring;
  • righe 19–29: librerie Spring per la gestione delle transazioni del database;
  • righe 30–35: la libreria Spring per lavorare con un ORM (Object Relational Mapper);
  • righe 36–41: Spring Data utilizzato per accedere al database;
  • righe 42–47: Spring Boot per avviare l'applicazione;
  • righe 54–59: il DBMS H2;
  • righe 60–70: i database sono spesso utilizzati con pool di connessioni, che evitano di aprire e chiudere ripetutamente le connessioni. In questo caso, l'implementazione utilizzata è [commons-dbcp];

Sempre in [pom.xml], modifichiamo il nome della classe eseguibile:


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

Nel nuovo progetto, l'entità [Customer] e l'interfaccia [CustomerRepository] rimangono invariate. Modificheremo la classe [Application], che verrà suddivisa in due classi:

  • [Config], che sarà la classe di configurazione:
  • [Main], che sarà la classe eseguibile;
  

La classe eseguibile [Main] è la stessa di prima, senza le annotazioni di configurazione:


package demo.console;
 
import java.util.List;
 
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
 
public class Main {
 
    public static void main(String[] args) {
 
        ConfigurableApplicationContext context = SpringApplication.run(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
...
 
        context.close();
    }
 
}
  • riga 12: la classe [Main] non presenta più alcuna annotazione di configurazione;
  • riga 16: l'applicazione viene avviata con Spring Boot. Il parametro [Config.class] è la nuova classe di configurazione del progetto;

La classe [Config] che configura il progetto è la seguente:


package demo.config;
 
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
 
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
    // h2 data source
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }
 
    // the provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("demo.entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • riga 22: l'annotazione [@Configuration] rende la classe [Config] una classe di configurazione Spring;
  • riga 21: l'annotazione [@EnableJpaRepositories] specifica le directory in cui si trovano le interfacce [CrudRepository] di Spring Data. Queste interfacce diventeranno componenti Spring e saranno disponibili nel suo contesto;
  • riga 20: l'annotazione [@EnableTransactionManagement] indica che i metodi delle interfacce [CrudRepository] devono essere eseguiti all'interno di una transazione;
  • riga 19: l'annotazione [@EntityScan] specifica le directory in cui devono essere cercate le entità JPA. Qui è stata commentata perché questa informazione è stata fornita esplicitamente alla riga 50. Questa annotazione dovrebbe essere presente se si utilizza la modalità [@EnableAutoConfiguration] e le entità JPA non si trovano nella stessa directory della classe di configurazione;
  • riga 18: l'annotazione [@ComponentScan] consente di specificare le directory in cui cercare i componenti Spring. I componenti Spring sono classi contrassegnate da annotazioni Spring quali @Service, @Component, @Controller, ecc. In questo caso, non ce ne sono altre oltre a quelle definite all'interno della classe [Config], quindi l'annotazione è stata commentata;
  • Righe 25–33: definiscono la fonte dati, il database H2. È l'annotazione @Bean alla riga 25 che rende l'oggetto creato da questo metodo un componente gestito da Spring. Il nome del metodo qui può essere qualsiasi cosa. Tuttavia, deve essere denominato [dataSource] se l'EntityManagerFactory alla riga 47 è assente e definita tramite configurazione automatica;
  • riga 29: il database si chiamerà [demo] e verrà generato nella cartella del progetto;
  • Righe 36–43: definiscono l'implementazione JPA utilizzata, in questo caso un'implementazione Hibernate. Il nome del metodo può essere qualsiasi cosa;
  • riga 39: nessun log SQL;
  • riga 30: il database verrà creato se non esiste;
  • righe 46–54: definiscono l'EntityManagerFactory che gestirà la persistenza JPA. Il metodo deve essere denominato [entityManagerFactory];
  • riga 47: il metodo riceve due parametri dei tipi dei due bean definiti in precedenza. Questi saranno poi costruiti e iniettati da Spring come parametri del metodo;
  • riga 49: imposta l'implementazione JPA da utilizzare;
  • riga 50: specifica le directory in cui si trovano le entità JPA;
  • riga 51: imposta l'origine dati da gestire;
  • righe 57–62: il gestore delle transazioni. Il metodo deve essere denominato [transactionManager]. Riceve il bean delle righe 46–54 come parametro;
  • riga 60: il gestore delle transazioni è associato all'EntityManagerFactory;

I metodi precedenti possono essere definiti in qualsiasi ordine.

L'esecuzione del progetto produce gli stessi risultati. Nella cartella del progetto compare un nuovo file, il file del database H2:

  

Finalmente possiamo fare a meno di Spring Boot. Creiamo una seconda classe eseguibile [Main2]:

  

La classe [Main2] contiene il seguente codice:


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

    public static void main(String[] args) {
 
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
....
 
        context.close();
    }
 
}
  • Riga 15: La classe di configurazione [Config] viene ora utilizzata dalla classe Spring [AnnotationConfigApplicationContext]. Come si vede alla riga 5, non ci sono più dipendenze da Spring Boot.

L'esecuzione produce gli stessi risultati di prima.

8.4.1.6. Creazione di un archivio eseguibile

Per creare un archivio eseguibile del progetto, procedere come segue:

  • in [1]: creare una configurazione di runtime;
  • in [2]: di tipo [Applicazione Java]
  • in [3]: specificare il progetto da eseguire (utilizzare il pulsante Sfoglia);
  • in [4]: specificare la classe da eseguire;
  • in [5]: il nome della configurazione di esecuzione – può essere qualsiasi cosa;
  • in [6]: esporta il progetto;
  • in [7]: come archivio JAR eseguibile;
  • in [8]: specificare il percorso e il nome del file eseguibile da creare;
  • in [9]: il nome della configurazione di esecuzione creata in [5];

Una volta fatto ciò, aprire una console nella cartella contenente l'archivio eseguibile:

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

L'archivio viene eseguito come segue:


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

I risultati visualizzati nella console sono i seguenti:

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

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

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

8.4.1.7. Crea un nuovo progetto Spring Data

Per creare un modello di progetto Spring Data, segui questi passaggi:

  • In [1], creare un nuovo progetto;
  • in [2]: selezionare [Spring Starter Project];
  • Il progetto generato sarà un progetto Maven. In [3], specificare il nome del gruppo di progetto;
  • In [4], specificare il nome dell'artefatto (un file JAR in questo caso) che verrà creato al momento della compilazione del progetto;
  • in [5]: specificare il pacchetto della classe eseguibile che verrà creata nel progetto;
  • in [6]: il nome Eclipse del progetto – può essere qualsiasi cosa (non deve necessariamente essere uguale a [4]);
  • in [7]: specificare che si sta creando un progetto con un livello [JPA]. Le dipendenze richieste per un progetto di questo tipo saranno quindi incluse nel file [pom.xml];
  • in [8]: il progetto creato;

Il file [pom.xml] include le dipendenze necessarie per un progetto JPA:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
  • righe 9–12: dipendenze richieste per JPA — includeranno [Spring Data];
  • righe 13–17: dipendenze necessarie per i test JUnit integrati con Spring;

La classe eseguibile [Application] non fa nulla ma è preconfigurata:


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

La classe di test [ApplicationTests] non fa nulla, ma è preconfigurata:


package istia.st;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
 
    @Test
    public void contextLoads() {
    }
 
}
  • Riga 9: l'annotazione [@SpringApplicationConfiguration] consente di utilizzare il file di configurazione [Application]. La classe di test potrà così beneficiare di tutti i bean definiti in questo file;
  • riga 8: l'annotazione [@RunWith] consente l'integrazione di Spring con JUnit: la classe potrà essere eseguita come test JUnit. [@RunWith] è un'annotazione JUnit (riga 4), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 6);

Ora che disponiamo di uno scheletro di applicazione JPA, possiamo completarlo per scrivere il livello di persistenza lato server della nostra applicazione di gestione degli appuntamenti.

8.4.2. Il progetto server di Eclipse

  

Le componenti principali del progetto sono le seguenti:

  • [pom.xml]: il file di configurazione Maven del progetto;
  • [rdvmedecins.entities]: le entità JPA;
  • [rdvmedecins.repositories]: interfacce Spring Data per l'accesso alle entità JPA;
  • [rdvmedecins.metier]: il livello [business];
  • [rdvmedecins.domain]: le entità gestite dal livello [business];
  • [rdvmdecins.config]: le classi di configurazione del livello di persistenza;
  • [rdvmedecins.boot]: un'applicazione console di base;

8.4.3. La configurazione Maven

Il file [pom.xml] del progetto è il seguente:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <modelVersion>4.0.0</modelVersion>
        <groupId>istia.st.spring4.rdvmedecins</groupId>
        <artifactId>rdvmedecins-metier-dao</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
        </parent>
        <dependencies>
                <!-- Spring Data JPA -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-data-jpa</artifactId>
                </dependency>
                <!-- Spring test -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <!-- Spring security -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-security</artifactId>
                </dependency>
                <!-- driver JDBC / MySQL -->
                <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                </dependency>
                <!-- Tomcat JDBC -->
                <dependency>
                        <groupId>org.apache.tomcat</groupId>
                        <artifactId>tomcat-jdbc</artifactId>
                </dependency>
                <!-- mapper jSON -->
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-databind</artifactId>
                </dependency>
                <!-- Googe Guava -->
                <dependency>
                        <groupId>com.google.guava</groupId>
                        <artifactId>guava</artifactId>
                        <version>16.0.1</version>
                </dependency>
        </dependencies>
        <properties>
                <!-- use UTF-8 for everything -->
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
                <start-class>rdvmedecins.boot.Boot</start-class>
                <java.version>1.8</java.version>
        </properties>
        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>
        <repositories>
                <repository>
                        <id>spring-milestones</id>
                        <name>Spring Milestones</name>
                        <url>http://repo.spring.io/libs-milestone</url>
                        <snapshots>
                                <enabled>false</enabled>
                        </snapshots>
                </repository>
                <repository>
                        <id>org.jboss.repository.releases</id>
                        <name>JBoss Maven Release Repository</name>
                        <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
                        <snapshots>
                                <enabled>false</enabled>
                        </snapshots>
                </repository>
        </repositories>
        <pluginRepositories>
                <pluginRepository>
                        <id>spring-milestones</id>
                        <name>Spring Milestones</name>
                        <url>http://repo.spring.io/libs-milestone</url>
                        <snapshots>
                                <enabled>false</enabled>
                        </snapshots>
                </pluginRepository>
        </pluginRepositories>
</project>
  • righe 8–12: Il progetto si basa sul progetto padre [spring-boot-starter-parent]. Per le dipendenze già presenti nel progetto padre, non viene specificata alcuna versione. Verrà utilizzata la versione definita nel progetto padre. Le altre dipendenze sono dichiarate come di consueto;
  • righe 15–18: per Spring Data;
  • righe 20–24: per i test JUnit;
  • righe 26–29: per la libreria Spring Security, il cui livello [DAO] utilizza una delle classi di crittografia delle password;
  • righe 31–34: driver JDBC per il DBMS MySQL5;
  • righe 36–39: pool di connessioni JDBC di Tomcat. Un pool di connessioni raccoglie le connessioni aperte a un database. Quando il codice desidera aprire una connessione, ne richiede una dal pool. Quando il codice chiude la connessione, questa non viene chiusa, ma restituita al pool. Tutto ciò avviene in modo trasparente a livello di codice. Le prestazioni risultano migliorate poiché l'apertura e la chiusura ripetute di una connessione richiedono tempo. In questo caso, il pool di connessioni stabilisce un certo numero di connessioni al database al momento dell'istanziazione. Successivamente, non vi è alcuna apertura o chiusura di connessioni, a meno che il numero di connessioni memorizzate nel pool non si riveli insufficiente. In tal caso, il pool crea automaticamente nuove connessioni;
  • righe 41–44: libreria Jackson per la gestione di JSON;
  • righe 46–50: libreria Google Collections;

8.4.4. Entità JPA

Le entità JPA sono gli oggetti che incapsulano le righe delle tabelle del database.

  

La classe [AbstractEntity] è la classe padre delle entità [Person, Slot, Appointment]. La sua definizione è la seguente:


package rdvmedecins.entities;
 
import java.io.Serializable;
 
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
 
@MappedSuperclass
public class AbstractEntity implements Serializable {
 
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;
    @Version
    protected Long version;
 
    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }
 
    // initialization
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }
 
        @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1) || entity==null) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id.longValue() == other.id.longValue();
    }
 
 
    // getters and setters
    ..
}
  • riga 11: l'annotazione [@MappedSuperclass] indica che la classe annotata è un genitore delle entità JPA [@Entity];
  • righe 15–17: definiscono la chiave primaria [id] per ciascuna entità. È l'annotazione [@Id] che rende il campo [id] una chiave primaria. L'annotazione [@GeneratedValue(strategy = GenerationType.IDENTITY)] indica che il valore di questa chiave primaria è generato dal DBMS e che viene applicata la modalità di generazione [IDENTITY]. Per il DBMS MySQL, ciò significa che le chiavi primarie saranno generate dal DBMS con l'attributo [AUTO_INCREMENT]
  • Righe 18–19: definiscono la versione di ciascuna entità. L'implementazione JPA incrementerà questo numero di versione ogni volta che l'entità viene modificata. Questo numero viene utilizzato per impedire aggiornamenti simultanei dell'entità da parte di due utenti diversi: due utenti, U1 e U2, leggono l'entità E con un numero di versione pari a V1. U1 modifica E e salva questa modifica nel database: il numero di versione passa quindi a V1+1. A sua volta, U2 modifica E e salva questa modifica nel database: riceverà un'eccezione perché la sua versione (V1) differisce da quella nel database (V1+1);
  • righe 29–33: il metodo [build] inizializza i due campi di [AbstractEntity]. Questo metodo restituisce un riferimento all'istanza di [AbstractEntity] così inizializzata;
  • righe 36–44: il metodo [equals] della classe viene sovrascritto: due entità sono considerate uguali se hanno lo stesso nome di classe e lo stesso identificatore id;
  • righe 21–26: quando si sovrascrive il metodo [equals] di una classe, deve essere sovrascritto anche il suo metodo [hashCode] (righe 21–26). La regola è che due entità ritenute uguali dal metodo [equals] devono avere anche lo stesso [hashCode]. Qui, l’[hashCode] di un’entità è uguale alla sua chiave primaria [id]. L'[hashCode] di una classe viene utilizzato, in particolare, nella gestione di dizionari i cui valori sono istanze della classe;

L'entità [Person] è la classe padre delle entità [Doctor] e [Client]:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
 
@MappedSuperclass
public class Personne extends AbstractEntity {
    private static final long serialVersionUID = 1L;
    // attributes of a person
    @Column(length = 5)
    private String titre;
    @Column(length = 20)
    private String nom;
    @Column(length = 20)
    private String prenom;
 
    // default builder
    public Personne() {
    }
 
    // builder with parameters
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }
 
    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }
 
    // getters and setters
    ...
}
  • riga 6: l'annotazione [@MappedSuperclass] indica che la classe annotata è una classe padre delle entità JPA [@Entity];
  • righe 10–15: una persona ha un titolo (Ms.), un nome (Jacqueline) e un cognome (Tatou). Non vengono fornite informazioni sulle colonne della tabella. Per impostazione predefinita, avranno quindi gli stessi nomi dei campi;

L'entità [Medecin] è la seguente:


package rdvmedecins.entities;
 
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
 
    private static final long serialVersionUID = 1L;
 
    // default builder
    public Medecin() {
    }
 
    // builder with parameters
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }
 
    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }
 
}
  • riga 6: la classe è un'entità JPA;
  • riga 7: associata alla tabella [DOCTORS] nel database;
  • riga 8: l'entità [Doctor] deriva dall'entità [Person];

Un medico può essere inizializzato come segue:

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

Se, inoltre, vogliamo assegnargli un ID e una versione, possiamo scrivere:

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

dove il metodo [build] è quello definito in [AbstractEntity].

L'entità [Client] è la seguente:


package rdvmedecins.entities;
 
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "clients")
public class Client extends Personne {
 
    private static final long serialVersionUID = 1L;
 
    // default builder
    public Client() {
    }
 
    // builder with parameters
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }
 
    // identity
    public String toString() {
        return String.format("Client[%s]", super.toString());
    }
 
}
  • riga 6: la classe è un'entità JPA;
  • riga 7: associata alla tabella [CLIENTS] nel database;
  • riga 8: l'entità [Client] deriva dall'entità [Person];

L'entità [TimeSlot] è la seguente:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
 
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
 
    // default builder
    public Creneau() {
    }
 
    // builder with parameters
    public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
        this.medecin = medecin;
        this.hdebut = hdebut;
        this.mdebut = mdebut;
        this.hfin = hfin;
        this.mfin = mfin;
    }
 
    // toString
    public String toString() {
        return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
    }
 
    // foreign key
    public long getIdMedecin() {
        return idMedecin;
    }
 
    // setters - getters
    ...
}
  • riga 10: la classe è un'entità JPA;
  • riga 11: associata alla tabella [CRENEAUX] nel database;
  • riga 12: l'entità [Creneau] deriva dall'entità [AbstractEntity] e quindi eredita i campi [id] e [version];
  • riga 16: ora di inizio dello slot (14);
  • riga 17: minuti di inizio dello slot (20);
  • riga 18: ora di fine dello slot (14);
  • riga 19: minuti di fine dello slot (40);
  • righe 22–24: il medico a cui appartiene lo slot. La tabella [CRENEAUX] ha una chiave esterna sulla tabella [MEDECINS]. Questa relazione è rappresentata dalle righe 22–24;
  • riga 22: l'annotazione [@ManyToOne] indica una relazione molti-a-uno (slot a medico). L'attributo [fetch=FetchType.LAZY] indica che quando viene richiesta un'entità [Slot] dal contesto di persistenza e questa deve essere recuperata dal database, l'entità [Doctor] non viene restituita insieme ad essa. Il vantaggio di questa modalità è che l'entità [Doctor] viene recuperata solo se lo sviluppatore la richiede. Ciò consente di risparmiare memoria e migliora le prestazioni;
  • riga 23: specifica il nome della colonna della chiave esterna nella tabella [CRENEAUX];
  • righe 27–28: la chiave esterna nella tabella [MEDECINS];
  • riga 27: la colonna [ID_MEDECIN] è già stata utilizzata alla riga 23. Ciò significa che può essere modificata in due modi diversi, cosa non consentita dallo standard JPA. Aggiungiamo quindi gli attributi [insertable = false, updatable = false], assicurandoci che la colonna sia di sola lettura;

L'entità [Rv] è la seguente:


package rdvmedecins.entities;
 
import java.util.Date;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
 
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // characteristics of an Rv
    @Temporal(TemporalType.DATE)
    private Date jour;
 
    // an appointment is linked to a customer
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;
 
    // an appointment is linked to a time slot
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;
 
    // foreign keys
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;
 
    // default builder
    public Rv() {
    }
 
    // with parameters
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }
 
    // toString
    public String toString() {
        return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
    }
 
    // foreign keys
    public long getIdCreneau() {
        return idCreneau;
    }
 
    public long getIdClient() {
        return idClient;
    }
 
    // getters and setters
...
}
  • riga 14: la classe è un'entità JPA;
  • riga 15: associata alla tabella [RV] nel database;
  • riga 16: l'entità [Rv] deriva dall'entità [AbstractEntity] e quindi eredita i campi [id] e [version];
  • riga 21: la data dell'appuntamento;
  • riga 20: il tipo Java [Date] contiene sia una data che un'ora. Qui specifichiamo che viene utilizzata solo la data;
  • righe 24–26: il cliente per il quale è stato fissato questo appuntamento. La tabella [RV] ha una chiave esterna sulla tabella [CLIENTS]. Questa relazione è rappresentata dalle righe 24–26;
  • righe 29–31: la fascia oraria dell'appuntamento. La tabella [RV] ha una chiave esterna sulla tabella [CRENEAUX]. Questa relazione è rappresentata dalle righe 29–31;
  • righe 34–35: la chiave esterna [idClient];
  • righe 36–37: la chiave esterna [idCreneau];

8.4.5. Il livello [DAO]

Implementeremo il livello [DAO] utilizzando Spring Data:

  

Il livello [DAO] è implementato utilizzando quattro interfacce Spring Data:

  • [ClientRepository]: fornisce l'accesso alle entità JPA [Client];
  • [CreneauRepository]: fornisce l'accesso alle entità JPA [Creneau];
  • [MedecinRepository]: fornisce l'accesso alle entità JPA [Medecin];
  • [RvRepository]: fornisce l'accesso alle entità JPA [Rv];

L'interfaccia [MedecinRepository] è la seguente:


package rdvmedecins.repositories;
 
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Medecin;
 
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
  • Riga 7: L'interfaccia [MedecinRepository] eredita semplicemente i metodi dall'interfaccia [CrudRepository] senza aggiungerne altri;

L'interfaccia [ClientRepository] è la seguente:


package rdvmedecins.repositories;
 
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Client;
 
public interface ClientRepository extends CrudRepository<Client, Long> {
}
  • Riga 7: L'interfaccia [ClientRepository] eredita semplicemente i metodi dall'interfaccia [CrudRepository] senza aggiungerne altri;

L'interfaccia [CreneauRepository] è la seguente:


package rdvmedecins.repositories;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Creneau;
 
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
    // liste des créneaux horaires d'un médecin
    @Query("select c from Creneau c where c.medecin.id=?1")
    Iterable<Creneau> getAllCreneaux(long idMedecin);
}
  • riga 8: l'interfaccia [CreneauRepository] eredita i metodi dell'interfaccia [CrudRepository];
  • righe 10-11: il metodo [getAllCreneaux] recupera le fasce orarie disponibili di un medico;
  • riga 11: il parametro è l'ID del medico. Il risultato è un elenco di fasce orarie sotto forma di un oggetto [Iterable<Creneau>];
  • riga 10: l'annotazione [@Query] viene utilizzata per specificare la query JPQL (Java Persistence Query Language) che implementa il metodo. Il parametro [?1] verrà sostituito dal parametro [idMedecin] del metodo;

L'interfaccia [RvRepository] è la seguente:


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

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Rv;
 
public interface RvRepository extends CrudRepository<Rv, Long> {
 
    @Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
    Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
  • riga 10: l'interfaccia [RvRepository] eredita i metodi dell'interfaccia [CrudRepository];
  • righe 12–13: il metodo [getRvMedecinJour] recupera gli appuntamenti di un medico per un determinato giorno;
  • riga 13: i parametri sono l'ID del medico e il giorno. Il risultato è un elenco di appuntamenti sotto forma di oggetto [Iterable<Rv>];
  • riga 12: l'annotazione [@Query] consente di specificare la query JPQL che implementa il metodo. Il parametro [?1] sarà sostituito dal parametro [idMedecin] del metodo, mentre il parametro [?2] sarà sostituito dal parametro [jour] del metodo. La seguente query JPQL non è sufficiente:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

perché i campi della classe Rv, di tipo [Client] e [Creneau], vengono recuperati in modalità [FetchType.LAZY], il che significa che devono essere esplicitamente richiesti per essere ottenuti. Ciò avviene nella query JPQL utilizzando la sintassi [left join fetch entity], che richiede l'esecuzione di un join con la tabella a cui fa riferimento la chiave esterna per recuperare l'entità di riferimento;

8.4.6. Il livello [business]

  
  • [IMetier] è l'interfaccia per il livello [business], mentre [Metier] ne costituisce l'implementazione;
  • [Doctor'sDailySchedule] e [Doctor'sDailyTimeSlot] sono due entità di business;

8.4.6.1. Le entità

L'entità [CreneauMedecinJour] associa una fascia oraria a qualsiasi appuntamento prenotato all'interno di tale fascia:


package rdvmedecins.domain;
 
import java.io.Serializable;
 
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
 
public class CreneauMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Creneau creneau;
    private Rv rv;
 
    // manufacturers
    public CreneauMedecinJour() {
 
    }
 
    public CreneauMedecinJour(Creneau creneau, Rv rv) {
        this.creneau=creneau;
        this.rv=rv;
    }
 
    // toString
    @Override
    public String toString() {
        return String.format("[%s %s]", creneau, rv);
    }
 
    // getters and setters
...
}
  • riga 12: la fascia oraria;
  • riga 13: l'appuntamento, se presente – null in caso contrario;

L'entità [AgendaMedecinJour] rappresenta l'agenda di un medico per un determinato giorno, ovvero l'elenco dei suoi appuntamenti:


package rdvmedecins.domain;
 
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
 
import rdvmedecins.entities.Medecin;
 
public class AgendaMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;
 
    // manufacturers
    public AgendaMedecinJour() {
 
    }
 
    public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
        this.medecin = medecin;
        this.jour = jour;
        this.creneauxMedecinJour = creneauxMedecinJour;
    }
 
    public String toString() {
        StringBuffer str = new StringBuffer("");
        for (CreneauMedecinJour cr : creneauxMedecinJour) {
            str.append(" ");
            str.append(cr.toString());
        }
        return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
    }
 
    // getters and setters
...
}
  • riga 13: il medico;
  • riga 14: il giorno del calendario;
  • riga 15: le loro fasce orarie disponibili, con o senza appuntamento;

8.4.6.2. Il servizio

L'interfaccia del livello [aziendale] è la seguente:


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

I commenti spiegano il ruolo di ciascun metodo.

L'implementazione dell'interfaccia [IMetier] è la seguente classe [Metier]:


package rdvmedecins.metier;
 
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
 
import com.google.common.collect.Lists;
 
@Service("métier")
public class Metier implements IMetier {
 
    // repositories
    @Autowired
    private MedecinRepository medecinRepository;
    @Autowired
    private ClientRepository clientRepository;
    @Autowired
    private CreneauRepository creneauRepository;
    @Autowired
    private RvRepository rvRepository;
 
    // interface implementation
    @Override
    public List<Client> getAllClients() {
        return Lists.newArrayList(clientRepository.findAll());
    }
 
    @Override
    public List<Medecin> getAllMedecins() {
        return Lists.newArrayList(medecinRepository.findAll());
    }
 
    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
    }
 
    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
    }
 
    @Override
    public Client getClientById(long id) {
        return clientRepository.findOne(id);
    }
 
    @Override
    public Medecin getMedecinById(long id) {
        return medecinRepository.findOne(id);
    }
 
    @Override
    public Rv getRvById(long id) {
        return rvRepository.findOne(id);
    }
 
    @Override
    public Creneau getCreneauById(long id) {
        return creneauRepository.findOne(id);
    }
 
    @Override
    public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
        return rvRepository.save(new Rv(jour, client, créneau));
    }
 
    @Override
    public void supprimerRv(Rv rv) {
        rvRepository.delete(rv.getId());
    }
 
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
    ...
    }
 
}
  • riga 24: l'annotazione [@Service] è un'annotazione Spring che rende la classe annotata un componente gestito da Spring. È possibile assegnare o meno un nome al componente. Questo è denominato [business];
  • riga 25: la classe [Metier] implementa l'interfaccia [IMetier];
  • riga 28: l'annotazione [@Autowired] è un'annotazione Spring. Il valore del campo annotato in questo modo verrà inizializzato (iniettato) da Spring con il riferimento a un componente Spring del tipo o del nome specificato. Qui, l'annotazione [@Autowired] non specifica un nome. Pertanto, verrà eseguita l'iniezione basata sul tipo;
  • riga 29: il campo [medecinRepository] verrà inizializzato con il riferimento a un componente Spring di tipo [MedecinRepository]. Questo sarà il riferimento alla classe generata da Spring Data per implementare l'interfaccia [MedecinRepository] che abbiamo già presentato;
  • righe 30–35: questo processo viene ripetuto per le altre tre interfacce discusse;
  • righe 39–41: implementazione del metodo [getAllClients];
  • riga 40: utilizziamo il metodo [findAll] dell'interfaccia [ClientRepository]. Questo metodo restituisce un tipo [Iterable<Client>], che convertiamo in un [List<Client>] utilizzando il metodo statico [Lists.newArrayList]. La classe [Lists] è definita nella libreria Google Guava. Nel file [pom.xml], questa dipendenza è stata importata:

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
  • righe 38–86: i metodi dell'interfaccia [IMetier] sono implementati utilizzando classi del livello [DAO];

Solo il metodo alla riga 88 è specifico del livello [business]. È stato collocato qui perché esegue una logica di business che va oltre il semplice accesso ai dati. Senza questo metodo, non ci sarebbe motivo di creare un livello [business]. Il metodo [getAgendaMedecinJour] è il seguente:


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

Si invitano i lettori a leggere i commenti. L'algoritmo è il seguente:

  • recuperare tutte le fasce orarie per il medico specificato;
  • recuperare tutti i suoi appuntamenti per il giorno specificato;
  • con queste due informazioni, possiamo determinare se una fascia oraria è libera o prenotata;

8.4.7. La configurazione del progetto Spring

  

La classe [DomainAndPersistenceConfig] configura l'intero progetto:


package rdvmedecins.config;
 
import javax.persistence.EntityManagerFactory;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
 
@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {
 
    // JPA entity packages
    public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
 
    // the MySQL data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration JDBC
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
 
    // provider JPA is Hibernate
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }
 
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(ENTITIES_PACKAGES);
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • riga 17: la classe è una classe di configurazione Spring;
  • riga 18: i pacchetti contenenti le interfacce [CrudRepository] di Spring Data. Questi saranno aggiunti al contesto Spring;
  • riga 19: aggiunge al contesto Spring tutte le classi del pacchetto [rdvmedecins] e le sue sottoclassi che presentano un'annotazione Spring. Nel pacchetto [rdvmedecins.metier], verrà individuata la classe [Metier] con la sua annotazione [@Service] e aggiunta al contesto Spring;
  • righe 26–39: configurano il pool di connessioni JDBC di Tomcat (riga 5);
  • riga 36: il pool di connessioni avrà 5 connessioni aperte per impostazione predefinita. Questa riga è mostrata a scopo illustrativo. Nel nostro caso, sarebbe sufficiente 1 connessione. Se il livello [DAO] dovesse essere utilizzato da più thread, questa riga sarebbe necessaria. Questo sarà il caso in seguito, quando il livello [DAO] fungerà da base per un'applicazione web che, per sua natura, supporta più utenti serviti contemporaneamente;
  • Righe 42–49: l'implementazione JPA utilizzata è un'implementazione Hibernate;
  • riga 45: nessun log SQL;
  • riga 46: nessuna rigenerazione delle tabelle;
  • riga 47: il DBMS utilizzato è MySQL;
  • Righe 53–61: definiscono l'EntityManagerFactory per il livello JPA. Da questo oggetto otteniamo l'oggetto [EntityManager], che viene utilizzato per eseguire operazioni JPA;
  • Riga 57: specifica i pacchetti in cui si trovano le entità JPA;
  • riga 58: specifica l'origine dati da collegare al livello JPA;
  • righe 64–69: il gestore delle transazioni associato al precedente EntityManagerFactory. Per impostazione predefinita, i metodi delle interfacce [CrudRepository] di Spring Data vengono eseguiti all'interno di una transazione. La transazione viene avviata prima di entrare nel metodo e viene completata (tramite un commit o un rollback) dopo esserne usciti;

8.4.8. Test per il livello [business]

  

La classe [rdvmedecins.tests.Metier] è una classe di test Spring/JUnit 4:


package rdvmedecins.tests;
 
import java.text.ParseException;
import java.util.Date;
import java.util.List;
 
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
 
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
 
    @Autowired
    private IMetier métier;
 
    @Test
    public void test1(){
        // customer display
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // physician display
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // display doctor's slots
        Medecin médecin = medecins.get(0);
        List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
        display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
        // list of doctor's appointments on a given day
        Date jour = new Date();
        display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // add a RV to the list
        Rv rv = null;
        Creneau créneau = creneaux.get(2);
        Client client = clients.get(0);
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        rv = métier.ajouterRv(jour, créneau, client);
        // check
        Rv rv2 = métier.getRvById(rv.getId());
        Assert.assertEquals(rv, rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // add a RV in the same slot on the same day
        // must trigger an exception
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        Boolean erreur = false;
        try {
            rv = métier.ajouterRv(jour, créneau, client);
            System.out.println("Rv ajouté");
        } catch (Exception ex) {
            Throwable th = ex;
            while (th != null) {
                System.out.println(ex.getMessage());
                th = th.getCause();
            }
            // we note the error
            erreur = true;
        }
        // check for errors
        Assert.assertTrue(erreur);
        // RV list
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // calendar display
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // delete a RV
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // check
        rv2 = métier.getRvById(rv.getId());
        Assert.assertNull(rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
    }
 
    // utility method - displays items in a collection
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
 
}
  • riga 22: l'annotazione [@SpringApplicationConfiguration] consente di utilizzare il file di configurazione [DomainAndPersistenceConfig] descritto in precedenza. La classe di test beneficia così di tutti i bean definiti da questo file;
  • riga 23: l'annotazione [@RunWith] consente l'integrazione di Spring con JUnit: la classe può essere eseguita come un test JUnit. [@RunWith] è un'annotazione JUnit (riga 9), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 12);
  • Righe 26–27: Iniezione di un riferimento al livello [business] nella classe di test;
  • molti test sono semplicemente test visivi:
    • righe 32–33: elenco dei clienti;
    • righe 35–36: elenco dei medici;
    • righe 39-40: elenco delle fasce orarie di un medico;
    • riga 43: elenco degli appuntamenti di un medico;
  • riga 50: aggiunta di un nuovo appuntamento. Il metodo [addAppt] restituisce l'appuntamento con informazioni aggiuntive, ovvero il suo ID chiave primaria;
  • riga 53: questa chiave primaria viene utilizzata per cercare l'appuntamento nel database;
  • riga 54: verifichiamo che l'appuntamento cercato e quello trovato siano gli stessi. Ricordiamo che il metodo [equals] dell'entità [Rv] è stato ridefinito: due appuntamenti sono uguali se hanno lo stesso id. Qui, questo ci mostra che l'appuntamento aggiunto è stato effettivamente inserito nel database;
  • Righe 61–73: proviamo ad aggiungere lo stesso appuntamento una seconda volta. Questo dovrebbe essere rifiutato dal DBMS perché esiste un vincolo di unicità:

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

La riga 8 sopra specifica che la combinazione [DAY, SLOT_ID] deve essere univoca, il che impedisce che due appuntamenti vengano programmati nella stessa fascia oraria dello stesso giorno.

  • riga 73: verifichiamo che si sia effettivamente verificata un'eccezione;
  • riga 77: recuperiamo il calendario del medico per il quale abbiamo appena aggiunto un appuntamento;
  • riga 79: verifichiamo che l'appuntamento aggiunto sia effettivamente presente nel suo calendario;
  • riga 82: eliminiamo l'appuntamento aggiunto;
  • riga 84: recuperiamo l'appuntamento cancellato dal database;
  • riga 85: verifichiamo di aver recuperato un puntatore nullo, a indicare che l'appuntamento che stavamo cercando non esiste;

Il test viene eseguito con successo:

 

8.4.9. Il programma da console

  

Il programma da console è semplice. Mostra come recuperare una chiave esterna:


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

Il programma aggiunge un appuntamento e poi verifica che sia stato aggiunto.

  • riga 19: la classe [SpringApplication] utilizzerà la classe di configurazione [DomainAndPersistenceConfig];
  • riga 20: soppressione dei log di avvio dell'applicazione;
  • riga 22: viene eseguita la classe [SpringApplication]. Restituisce un contesto Spring, ovvero l'elenco dei bean registrati;
  • riga 24: viene recuperato un riferimento al bean che implementa l'interfaccia [IMetier]. Si tratta quindi di un riferimento al livello [business];
  • righe 27–31: aggiungi un nuovo appuntamento per oggi, per il cliente n. 1 nello slot n. 1. Il cliente e lo slot sono stati creati da zero per dimostrare che vengono utilizzati solo gli identificatori. Abbiamo inizializzato la versione qui, ma avremmo potuto utilizzare qualsiasi valore. Qui non viene utilizzata;
  • riga 34: vogliamo sapere quale medico ha lo slot n. 1. Per farlo, dobbiamo interrogare il database per lo slot n. 1. Poiché siamo in modalità [FetchType.LAZY], il medico non viene restituito con lo slot. Tuttavia, ci siamo assicurati di includere un campo [idMedecin] nell'entità [Creneau] per recuperare la chiave primaria del medico;
  • riga 35: recuperiamo la chiave primaria del medico;
  • riga 36: visualizziamo l'elenco degli appuntamenti del medico;

L'output della console è il seguente:

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

8.4.10. Gestione dei log

I log della console sono configurati tramite due file: [application.properties] e [logback.xml] [1]:

Il file [application.properties] viene utilizzato dal framework Spring Boot. Consente di definire un'ampia gamma di impostazioni per sovrascrivere i valori predefiniti utilizzati da Spring Boot (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Ecco il suo contenuto:


logging.level.org.hibernate=OFF
spring.main.show-banner=false
  • Riga 1: controlla il livello di registrazione di Hibernate; in questo caso, nessuna registrazione
  • riga 2: controlla la visualizzazione del banner di Spring Boot — in questo caso, nessun banner

Il file [logback.xml] è il file di configurazione del framework di log [logback] [2]:


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- log level control -->
        <root level="info"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>
  • Il livello di log generale è controllato dalla riga 9: in questo caso, i log di livello [info];

Questo produce il seguente risultato:

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

Se impostiamo il livello di log di Hibernate su [info] (senza modificare nient'altro):


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

questo produce il seguente risultato:

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

Se impostiamo il livello di log su [debug] (senza modificare nient'altro):


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

Questo produce il seguente risultato:


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

8.4.11. Il livello [web / JSON]

  

Realizzeremo il livello [web / JSON] in diverse fasi:

  • Fase 1: Un livello web operativo senza autenticazione;
  • Fase 2: Implementazione dell'autenticazione con Spring Security;
  • Passaggio 3: Implementazione del CORS [Il Cross-Origin Resource Sharing (CORS) è un meccanismo che consente di richiedere molte risorse (ad esempio, font, JavaScript, ecc.) presenti in una pagina web da un dominio diverso da quello di origine della risorsa. (Wikipedia)]. Il client per il nostro servizio web sarà un client web Angular che non appartiene necessariamente allo stesso dominio del nostro servizio web. Per impostazione predefinita, non può accedervi a meno che il servizio web non lo autorizzi a farlo. Vedremo come;

8.4.11.1. Configurazione di Maven

Il file [pom.xml] del progetto è il seguente:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>istia.st.spring4.mvc</groupId>
        <artifactId>rdvmedecins-webjson-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
 
        <name>rdvmedecins-webjson-server</name>
        <description>Gestion de RV Médecins</description>
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
        </parent>
        <dependencies>
                <!-- spring mvc web layer -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <!-- test layer -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <!-- layer DAO -->
                <dependency>
                        <groupId>istia.st.spring4.rdvmedecins</groupId>
                        <artifactId>rdvmedecins-metier-dao</artifactId>
                        <version>0.0.1-SNAPSHOT</version>
                </dependency>
        </dependencies>
...
</project>
  • righe 12–15: il progetto Maven principale;
  • righe 19–22: dipendenze per un progetto Spring MVC;
  • righe 24–28: dipendenze per i test JUnit/Spring;
  • righe 30–34: dipendenze relative ai livelli del progetto [logica di business, DAO, JPA];

8.4.11.2. L'interfaccia del servizio web

  • in [1], sopra, il browser può richiedere solo un numero limitato di URL con una sintassi specifica;
  • in [4], riceve una risposta JSON;

Le risposte del nostro servizio web avranno tutte lo stesso formato, corrispondente alla rappresentazione JSON di un oggetto di tipo [Response] come segue:


package rdvmedecins.web.models;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}
  • riga 7: codice di errore della risposta 0: OK, in caso contrario: KO;
  • riga 11: un elenco di messaggi di errore, se c'è un errore;
  • riga 13: il corpo della risposta;

Presentiamo ora le schermate che illustrano l'interfaccia del servizio web / JSON:

Elenco di tutti i pazienti dello studio medico [/getAllClients]

Elenco di tutti i medici dello studio medico [/getAllMedecins]

Elenco degli orari disponibili di un medico [/getAllCreneaux/{idMedecin}]

Elenco degli appuntamenti di un medico [/getRvMedecinJour/{idMedecin}/{yyyy-mm-dd}

Agenda giornaliera del medico [/getAgendaMedecinJour/{idMedecin}/{yyyy-mm-dd}]

Per aggiungere o eliminare un appuntamento, utilizziamo l'estensione di Chrome [Advanced Rest Client] poiché queste operazioni vengono eseguite tramite una richiesta POST.

Aggiungi un appuntamento [/addAppointment]

  • in [0], l'URL del servizio web;
  • in [1], viene utilizzato il metodo POST;
  • in [2], il testo JSON delle informazioni inviate al servizio web nel formato {day, clientId, slotId};
  • in [3], il client specifica al servizio web che sta inviando informazioni in formato JSON;

La risposta è quindi la seguente:

  • in [4]: il client invia l'intestazione indicando che i dati che sta inviando sono in formato JSON;
  • in [5]: il servizio web risponde che sta inviando anch'esso JSON;
  • in [6]: la risposta JSON del servizio web. Il campo [body] contiene la rappresentazione JSON dell'appuntamento aggiunto;

È possibile verificare la presenza del nuovo appuntamento:

Prendere nota dell'ID dell'appuntamento [50]. Questo verrà eliminato.

Elimina un appuntamento [/deleteApp]

  • in [1], l'URL del servizio web;
  • in [2], viene utilizzato il metodo POST;
  • in [3], il testo JSON delle informazioni inviate al servizio web nella forma {idRv};
  • in [4], il client specifica al servizio web che sta inviando dati JSON;

La risposta è quindi la seguente:

  • in [5]: il campo [status] è impostato su 0, a indicare che l'operazione è andata a buon fine;

È possibile verificare l'eliminazione dell'appuntamento:

Come si può vedere sopra, l'appuntamento della paziente [sig.ra GERMAIN] non è più presente.

Il servizio web consente inoltre di recuperare le entità in base al loro ID:

Tutti questi URL sono gestiti dal controller [RdvMedecinsController], che presenteremo tra poco.

8.4.11.3. Configurazione del servizio web

  

La classe di configurazione [AppConfig] è la seguente:


package rdvmedecins.web.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
 
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
 
}
  • Riga 12: la classe [AppConfig] configura l'intera applicazione;
  • riga 9: la classe [AppConfig] è una classe di configurazione Spring;
  • riga 10: specifichiamo che i componenti Spring devono essere cercati nel pacchetto [rdvmedecins.web] e nei suoi sottopacchetti. In questo modo verranno individuati i seguenti componenti:
    • [@RestController RdvMedecinsController] nel pacchetto [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] nel pacchetto [rdvmedecins.web.models];
  • Riga 11: importiamo la classe [DomainAndPersistenceConfig], che configura il progetto [rdvmedecins-metier-dao] per fornire l'accesso ai bean di quel progetto;
  • riga 11: la classe [SecurityConfig] configura la sicurezza dell'applicazione web. Per ora la ignoreremo;
  • riga 11: la classe [WebConfig] configura il livello [web / JSON];

La classe [WebConfig] è la seguente:


package rdvmedecins.web.config;
 
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
@Configuration
@EnableWebMvc
public class WebConfig {
 
    // dispatcherservlet configuration for CORS headers
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
 
    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }
 
    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8080);
    }
 
    // mappers jSON
    @Bean
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
    }
 
    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(
                new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
        return jsonMapperLongRv;
    }
 
    @Bean
    public ObjectMapper jsonMapperShortRv() {
        ObjectMapper jsonMapperShortRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
        jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
        return jsonMapperShortRv;
    }
 
}
  • righe 20–25: definiscono il bean [dispatcherServlet]. La classe [DispatcherServlet] è il servlet del framework Spring MVC. Agisce come un [FrontController]: intercetta le richieste inviate al sito Spring MVC e le instrada verso uno dei controller del sito;
  • riga 22: istanziazione della classe;
  • riga 23: questa riga può essere ignorata per ora;
  • righe 27–30: il servlet [dispatcherServlet] gestisce tutti gli URL;
  • righe 27–30: attivazione del server Tomcat incorporato nelle dipendenze del progetto. Verrà eseguito sulla porta 8080;
  • righe 38–67: quattro mappatori JSON configurati con diversi filtri JSON;
  • righe 38–41: un mappatore JSON senza filtri;
  • righe 43–49: il mappatore JSON [jsonMapperShortCreneau] serializza/deserializza un oggetto [Creneau] ignorando il campo [Creneau.medecin];
  • righe 51–59: il mappatore JSON [jsonMapperLongRv] serializza/deserializza un oggetto [Rv] ignorando il campo [Rv.creneau.medecin];
  • righe 61-67: il mappatore JSON [jsonMapperShortRv] serializza/deserializza un oggetto [Rv] ignorando i campi [Rv.creneau] e [Rv.client];

8.4.11.4. La classe [ApplicationModel]

  

La classe [ApplicationModel] avrà due scopi:

  • come cache per memorizzare gli elenchi di medici e pazienti (clienti);
  • come interfaccia unica per i controller;

package rdvmedecins.web.models;
 
import java.util.Date;
import java.util.List;
 
import javax.annotation.PostConstruct;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;
 
@Component
public class ApplicationModel implements IMetier {
 
    // the [business] layer
    @Autowired
    private IMetier métier;
 
    // data from the [business] layer
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    // configuration data
    private boolean CORSneeded = false;
    private boolean secured = false;
 
    @PostConstruct
    public void init() {
        // we get the doctors and the customers
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }
 
    // getter
    public List<String> getMessages() {
        return messages;
    }
 
    // ------------------------- [business] layer interface
    @Override
    public List<Client> getAllClients() {
        return clients;
    }
 
    @Override
    public List<Medecin> getAllMedecins() {
        return médecins;
    }
 
    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return métier.getAllCreneaux(idMedecin);
    }
 
    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return métier.getRvMedecinJour(idMedecin, jour);
    }
 
    @Override
    public Client getClientById(long id) {
        return métier.getClientById(id);
    }
 
    @Override
    public Medecin getMedecinById(long id) {
        return métier.getMedecinById(id);
    }
 
    @Override
    public Rv getRvById(long id) {
        return métier.getRvById(id);
    }
 
    @Override
    public Creneau getCreneauById(long id) {
        return métier.getCreneauById(id);
    }
 
    @Override
    public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
        return métier.ajouterRv(jour, creneau, client);
    }
 
    @Override
    public void supprimerRv(long idRv) {
        métier.supprimerRv(idRv);
    }
 
    @Override
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        return métier.getAgendaMedecinJour(idMedecin, jour);
    }
 
     // getters and setters
public boolean isCORSneeded() {
        return CORSneeded;
    }
 
    public boolean isSecured() {
        return secured;
    }
 
}
  • riga 19: l'annotazione [@Component] rende la classe [ApplicationModel] un componente Spring. Come tutti i componenti Spring visti finora (ad eccezione di @Controller), verrà istanziato un solo oggetto di questo tipo (singleton);
  • riga 20: la classe [ApplicationModel] implementa l'interfaccia [IMetier];
  • righe 23–24: Spring inietta un riferimento al livello [business];
  • riga 34: l'annotazione [@PostConstruct] garantisce che il metodo [init] venga eseguito immediatamente dopo l'istanziazione della classe [ApplicationModel];
  • righe 38–39: recupero degli elenchi di medici e clienti dal livello [business];
  • Riga 41: se si verifica un'eccezione, i messaggi dello stack di eccezioni vengono memorizzati nel campo alla riga 17;

L'architettura del livello web si evolve come segue:

  • In [2b], i metodi del/i controller comunicano con il singleton [ApplicationModel];

Questa strategia offre flessibilità nella gestione della cache. Attualmente, gli slot degli appuntamenti dei medici non vengono memorizzati nella cache. Per memorizzarli, è sufficiente modificare la classe [ApplicationModel]. Ciò non ha alcun impatto sul controller, che continuerà a utilizzare il metodo [List<Creneau> getAllCreneaux(long idMedecin)] come prima. È l’implementazione di questo metodo in [ApplicationModel] che verrà modificata.

8.4.11.5. La classe Static

La classe [Static] contiene un insieme di metodi di utilità statici che non hanno aspetti "aziendali" o "web":

  

Il codice è il seguente:


package rdvmedecins.web.helpers;
 
import java.util.ArrayList;
import java.util.List;
 
public class Static {
 
    public Static() {
    }
 
    // list of exception error messages
    public static List<String> getErreursForException(Exception exception) {
        // retrieve the list of exception error messages
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }
}
  • riga 12: il metodo [Static.getErrorsForException] utilizzato (riga 8 sotto) nel metodo [init] della classe [ApplicationModel]:

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

Il metodo costruisce un oggetto [List<String>] contenente i messaggi di errore [exception.getMessage()] di un'eccezione [exception] e quelli della sua eccezione interna [exception.getCause()].

8.4.11.6. Lo scheletro del controller [RdvMedecinsController]

  

Descriveremo ora in dettaglio la gestione degli URL del servizio web. In questo processo sono coinvolte tre classi principali:

  • il controller [RdvMedecinsController];
  • la classe dei metodi di utilità [Static];
  • la classe della cache [ApplicationModel];
  

Il controller [RdvMedecinsController] è il seguente:


package rdvmedecins.web.controllers;
 
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;
 
@Controller
public class RdvMedecinsController {
 
    @Autowired
    private ApplicationModel application;
 
    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;
 
    // message list
    private List<String> messages;
 
    // mappers jSON
    @Autowired
    private ObjectMapper jsonMapper;
 
    @Autowired
    private ObjectMapper jsonMapperShortCreneau;
 
    @Autowired
    private ObjectMapper jsonMapperLongRv;
 
    @Autowired
    private ObjectMapper jsonMapperShortRv;
 
    @PostConstruct
    public void init() {
        // application error messages
        messages = application.getMessages();
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {...}
 
    // customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllClients() throws JsonProcessingException {...}
 
    // list of physician slots
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
 
    // list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
                    throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
                    throws JsonProcessingException {...}
 
    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {...}
}
  • riga 35: l'annotazione [@Controller] rende la classe [RdvMedecinsController] un controller Spring, la C in MVC;
  • righe 38–39: un oggetto di tipo [ApplicationModel] verrà iniettato qui da Spring. Lo abbiamo già presentato;
  • righe 41-42: un oggetto di tipo [RdvMedecinsCorsController] verrà iniettato qui da Spring. Introdurremo questo oggetto più avanti;
  • righe 48–58: i mappatori JSON definiti nella classe di configurazione [WebConfig];
  • riga 60: l'annotazione [@PostConstruct] contrassegna un metodo da eseguire immediatamente dopo l'istanziazione della classe. Quando questo metodo viene eseguito, gli oggetti iniettati da Spring sono disponibili;
  • riga 63: recuperiamo eventuali messaggi di errore dall'oggetto [ApplicationModel]. Questo oggetto è stato istanziato all'avvio dell'applicazione e ha tentato di memorizzare nella cache i medici e i clienti. Se l'operazione non è andata a buon fine, allora [messages!=null]. Ciò consentirà ai metodi del controller di determinare se l'applicazione si è inizializzata correttamente;
  • righe 67–118: gli URL esposti dal servizio [web/jSON]. Tutti i metodi restituiscono una stringa JSON del seguente tipo [Response<T>]:
 

package rdvmedecins.web.models;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}
  • riga 9: un codice di errore: 0 significa nessun errore;
  • riga 11: se [status!=0], allora [messages] è un elenco di messaggi di errore;
  • riga 13: un oggetto T incapsulato nella risposta. T è nullo in caso di errore;

Questo oggetto viene serializzato in JSON prima di essere inviato al browser del client;

  • riga 67: l'URL esposto è [/getAllDoctors]. Il client deve utilizzare un metodo [GET] per effettuare la richiesta (method = RequestMethod.GET). Se questo URL fosse richiesto tramite un POST, verrebbe rifiutato e Spring MVC invierebbe un codice di errore HTTP al client web. Il metodo stesso restituisce la risposta al client (riga 68). Si tratterà di una stringa (riga 67). L'intestazione HTTP [Content-type: application/json; charset=UTF-8] verrà inviata al client per indicare che riceverà una stringa JSON (riga 67);
  • riga 77: l'URL è configurato con {idMedecin}. Questo parametro viene recuperato utilizzando l'annotazione [@PathVariable] alla riga 79;
  • Riga 79: il parametro [long idMedecin] ottiene il suo valore dal parametro {idMedecin} nell'URL [@PathVariable("idMedecin")]. Il parametro nell'URL e quello nel metodo possono avere nomi diversi . Si noti che [@PathVariable("idMedecin")] è di tipo String (l'intero URL è una String), mentre il parametro [long idMedecin] è di tipo [long]. La conversione di tipo viene eseguita automaticamente. Se questa conversione di tipo fallisce, viene restituito un codice di errore HTTP;
  • riga 105: l'annotazione [@RequestBody] si riferisce al corpo della richiesta. In una richiesta GET, non c'è quasi mai un corpo (ma è possibile includerne uno). In una richiesta POST, di solito ce n'è uno (ma è possibile ometterlo). Per l'URL [ajouterRv], il client web invia la seguente stringa JSON nel suo POST:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

La sintassi [@RequestBody PostAjouterRv post] (riga 105), combinata con il fatto che il metodo si aspetta JSON [consumes = "application/json; charset=UTF-8"] (riga 103), significa che la stringa JSON inviata dal client web verrà deserializzata in un oggetto di tipo [PostAjouterRv]. Questo avviene come segue:


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

Anche in questo caso, le conversioni di tipo necessarie avverranno automaticamente;

  • Le righe 107–109 contengono un meccanismo simile per l'URL [/supprimerRv]. La stringa JSON inviata è la seguente:
{"idRv":116}

e il tipo [PostSupprimerRv] è il seguente:


package rdvmedecins.web.models;
 
public class PostSupprimerRv {
 
    // pOST DATA
    private long idRv;
 
    // getters and setters
    ...
}

8.4.11.7. L'URL [/getAllDoctors]

L'URL [/getAllMedecins] è gestito dal seguente metodo nel controller [RdvMedecinsController]:


// list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {
        // the answer
        Response<List<Medecin>> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // list of doctors
            try {
                response = new Response<>(0, null, application.getAllMedecins());
            } catch (RuntimeException e) {
                response = new Response<>(1, Static.getErreursForException(e), null);
            }
        }
        // answer
        return jsonMapper.writeValueAsString(response);
    }
  • righe 9-10: verifichiamo se l'applicazione si è inizializzata correttamente (messages==null). In caso contrario, restituiamo una risposta con status=-1 e body=messages;
  • riga 13: altrimenti, richiediamo l'elenco dei medici dalla classe [ApplicationModel];
  • riga 19: inviamo la stringa JSON della risposta utilizzando il mappatore JSON [jsonMapper] poiché la classe [Medecin] non dispone di un filtro JSON. La risposta può essere priva di errori (riga 14) o contenere un errore (riga 16). Il metodo [application.getAllMedecins()] non genera un'eccezione perché restituisce semplicemente un elenco memorizzato nella cache. Tuttavia, manterremo questa gestione delle eccezioni nel caso in cui i medici non siano più memorizzati nella cache;

Non abbiamo ancora illustrato il caso in cui l'applicazione si sia inizializzata in modo errato. Arrestiamo il DBMS MySQL5, avviamo il servizio web e quindi richiediamo l'URL [/getAllMedecins]:

Image

Otteniamo effettivamente un errore. In circostanze normali, otteniamo la seguente vista:

8.4.11.8. L'URL [/getAllClients]

L'URL [/getAllClients] è gestito dal seguente metodo nel [RdvMedecinsController]:


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

È simile al metodo [getAllMedecins] che abbiamo già visto. I risultati ottenuti sono i seguenti:

8.4.11.9. L'URL [/getAllSlots/{doctorId}]

L'URL [/getAllSlots/{doctorId}] è gestito dal seguente metodo del controller [RdvMedecinsController]:


// list of physician slots
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
        // the answer
        Response<List<Creneau>> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        }
        // we get the doctor back
        Response<Medecin> responseMedecin = getMedecin(idMedecin);
        if (responseMedecin.getStatus() != 0) {
            response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
        } else {
            Medecin médecin = responseMedecin.getBody();
            // doctor's slots
            try {
                response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperShortCreneau.writeValueAsString(response);
    }
  • riga 12: il medico identificato dal parametro [id] viene richiesto da un metodo locale:

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

Questo metodo restituisce un valore di stato compreso nell'intervallo [0,1,2]. Torniamo al codice del metodo [getAllCreneaux]:

  • righe 13-14: se status!=0, costruiamo una risposta con un errore;
  • riga 16: recuperiamo il medico;
  • riga 19: recuperiamo le fasce orarie di questo medico;
  • riga 25: inviamo un oggetto [List<Creneau>] come risposta. Ricordiamo la definizione della classe [Creneau]:

@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • riga 13: il medico viene recuperato in modalità [FetchType.LAZY];

Ricordiamo la query JPQL che implementa il metodo [getAllCreneaux] nel livello [DAO]:


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

La notazione [c.medecin.id] impone un join tra le tabelle [CRENEAUX] e [MEDECINS]. Di conseguenza, la query restituisce tutti gli slot del medico, con il medico incluso in ciascuno di essi. Quando serializziamo questi slot in JSON, la stringa JSON del medico compare in ciascuno di essi. Ciò non è necessario. Per controllare la serializzazione, abbiamo bisogno di due cose:

  1. l'accesso all'oggetto da serializzare;
  2. configurare l'oggetto da serializzare;

Il punto 1 viene gestito iniettando nel controller il convertitore JSON appropriato per l'oggetto:


@Autowired
private ObjectMapper jsonMapperShortCreneau;

Il punto 2 si ottiene aggiungendo un'annotazione alla classe [Creneau] definita nel progetto [rdvmedecins-metier-dao]:

  

@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
  • Riga 3: un'annotazione della libreria JSON Jackson. Crea un filtro chiamato [creneauFilter]. Utilizzando questo filtro, saremo in grado di definire a livello di programmazione quali campi debbano o non debbano essere serializzati;

La serializzazione dell'oggetto [Creneau] avviene nella seguente riga del metodo [getAllCreneaux]:


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

Il mappatore JSON [jsonMapperShortCreneau] è stato definito nella classe [WebConfig] come segue:


    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
}
  • Riga 5: Il filtro denominato [creneauFilter] è associato al filtro [creneauFilter] della riga 4. Questo filtro serializza l'oggetto [Creneau] senza il suo campo [medecin];

Il risultato restituito dal metodo [getAllCreneaux] è una stringa JSON di tipo [Response<List<Creneau>].

I risultati ottenuti sono i seguenti:

oppure questi se lo slot non esiste:

Da questo esempio possiamo ricavare la seguente regola:

  • I metodi del server Web / JSON restituiscono un oggetto di tipo [Response<T>] serializzato in JSON;
  • se il tipo T ha uno o più filtri JSON, per serializzarlo verrà utilizzato un mappatore con gli stessi filtri;

8.4.11.10. L'URL [/getRvMedecinJour/{idMedecin}/{jour}]

L'URL [/getRvMedecinJour/{idMedecin}/{jour}] è gestito dal seguente metodo del controller [RdvMedecinsController]:


// list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
                    throws JsonProcessingException {
        // the answer
        Response<List<Rv>> response=null;
        boolean erreur = false;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // check the date
        Date jourAgenda = null;
        if (!erreur) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<List<Rv>>(3, messages, null);
                erreur = true;
            }
        }
        Response<Medecin> responseMedecin = null;
        if (!erreur) {
            // we get the doctor back
            responseMedecin = getMedecin(idMedecin);
            if (responseMedecin.getStatus() != 0) {
                response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            Medecin médecin = responseMedecin.getBody();
            // list of appointments
            try {
                response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
            } catch (RuntimeException e1) {
                response = new Response<>(4, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • Dobbiamo restituire la stringa JSON di tipo [Response<List<Rv>>]. La classe [Rv] ha un campo [Rv.creneau]. Se questo campo viene serializzato, incontreremo il filtro JSON [creneauFilter];
  • riga 47: l'oggetto di tipo [Response<List<Rv>>] della riga 7 viene serializzato in JSON;

Esaminiamo il caso in cui l'elenco degli appuntamenti è stato ottenuto alla riga 42. La classe [Rv] nel progetto [rdvmedecins-metier-dao] è definita come segue:


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

Ricordiamo la query JPQL che recupera gli appuntamenti:


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

I join vengono eseguiti in modo esplicito per recuperare i campi [client] e [slot]. Inoltre, grazie al join [cr.doctor.id=?1], otterremo anche il medico. Il medico apparirà quindi nella stringa JSON per ogni appuntamento. Tuttavia, queste informazioni duplicate non sono necessarie. Abbiamo visto come risolvere questo problema utilizzando un filtro JSON sull'oggetto [Creneau]. A causa delle modalità [FetchType.LAZY] dei campi [client] e [slot] nella classe [Rv], scopriremo presto la necessità di applicare un filtro JSON alla classe [RV] nel progetto [rdvmedecins-metier-dao]:


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

Controlleremo la serializzazione dell'oggetto [Rv] utilizzando il filtro [rvFilter]. Apparentemente, in questo caso, non abbiamo bisogno di filtrare perché ci servono tutti i campi dell'oggetto [Rv]. Tuttavia, poiché abbiamo specificato che la classe ha un filtro JSON, dobbiamo definirlo per qualsiasi serializzazione di un oggetto di tipo [Rv]; altrimenti, otterremo un'eccezione. Per farlo, usiamo il seguente mappatore JSON definito nella classe [rdvMedecinsController]:


    @Autowired
    private ObjectMapper jsonMapperLongRv;

Questo mapper è definito come segue nella classe di configurazione [WebConfig]:


    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
        return jsonMapperLongRv;
}
  • Riga 4: specifichiamo che tutti i campi dell'oggetto [Rv] devono essere serializzati;
  • riga 5: specifichiamo che nell'oggetto [Creneau], il campo [medecin] non deve essere serializzato;
  • riga 6: aggiungiamo i due filtri [rvFilter] e [creneauFilter] ai filtri JSON dell'oggetto [jsonMapperLongRv];

I risultati ottenuti sono i seguenti:

oppure questi con una giornata senza appuntamenti:

oppure questi con un giorno errato:

oppure questi con un medico errato:

8.4.11.11. L'URL [/getAgendaMedecinJour/{idMedecin}/{jour}]

L'URL [/getAgendaMedecinJour/{idMedecin}/{jour}] è gestito dal seguente metodo nel controller [RdvMedecinsController]:


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

Il tipo [AgendaMedecinJour] è il seguente:


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

Il tipo [CreneauMedecinJour] è il seguente:


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

I campi [creneau] e [rv] hanno filtri JSON che devono essere configurati. Questo è ciò che fa la riga 49 del metodo [getAgendaMedecinJour], utilizzando il mappatore JSON [jsonMapperLongRv] che abbiamo incontrato in precedenza:


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

I risultati ottenuti sono i seguenti:

Come si vede sopra, il 28/01/2015 il dottor PELISSIER ha un appuntamento con la signora Brigitte BISTROU alle 8:20;

oppure questi, se la data non è corretta:

oppure questi se l'ID del medico non è valido:

8.4.11.12. L'URL [/getMedecinById/{id}]

L'URL [/getMedecinById/{id}] è gestito dal seguente metodo nel [RdvMedecinsController]:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Medecin> response;
        // application status
        if (messages != null) {
            response = new Response<Medecin>(-1, messages, null);
        } else {
            response = getMedecin(id);
        }
        // answer
        return jsonMapper.writeValueAsString(response);
}
  • Righe 5, 13: Il metodo restituisce una stringa JSON di tipo [Doctor]. Questo tipo non ha alcuna annotazione di filtro JSON. Pertanto, alla riga 14, il mappatore JSON viene utilizzato senza filtri;

Riga 10: il metodo [getMedecin] è il seguente:


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

I risultati sono i seguenti:

oppure questi se l'ID del medico non è corretto:

8.4.11.13. L'URL [/getClientById/{id}]

L'URL [/getClientById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Client> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            response = getClient(id);
        }
        // answer
        return jsonMapper.writeValueAsString(response);
}
  • Righe 5, 13: Il metodo restituisce una stringa JSON di tipo [Client]. Questo tipo non ha annotazioni di filtro JSON. Pertanto, alla riga 13, il mappatore JSON viene utilizzato senza filtri;

Riga 11: il metodo [getClient] è il seguente:


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

I risultati sono i seguenti:

oppure questi se l'ID cliente non è corretto:

8.4.11.14. L'URL [/getCreneauById/{id}]

L'URL [/getCreneauById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Creneau> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // we give back the slot
            response = getCreneau(id);
        }
        // answer
        return jsonMapperShortCreneau.writeValueAsString(response);
}
  • Righe 5, 14: il metodo restituisce una stringa JSON di tipo [Response<Creneau>];

Riga 8: il metodo [getCreneau] è il seguente:


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

Rivediamo il codice dell'entità [Creneau]:


@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
  • righe 14-16: poiché il campo [doctor] è in modalità [fetch = FetchType.LAZY], non viene recuperato quando si recupera uno slot tramite il suo [id]. È quindi necessario escluderlo dalla serializzazione. Senza questa esclusione, si verifica un'eccezione. Ciò è dovuto al fatto che l'oggetto di serializzazione [mapper] chiamerà il metodo [getMedecin] per recuperare il campo [medecin]. Tuttavia, con un'implementazione JPA/Hibernate, la modalità [fetch = FetchType.LAZY] del campo [medecin] restituisce un oggetto [Creneau] il cui metodo [getMedecin] è programmato per recuperare il medico dal contesto JPA. Questo è chiamato oggetto [proxy]. Ora, ricordiamo l'architettura dell'applicazione web:

Il controller si trova nel blocco [Controllers / Actions]. Una volta all'interno di questo blocco, il concetto di contesto JPA non è più applicabile. Il contesto JPA viene creato durante le operazioni nel livello [DAO] e non persiste oltre tale livello. Pertanto, quando il controller tenta di accedere al contesto JPA, si verifica un'eccezione che indica che è chiuso. Per evitare questa eccezione, è necessario impedire la serializzazione del campo [medecin] della classe [Rv]. Questo è ciò che fa il mappatore JSON [jsonMapperShortCreneau]:


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

I risultati ottenuti sono i seguenti:

oppure questi se il numero dello slot non è corretto:

8.4.11.15. L'URL [/getRvById/{id}]

L'URL [/getRvById/{id}] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Rv> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // we recover rv
            response = getRv(id);
        }
        // answer
        return jsonMapperShortRv.writeValueAsString(response);
}
  • Righe 5, 14: Il metodo restituisce una stringa JSON di tipo [Response<Rv>];

Riga 11: il metodo [getRv] è il seguente:


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

La classe [Rv] presenta due campi annotati con [fetch = FetchType.LAZY]: i campi [creneau] e [client]. Questi campi non vengono quindi recuperati quando si recupera un [Rv] tramite la sua chiave primaria. Per gli stessi motivi di prima, devono quindi essere esclusi dalla serializzazione. Questo è ciò che fa il seguente mappatore [jsonMapperShortRv], definito nella classe [WebConfig]:


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

I risultati ottenuti sono i seguenti:

oppure questi, se il numero dell'appuntamento non è corretto:

8.4.11.16. L'URL [/ajouterRv]

L'URL [/addAppt] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
        // the answer
        Response<Rv> response = null;
        boolean erreur = false;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // retrieve posted values
        String jour;
        long idCreneau = -1;
        long idClient = -1;
        Date jourAgenda = null;
        if (!erreur) {
            // retrieve posted values
            jour = post.getJour();
            idCreneau = post.getIdCreneau();
            idClient = post.getIdClient();
            // check the date
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<>(6, messages, null);
                erreur = true;
            }
        }
        // we get the slot back
        Response<Creneau> responseCréneau = null;
        if (!erreur) {
            // we get the slot back
            responseCréneau = getCreneau(idCreneau);
            if (responseCréneau.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
            }
        }
        // we get the customer back
        Response<Client> responseClient = null;
        Creneau créneau = null;
        if (!erreur) {
            créneau = (Creneau) responseCréneau.getBody();
            // we get the customer back
            responseClient = getClient(idClient);
            if (responseClient.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
            }
        }
        if (!erreur) {
            Client client = responseClient.getBody();
            // we add the Rv
            try {
                response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
            } catch (RuntimeException e1) {
                erreur = true;
                response = new Response<>(5, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • righe 5, 67: il metodo deve restituire una stringa JSON di tipo [Response<Rv>];
  • riga 3: l'annotazione [@RequestBody PostAjouterRv post] recupera il corpo POST e lo inserisce nel parametro [PostAjouterRv post]. Questo corpo è JSON [consumes = "application/json; charset=UTF-8"] che verrà automaticamente deserializzato nel seguente tipo [PostAjouterRv]:

public class PostAjouterRv {
 
    // pOST DATA
    private String jour;
    private long idClient;
    private long idCreneau;
...
  • poi c'è del codice che è già stato incontrato in una forma o nell'altra;
  • riga 67: configurazione dei filtri JSON [creneauFilter] e [rvFilter]. Il metodo restituisce una stringa JSON di tipo [Response<Rv>], dove Rv è stato ottenuto alla riga 61. L'oggetto [Rv] incapsula un oggetto [Creneau] e un oggetto [Client]. L'oggetto [Creneau] ha una dipendenza [FetchType.LAZY] su un oggetto [Medecin] ed è stato recuperato nelle righe 36–44. È stato prelevato dal contesto JPA tramite la sua chiave primaria ed è stato recuperato senza la sua dipendenza [FetchType.LAZY]. In definitiva,
    • l'oggetto [Rv] ha tutte le sue dipendenze. Queste possono essere serializzate;
    • l'oggetto [Creneau] non ha la sua dipendenza [medecin]. Pertanto, questa dipendenza non deve essere serializzata;

Il mappatore JSON [jsonMapperLongRv] definito nella classe [WebConfig] soddisfa questi vincoli:


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

I risultati ottenuti con il client [Advanced Rest Client] sono i seguenti:

  • in [1], l'URL POST;
  • in [2], la richiesta POST;
  • in [3], il valore inviato;
  • in [4a], questo valore inviato è in formato JSON;
  • in [4b], il client indica che sta inviando JSON;
  • in [5], il server indica che sta restituendo JSON;
  • in [6], la risposta JSON del server che rappresenta l'appuntamento aggiunto. Mostra l'ID [id] dell'appuntamento aggiunto;

Otteniamo quanto segue con un numero di slot inesistente:

8.4.11.17. L'URL [/deleteAppointment]

L'URL [/deleteAppointment] viene gestito dal seguente metodo nel controller [RdvMedecinsController]:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
        // the answer
        Response<Void> response = null;
        boolean erreur = false;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // retrieve posted values
        long idRv = post.getIdRv();
        // recovering the rv
        if (!erreur) {
            Response<Rv> responseRv = getRv(idRv);
            if (responseRv.getStatus() != 0) {
                response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            // rv deletion
            try {
                application.supprimerRv(idRv);
                response = new Response<Void>(0, null, null);
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapper.writeValueAsString(response);
    }
  • riga 5: il tipo [Void] è la classe corrispondente al tipo primitivo [void];
  • righe 5, 34: il metodo restituisce una stringa JSON di tipo [Response<Void>] che non ha filtri JSON. Pertanto, alla riga 34, utilizziamo il mappatore JSON senza filtri;
  • riga 3: il metodo accetta il corpo POST come parametro, ovvero il valore inviato. Questo viene ricevuto in formato JSON [content-type="application/json; charset=UTF-8"] e automaticamente deserializzato nel seguente tipo [PostSupprimerRv]:

public class PostSupprimerRv {
 
    // pOST DATA
    private long idRv;
 
  • riga 28: quando l'eliminazione va a buon fine, viene inviata una risposta con [status=0];

I risultati ottenuti sono i seguenti:

  • in [5], il campo [status=0] indica che l'eliminazione è andata a buon fine;

Con un ID appuntamento inesistente, otteniamo quanto segue:

Abbiamo finito con il controller. Ora vediamo come eseguire il progetto.

8.4.11.18. La classe eseguibile del servizio web

La classe [Boot] [1] è la seguente:


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

Riga 10: Il metodo statico [SpringApplication.run] viene eseguito con la classe di configurazione del progetto [AppConfig] come primo parametro. Questo metodo configurerà automaticamente il progetto, avvierà il server Tomcat incorporato nelle dipendenze e distribuirà il controller [RdvMedecinsController] su di esso.

I log sono gestiti dai seguenti file [2]:

[logback.xml]


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

[application.properties]


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

Le righe 1-2 impostano un livello di registrazione specifico per alcune parti dell'applicazione:

  • riga 1: vogliamo i log dal livello [web];
  • riga 2: non vogliamo i log dal livello [JPA];
  • riga 3: nessun banner di Spring Boot;

I log durante l'esecuzione sono i seguenti:


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

Se modifichiamo il file [application.properties] come segue:


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

otteniamo i seguenti log:

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

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

Inoltre, se modifichiamo il file [logback.xml] come segue:


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

Si ottengono i seguenti log:

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

Possiamo quindi vedere che abbiamo un certo controllo sui log che appaiono nella console. Il livello [info] è spesso il livello di log appropriato.

Ora disponiamo di un servizio web operativo che può essere interrogato utilizzando un client web. Ci occuperemo ora della sicurezza di questo servizio: vogliamo che solo determinate persone possano gestire gli appuntamenti dei medici. Per farlo, utilizzeremo il framework Spring Security, un componente dell'ecosistema Spring.

8.4.12. Introduzione a Spring Security

Importeremo nuovamente una guida Spring seguendo i passaggi da 1 a 3 riportati di seguito:

  

Il progetto comprende i seguenti elementi:

  • Nella cartella [templates] troverete le pagine HTML del progetto;
  • [Application]: è la classe eseguibile del progetto;
  • [MvcConfig]: è la classe di configurazione Spring MVC;
  • [WebSecurityConfig]: è la classe di configurazione di Spring Security;

8.4.12.1. Configurazione Maven

Il progetto [3] è un progetto Maven. Esaminiamo il suo file [pom.xml] per vedere le sue dipendenze:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>org.springframework</groupId>
    <artifactId>gs-securing-web</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- tag::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- end::security[] -->
    </dependencies>
 
    <properties>
        <start-class>hello.Application</start-class>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>
  • righe 10–14: il progetto è un progetto Spring Boot;
  • righe 17–20: dipendenza dal framework [Thymeleaf];
  • righe 22–25: dipendenza dal framework Spring Security;

8.4.12.2. viste Thymeleaf

  

La vista [home.html] è la seguente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>
 
    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • riga 12: l'attributo [th:href="@{/hello}"] genererà l'attributo [href] del tag <a>. Il valore [@{/hello}] genererà il percorso [<context>/hello], dove [context] è il contesto dell'applicazione web;

Il codice HTML generato è il seguente:


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

La vista [hello.html] è la seguente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • riga 9: L'attributo [th:inline="text"] genererà il testo del tag <h1>. Questo testo contiene un'espressione $ che deve essere valutata. L'elemento [[${#httpServletRequest.remoteUser}]] è il valore dell'attributo [RemoteUser] della richiesta HTTP corrente. Questo è il nome dell'utente che ha effettuato l'accesso;
  • riga 10: un modulo HTML. L'attributo [th:action="@{/logout}"] genererà l'attributo [action] del tag [form]. Il valore [@{/logout}] genererà il percorso [<context>/logout], dove [context] è il contesto dell'applicazione web;

Il codice HTML generato è il seguente:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello user!</h1>
        <form method="post" action="/logout">
            <input type="submit" value="Sign Out" />
            <input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
        </form>
    </body>
</html>
  • riga 8: la traduzione di Hello [[${#httpServletRequest.remoteUser}]]!;
  • riga 9: la traduzione di @{/logout};
  • riga 11: un campo nascosto denominato (attributo name) _csrf;

La vista finale [login.html] è la seguente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • riga 9: l'attributo [th:if="${param.error}"] garantisce che il tag <div> venga generato solo se l'URL che visualizza la pagina di login contiene il parametro [error] (http://context/login?error);
  • riga 10: l'attributo [th:if="${param.logout}"] garantisce che il tag <div> venga generato solo se l'URL che visualizza la pagina di accesso contiene il parametro [logout] (http://context/login?logout);
  • righe 11–23: un modulo HTML;
  • riga 11: il modulo verrà inviato all'URL [<context>/login], dove <context> è il contesto dell'applicazione web;
  • riga 13: un campo di immissione denominato [username];
  • riga 17: un campo di immissione denominato [password];

Il codice HTML generato è il seguente:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
 
        <div>
            You have been logged out.
        </div>
        <form method="post" action="/login">
            <div>
                <label>
                    User Name :
                    <input type="text" name="username" />
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" name="password" />
                </label>
            </div>
            <div>
                <input type="submit" value="Sign In" />
            </div>
            <input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
        </form>
    </body>
</html>

Si noti alla riga 28 che Thymeleaf ha aggiunto un campo nascosto denominato [_csrf].

8.4.12.3. Configurazione Spring MVC

  

La classe [MvcConfig] configura il framework Spring MVC:


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
 
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }
 
}
  • riga 7: l'annotazione [@Configuration] rende la classe [MvcConfig] una classe di configurazione;
  • riga 8: la classe [MvcConfig] estende la classe [WebMvcConfigurerAdapter] per sovrascrivere determinati metodi;
  • riga 10: ridefinizione di un metodo della classe padre;
  • righe 11–16: il metodo [addViewControllers] consente di associare gli URL alle viste HTML. Qui vengono effettuate le seguenti associazioni:
URL
vista
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/modelli/login.html

Il suffisso [html] e la cartella [templates] sono i valori predefiniti utilizzati da Thymeleaf. Possono essere modificati tramite la configurazione. La cartella [templates] deve trovarsi nella radice del classpath del progetto:

Nel punto [1] sopra, le cartelle [java] e [resources] sono entrambe cartelle sorgente. Ciò significa che il loro contenuto si troverà nella radice del classpath del progetto. Pertanto, nel punto [2], le cartelle [hello] e [templates] si troveranno nella radice del classpath.

8.4.12.4. Configurazione di Spring Security

  

La classe [WebSecurityConfig] configura il framework Spring Security:


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
 
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • riga 9: l'annotazione [@Configuration] rende la classe [WebSecurityConfig] una classe di configurazione;
  • riga 10: l'annotazione [@EnableWebSecurity] rende la classe [WebSecurityConfig] una classe di configurazione di Spring Security;
  • riga 11: la classe [WebSecurity] estende la classe [WebSecurityConfigurerAdapter] per sovrascrivere determinati metodi;
  • riga 12: ridefinizione di un metodo della classe padre;
  • righe 13–16: il metodo [configure(HttpSecurity http)] viene sovrascritto per definire i diritti di accesso per i vari URL dell'applicazione;
  • riga 14: il metodo [http.authorizeRequests()] consente di associare gli URL ai diritti di accesso. Qui vengono effettuate le seguenti associazioni:
URL
regola
codice
/, /home
accesso senza autenticazione

http.authorizeRequests().antMatchers("/", "/home").permitAll()
altri URL
solo accesso autenticato
http.anyRequest().authenticated();
  • riga 15: definisce il metodo di autenticazione. L'autenticazione viene eseguita tramite un modulo URL [/login] accessibile a tutti [http.formLogin().loginPage("/login").permitAll()]. Anche il logout è accessibile a tutti;
  • righe 19–21: ridefiniscono il metodo [configure(AuthenticationManagerBuilder auth)] che gestisce gli utenti;
  • riga 20: l'autenticazione viene eseguita utilizzando utenti hardcoded [auth.inMemoryAuthentication()]. Qui viene definito un utente con il login [user], la password [password] e il ruolo [USER]. Agli utenti con lo stesso ruolo possono essere concesse le stesse autorizzazioni;

8.4.12.5. Classe eseguibile

  

La classe [Application] è la seguente:


package hello;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
 
    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }
 
}
  • Riga 8: l'annotazione [@EnableAutoConfiguration] indica a Spring Boot (riga 3) di eseguire la configurazione che lo sviluppatore non ha impostato esplicitamente;
  • riga 9: rende la classe [Application] una classe di configurazione Spring;
  • riga 10: indica al sistema di scansionare la directory contenente la classe [Application] per cercare i componenti Spring. Le due classi [MvcConfig] e [WebSecurityConfig] verranno quindi individuate poiché presentano l'annotazione [@Configuration];
  • riga 13: il metodo [main] della classe eseguibile;
  • riga 14: il metodo statico [SpringApplication.run] viene eseguito con la classe di configurazione [Application] come parametro. Abbiamo già incontrato questo processo e sappiamo che il server Tomcat incorporato nelle dipendenze Maven del progetto verrà avviato e il progetto distribuito su di esso. Abbiamo visto che sono stati gestiti quattro URL [/, /home, /login, /hello] e che alcuni erano protetti da diritti di accesso.

8.4.12.6. Test dell'applicazione

Iniziamo richiedendo l'URL [/], che è uno dei quattro URL accettati. È associato alla vista [/templates/home.html]:

 

L'URL richiesto [/] è accessibile a tutti. Ecco perché siamo riusciti a recuperarlo. Il link [qui] è il seguente:

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

L'URL [/hello] verrà richiesto quando clicchiamo sul link. Questo è protetto:

URL
regola
codice
/, /home
accesso senza autenticazione

http.authorizeRequests().antMatchers("/", "/home").permitAll()
altri URL
solo accesso autenticato
http.anyRequest().authenticated();

Per accedervi è necessario essere autenticati. Spring Security reindirizzerà quindi il browser del client alla pagina di autenticazione. In base alla configurazione mostrata, si tratta della pagina all'URL [/login]. Questa pagina è accessibile a tutti:


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

Quindi otteniamo [1]:

Il codice sorgente della pagina risultante è il seguente:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • alla riga 7, compare un campo nascosto che non è presente nella pagina originale [login.html]. È stato aggiunto da Thymeleaf. Questo codice, noto come CSRF (Cross-Site Request Forgery), è progettato per eliminare una vulnerabilità di sicurezza. Questo token deve essere rinviato a Spring Security insieme all'autenticazione affinché venga accettato;

Ricordiamo che solo la coppia utente/password viene riconosciuta da Spring Security. Se inseriamo qualcos'altro in [2], otteniamo la stessa pagina con un messaggio di errore in [3]. Spring Security ha reindirizzato il browser all'URL [http://localhost:8080/login?error]. La presenza del parametro [error] ha attivato la visualizzazione del tag:


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

Ora, inseriamo i valori previsti per utente/password [4]:

  • in [4], effettuiamo l'accesso;
  • In [5], Spring Security ci reindirizza all'URL [/hello] poiché si tratta dell'URL che avevamo richiesto quando siamo stati reindirizzati alla pagina di accesso. L'identità dell'utente è stata visualizzata dalla seguente riga del file [hello.html]:

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

La pagina [5] mostra il seguente modulo:


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

Quando si fa clic sul pulsante [Esci], viene inviata una richiesta POST all'URL [/logout]. Come l'URL [/login], questo URL è accessibile a tutti:


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

Nella nostra mappatura URL/vista, non abbiamo definito nulla per l'URL [/logout]. Cosa succederà? Proviamo:

  • In [6], clicchiamo sul pulsante [Sign Out];
  • in [7], vediamo che siamo stati reindirizzati all'URL [http://localhost:8080/login?logout]. Spring Security ha richiesto questo reindirizzamento. La presenza del parametro [logout] nell'URL ha fatto sì che nella vista venisse visualizzata la seguente riga:

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

8.4.12.7. Conclusione

Nell'esempio precedente, avremmo potuto scrivere prima l'applicazione web e poi proteggerla in un secondo momento. Spring Security è non intrusivo. È possibile implementare la sicurezza per un'applicazione web che è già stata scritta. Inoltre, abbiamo scoperto i seguenti punti:

  • è possibile definire una pagina di autenticazione;
  • l'autenticazione deve essere accompagnata dal token CSRF emesso da Spring Security;
  • se l'autenticazione fallisce, si viene reindirizzati alla pagina di autenticazione con un parametro di errore aggiuntivo nell'URL;
  • se l'autenticazione ha esito positivo, si viene reindirizzati alla pagina richiesta al momento dell'autenticazione. Se si richiede la pagina di autenticazione direttamente senza passare attraverso una pagina intermedia, Spring Security reindirizza all'URL [/] (questo caso non è stato dimostrato);
  • Si effettua il logout richiedendo l'URL [/logout] con una richiesta POST. Spring Security reindirizza quindi alla pagina di autenticazione con il parametro "logout" nell'URL;

Tutte queste conclusioni si basano sul comportamento predefinito di Spring Security. Questo comportamento può essere modificato tramite configurazione sovrascrivendo determinati metodi della classe [WebSecurityConfigurerAdapter].

Il tutorial precedente ci sarà di scarso aiuto per il prosieguo. Utilizzeremo infatti:

  • un database per memorizzare gli utenti, le loro password e i loro ruoli;
  • l'autenticazione basata su header HTTP;

Esistono relativamente pochi tutorial su ciò che vogliamo fare qui. La soluzione che proporremo è una combinazione di frammenti di codice trovati qua e là.

8.4.13. Implementazione della sicurezza sul servizio web di appuntamenti

8.4.13.1. Il database

Il database [rdvmedecins] è in fase di aggiornamento per includere gli utenti, le loro password e i loro ruoli. Sono state aggiunte tre nuove tabelle:

Image

Tabella [USERS]: users

  • ID: chiave primaria;
  • VERSION: colonna di versioning delle righe;
  • IDENTITY: identificatore descrittivo dell'utente;
  • LOGIN: nome utente;
  • PASSWORD: la sua password;

Nella tabella USERS, le password non sono memorizzate in chiaro:

 

L'algoritmo utilizzato per crittografare le password è l'algoritmo BCRYPT.

Tabella [ROLES]: ruoli

  • ID: chiave primaria;
  • VERSION: colonna di versioning per la riga;
  • NAME: nome del ruolo. Per impostazione predefinita, Spring Security si aspetta nomi nel formato ROLE_XX, ad esempio ROLE_ADMIN o ROLE_GUEST;
 

Tabella [USERS_ROLES]: tabella di join USERS/ROLES

Un utente può avere più ruoli e un ruolo può includere più utenti. Si tratta di una relazione molti-a-molti rappresentata dalla tabella [USERS_ROLES].

  • ID: chiave primaria;
  • VERSION: colonna di versioning delle righe;
  • USER_ID: identificatore utente;
  • ROLE_ID: identificatore di un ruolo;
 

Poiché stiamo modificando il database, è necessario modificare tutti i livelli del progetto [logica di business, DAO, JPA]:

8.4.13.2. Il nuovo progetto STS per [logica di business, DAO, JPA]

Il progetto [rdvmedecins-business-dao] si evolve come segue:

  • in [1]: il nuovo progetto;
  • in [2]: le modifiche introdotte dall'implementazione della sicurezza sono state raggruppate in un unico pacchetto [rdvmedecins.security]. Questi nuovi elementi appartengono ai livelli [JPA] e [DAO], ma per semplicità sono stati raggruppati nello stesso pacchetto.

8.4.13.3. Le nuove entità [JPA]

Il livello JPA definisce tre nuove entità:

  

La classe [User] rappresenta la tabella [USERS]:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // properties
    private String identity;
    private String login;
    private String password;
 
    // manufacturer
    public User() {
    }
 
    public User(String identity, String login, String password) {
        this.identity = identity;
        this.login = login;
        this.password = password;
    }
 
    // identity
    @Override
    public String toString() {
        return String.format("User[%s,%s,%s]", identity, login, password);
    }
 
    // getters and setters
....
}
  • riga 9: la classe estende la classe [AbstractEntity] già utilizzata per le altre entità;
  • righe 13–15: non vengono specificati nomi di colonne perché hanno gli stessi nomi dei campi associati;

La classe [Role] rispecchia la tabella [ROLES]:


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

La classe [UserRole] rappresenta la tabella [USERS_ROLES]:


package rdvmedecins.entities;
 
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
 
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
 
    // a UserRole refers to a User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // a UserRole refers to a Role
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;
 
    // getters and setters
...
}
  • righe 15-17: definisci la chiave esterna dalla tabella [USERS_ROLES] alla tabella [USERS];
  • righe 19-21: implementano la chiave esterna dalla tabella [USERS_ROLES] alla tabella [ROLES];

8.4.13.4. Modifiche al livello [DAO]

Il livello [DAO] è stato potenziato con tre nuovi [Repository]:

  

L'interfaccia [UserRepository] gestisce l'accesso alle entità [User]:


package rdvmedecins.repositories;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
 
public interface UserRepository extends CrudRepository<User, Long> {
 
    // liste des rôles d'un utilisateur identifié par son id
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);
 
    // liste des rôles d'un utilisateur identifié par son login et son mot de passe
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);
 
    // recherche d'un utilisateur via son login
    User findUserByLogin(String login);
}
  • riga 9: l'interfaccia [UserRepository] estende l'interfaccia [CrudRepository] di Spring Data (riga 4);
  • righe 12-13: il metodo [getRoles(User user)] recupera tutti i ruoli per un utente identificato dal proprio [id]
  • righe 16-17: come sopra, ma per un utente identificato dal proprio login e password;
  • Riga 20: per trovare un utente tramite il suo nome utente;

L'interfaccia [RoleRepository] gestisce l'accesso alle entità [Role]:


package rdvmedecins.security;
 
import org.springframework.data.repository.CrudRepository;
 
public interface RoleRepository extends CrudRepository<Role, Long> {
 
    // search for a role by name
    Role findRoleByName(String name);
 
}
  • riga 5: l'interfaccia [RoleRepository] estende l'interfaccia [CrudRepository];
  • riga 8: è possibile cercare un ruolo in base al suo nome;

L'interfaccia [userRoleRepository] gestisce l'accesso alle entità [UserRole]:


package rdvmedecins.security;
 
import org.springframework.data.repository.CrudRepository;
 
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
 
}
  • riga 5: l'interfaccia [UserRoleRepository] estende semplicemente l'interfaccia [CrudRepository] senza aggiungere alcun nuovo metodo;

8.4.13.5. Classi di gestione utenti e ruoli

  

Spring Security richiede la creazione di una classe che implementi la seguente interfaccia [UsersDetail]:

 

Questa interfaccia è implementata qui dalla classe [AppUserDetails]:


package rdvmedecins.security;
 
import java.util.ArrayList;
import java.util.Collection;
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // properties
    private User user;
    private UserRepository userRepository;
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }
 
    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
 
    @Override
    public String getPassword() {
        return user.getPassword();
    }
 
    @Override
    public String getUsername() {
        return user.getLogin();
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
 
    // getters and setters
    ...
}
  • riga 10: la classe [AppUserDetails] implementa l'interfaccia [UserDetails];
  • righe 15–16: la classe incapsula un utente (riga 15) e il repository che fornisce dettagli su quell'utente (riga 16);
  • righe 22–25: il costruttore che istanzia la classe con un utente e il relativo repository;
  • righe 28–35: implementazione del metodo [getAuthorities] dell'interfaccia [UserDetails]. Deve costruire una collezione di elementi di tipo [GrantedAuthority] o di un tipo derivato. Qui utilizziamo il tipo derivato [SimpleGrantedAuthority] (riga 32), che incapsula il nome di uno dei ruoli dell'utente della riga 15;
  • righe 31–33: iteriamo attraverso l'elenco dei ruoli dell'utente della riga 15 per costruire un elenco di elementi di tipo [SimpleGrantedAuthority];
  • righe 38–40: implementiamo il metodo [getPassword] dell'interfaccia [UserDetails]. Restituiamo la password dell'utente della riga 15;
  • righe 38–40: implementiamo il metodo [getUserName] dell'interfaccia [UserDetails]. Restituiamo il nome utente dell'utente della riga 15;
  • righe 47–50: l'account dell'utente non scade mai;
  • righe 52–55: l'account dell'utente non viene mai bloccato;
  • righe 57–60: le credenziali dell'utente non scadono mai;
  • righe 62–65: l'account dell'utente è sempre attivo;

Spring Security richiede inoltre l'esistenza di una classe che implementi l'interfaccia [AppUserDetailsService]:

 

Questa interfaccia è implementata dalla seguente classe [AppUserDetailsService]:


package rdvmedecins.security;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
 
@Service
public class AppUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // search for user via login
        User user = userRepository.findUserByLogin(login);
        // found?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(user, userRepository);
    }
 
}
  • riga 9: la classe sarà un componente Spring, quindi sarà disponibile nel suo contesto;
  • righe 12–13: il componente [UserRepository] verrà iniettato qui;
  • righe 16–25: implementazione del metodo [loadUserByUsername] dell'interfaccia [UserDetailsService] (riga 10). Il parametro è il login dell'utente;
  • riga 18: l'utente viene cercato utilizzando il suo nome utente;
  • righe 20–22: se l'utente non viene trovato, viene generata un'eccezione;
  • riga 24: viene costruito e restituito un oggetto [AppUserDetails]. È infatti di tipo [UserDetails] (riga 16);

8.4.13.6. Test del livello [DAO]

  

Per prima cosa, creiamo una classe eseguibile [CreateUser] in grado di creare un utente con un ruolo:


package rdvmedecins.security;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
 
public class CreateUser {
 
    public static void main(String[] args) {
        // syntax: login password roleName
 
        // three parameters are required
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // parameters are retrieved
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // does the role already exist?
        Role role = roleRepository.findRoleByName(roleName);
        // if it doesn't exist, we create it
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // does the user already exist?
        User user = userRepository.findUserByLogin(login);
        // if it doesn't exist, we create it
        if (user == null) {
            // hash the password with bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // save user
            user = userRepository.save(new User(login, login, crypt));
            // we create the relationship with the role
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // the user already exists - does he/she have the required role?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // if not found, we create the relationship with the role
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }
 
        // closing Spring context
        context.close();
    }
 
}
  • riga 17: la classe richiede tre argomenti che definiscono un utente: login, password e ruolo;
  • righe 25–27: i tre parametri vengono recuperati;
  • riga 29: il contesto Spring viene creato dalla classe di configurazione [DomainAndPersistenceConfig]. Questa classe era già presente nel progetto iniziale. Deve essere aggiornata come segue:

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
  • Riga 1: è necessario specificare che ora sono presenti componenti [Repository] nel pacchetto [rdvmedecins.security];
  • riga 4: è necessario specificare che ora nel pacchetto [rdvmedecins.security] sono presenti entità JPA;

Torniamo al codice per la creazione di un utente:

  • righe 30–32: recuperiamo i riferimenti dei tre oggetti [Repository] che potrebbero essere utili per la creazione dell'utente;
  • riga 34: verifichiamo se il ruolo esiste già;
  • righe 36–38: in caso contrario, lo creiamo nel database. Avrà un nome del tipo [ROLE_XX];
  • riga 40: controlliamo se il login esiste già;
  • righe 42-49: se il nome utente non esiste, lo creiamo nel database;
  • riga 44: crittografiamo la password. Qui utilizziamo la classe [BCrypt] di Spring Security (riga 4). Abbiamo quindi bisogno degli archivi per questo framework. Il file [pom.xml] include una nuova dipendenza:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • Riga 46: l'utente viene salvato nel database;
  • riga 48: così come la relazione che lo collega al suo ruolo;
  • righe 51–57: se l'account esiste già, verifichiamo se il ruolo che vogliamo assegnargli è già tra i suoi ruoli;
  • righe 59–61: se il ruolo desiderato non viene trovato, viene creata una riga nella tabella [USERS_ROLES] per collegare l'utente al proprio ruolo;
  • Non abbiamo previsto alcuna protezione contro potenziali eccezioni. Si tratta di una classe di supporto per la creazione rapida di un utente con un ruolo.

Quando la classe viene eseguita con gli argomenti [x x guest], si ottengono i seguenti risultati nel database:

Tabella [USERS]

Tabella [RUOLI]

 

Tabella [USERS_ROLES]

 

Consideriamo ora la seconda classe [UsersTest], che è un test JUnit:

  

package rdvmedecins.security;
 
import java.util.List;
 
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
 
import com.google.common.collect.Lists;
 
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
 
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;
 
    @Test
    public void findAllUsersWithTheirRoles() {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(user);
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }
 
    @Test
    public void findUserByLogin() {
        // user [admin] is retrieved
        User user = userRepository.findUserByLogin("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // check admin / admin role
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }
 
    @Test
    public void loadUserByUsername() {
        // user [admin] is retrieved
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // check admin / admin role
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }
 
    // utility method - displays items in a collection
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • righe 27–34: test visivo. Visualizziamo tutti gli utenti insieme ai loro ruoli;
  • righe 36–46: verifichiamo che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando [UserRepository];
  • riga 41: [admin] è la password in chiaro. Nel database, è crittografata utilizzando l'algoritmo BCrypt. Il metodo [BCrypt.checkpw] verifica che la password in chiaro crittografata corrisponda a quella nel database;
  • righe 48–59: verifichiamo che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando [appUserDetailsService];

I test vengono eseguiti con successo con i seguenti log:

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

8.4.13.7. Conclusione provvisoria

Le classi necessarie per Spring Security sono state aggiunte con modifiche minime al progetto originale. Riassumendo:

  • aggiunta di una dipendenza da Spring Security nel file [pom.xml];
  • creazione di tre tabelle aggiuntive nel database;
  • creazione di entità JPA e componenti Spring nel pacchetto [rdvmedecins.security];

Questo scenario molto favorevole deriva dal fatto che le tre tabelle aggiunte al database sono indipendenti da quelle esistenti. Avremmo potuto persino collocarle in un database separato. Ciò è stato possibile perché abbiamo deciso che un utente esistesse indipendentemente dai medici e dai clienti. Se questi ultimi fossero stati potenziali utenti, avremmo dovuto creare collegamenti tra la tabella [USERS] e le tabelle [MEDECINS] e [CLIENTS]. Ciò avrebbe avuto un impatto significativo sul progetto esistente.

8.4.13.8. Il progetto STS per il livello [web]

Il progetto [rdvmedecins-webjson] si sta evolvendo come segue[1]:

Le modifiche principali devono essere apportate nel file [rdvmedecins.web.config], dove è necessario configurare Spring Security. Ci sono altre modifiche minori nelle classi [AppConfig] e [ApplicationModel]. Abbiamo già incontrato una classe di configurazione di Spring Security:


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

Seguiremo la stessa procedura:

  • riga 11: definire una classe che estende la classe [WebSecurityConfigurerAdapter];
  • riga 13: definire un metodo [configure(HttpSecurity http)] che definisca i diritti di accesso ai vari URL del servizio web;
  • riga 19: definire un metodo [configure(AuthenticationManagerBuilder auth)] che definisce gli utenti e i loro ruoli;

La configurazione di Spring Security è gestita dalla classe [SecurityConfig]:


package rdvmedecins.web.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;
 
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;
    @Autowired
    private ApplicationModel application;
 
    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // authentication is performed by bean [appUserDetailsService]
        // the password is encrypted using the BCrypt hash algorithm
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // secure application?
        if (application.isSecured()) {
            // the password is transmitted by the header Authorization: Basic xxxx
            http.httpBasic();
            // the HTTP OPTIONS method must be authorized for all
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // only the ADMIN role can use the application
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // all URL
                    .hasRole("ADMIN");
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • riga 15: la classe [SecurityConfig] è una classe di configurazione Spring;
  • riga 16: per configurare la sicurezza del progetto;
  • righe 19–20: viene iniettata la classe [AppUserDetails], che fornisce l'accesso agli utenti dell'applicazione;
  • righe 21–22: viene iniettata la classe [ApplicationModel], che funge da cache per l'applicazione web. Abbiamo scelto di utilizzarla anche qui per configurare l'applicazione web in un unico punto. Essa definisce il valore booleano [isSecured] alla riga 36. Questo valore booleano protegge (true) o non protegge (false) l'applicazione web;
  • righe 25–29: il metodo [configure(HttpSecurity http)] definisce gli utenti e i loro ruoli. Accetta un tipo [AuthenticationManagerBuilder] come parametro. Questo parametro è arricchito con due informazioni (riga 28):
    • un riferimento all'[appUserDetailsService] della riga 20, che fornisce l'accesso agli utenti registrati. Si noti qui che il fatto che siano memorizzati in un database non è esplicitamente dichiarato. Potrebbero quindi trovarsi in una cache, essere forniti da un servizio web, ecc.
    • il tipo di crittografia utilizzato per la password. Ricordiamo che abbiamo utilizzato l'algoritmo BCrypt;
  • righe 38–47: il metodo [configure(HttpSecurity http)] definisce i diritti di accesso agli URL del servizio web;
  • riga 34: abbiamo visto nel progetto introduttivo che, per impostazione predefinita, Spring Security gestisce un token CSRF (Cross-Site Request Forgery) che l'utente che tenta di autenticarsi deve inviare al server. Qui, questo meccanismo è disabilitato. In combinazione con il valore booleano (isSecured=false), ciò consente di utilizzare l'applicazione web senza sicurezza;
  • riga 38: abilitiamo l'autenticazione tramite header HTTP. Il client deve inviare il seguente header HTTP:
Authorization:Basic code

dove code è la codifica Base64 della stringa login:password. Ad esempio, la codifica Base64 della stringa admin:admin è YWRtaW46YWRtaW4=. Pertanto, un utente con login [admin] e password [admin] invierà la seguente intestazione HTTP per autenticarsi:

Authorization:Basic YWRtaW46YWRtaW4=
  • Righe 40–42: indicano che tutti gli URL del servizio web sono accessibili agli utenti con il ruolo [ROLE_ADMIN]. Ciò significa che un utente senza questo ruolo non può accedere al servizio web;
  • Riga 47: la password dell'utente può essere memorizzata o meno in una sessione. Se viene memorizzata, l'utente deve autenticarsi solo la prima volta. Nelle richieste successive, le credenziali non vengono richieste. In questo caso, abbiamo scelto una modalità senza sessione. Ogni richiesta deve essere accompagnata da credenziali di sicurezza;

La classe [AppConfig], che configura l'intera applicazione, viene aggiornata come segue:

  

package rdvmedecins.web.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
 
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
 
}
  • La modifica avviene alla riga 11: viene aggiunta la classe di configurazione [SecurityConfig];

Infine, la classe [ApplicationModel] viene potenziata con un valore booleano:


@Component
public class ApplicationModel implements IMetier {
 
...
    // configuration data
    private boolean secured = false;
 
    public boolean isSecured() {
        return secured;
}
  • Riga 6: Imposta il valore booleano [secured] su [true / false] a seconda che tu voglia abilitare la sicurezza.

8.4.13.9. Test del servizio web

Testeremo il servizio web utilizzando il client Chrome [Advanced Rest Client]. Dovremo specificare l'intestazione di autenticazione HTTP:

Authorization:Basic code

dove [code] è la stringa [login:password] codificata in Base64. Per generare questo codice, è possibile utilizzare il seguente programma:

  

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

Se eseguiamo questo programma con i due argomenti [admin admin]:

  

otteniamo il seguente risultato:

YWRtaW46YWRtaW4=

Ora che sappiamo come generare l'intestazione di autenticazione HTTP, avviamo il servizio web, ora sicuro:


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

Quindi, utilizzando il client Chrome [Advanced Rest Client], richiediamo l'elenco di tutti i medici:

  • In [1], richiediamo l'URL dei medici;
  • in [2], utilizzando un metodo GET;
  • in [3], forniamo l'intestazione di autenticazione HTTP. Il codice [YWRtaW46YWRtaW4=] è la codifica Base64 della stringa [admin:admin];
  • in [4], inviamo la richiesta HTTP;

La risposta del server è la seguente:

  • in [1], l'intestazione di autenticazione HTTP;
  • in [2], il server restituisce una risposta JSON;
  • in [3], un elenco di intestazioni HTTP relative alla sicurezza dell'applicazione web;

Otteniamo con successo l'elenco dei medici:

 

Ora proviamo a inviare una richiesta HTTP con un'intestazione di autenticazione errata. La risposta è quindi la seguente:

  • in [1] e [3]: l'intestazione di autenticazione HTTP;
  • in [2]: la risposta del servizio web;

Ora proviamo con l'utente "user / user". Esiste ma non ha accesso al servizio web. Se eseguiamo il programma di codifica Base64 con i due argomenti [user user]:

  

otteniamo il seguente risultato:

dXNlcjp1c2Vy
  • in [1] e [3]: l'intestazione di autenticazione HTTP;
  • in [2]: la risposta del servizio web. È diversa dalla precedente, che era [401 Non autorizzato]. In questo caso, l'utente si è autenticato con successo ma non dispone delle autorizzazioni sufficienti per accedere all'URL;

Un servizio web sicuro è ora operativo. Lo estenderemo per consentire le richieste cross-domain. Questo requisito è stato menzionato nel documento [Tutorial AngularJS / Spring 4] e, sebbene non sia applicabile in questo caso, lo affronteremo comunque.

8.4.14. Implementazione delle richieste cross-domain

Esaminiamo la questione delle richieste cross-domain. Nel documento [AngularJS / Spring 4 Tutorial], stiamo sviluppando un'applicazione client/server in cui il client è un'applicazione AngularJS:

  • le pagine HTML/CSS/JS dell'applicazione Angular provengono dal server [1];
  • in [2], il servizio [dao] effettua una richiesta a un altro server, il server [2]. Tuttavia, ciò è vietato dal browser che esegue l'applicazione Angular poiché costituisce una vulnerabilità di sicurezza. L'applicazione può interrogare solo il server da cui ha origine, ovvero il server [1];

In realtà, non è corretto dire che il browser impedisca all'applicazione Angular di interrogare il server [2]. In realtà lo interroga per chiedere se consente a un client che non proviene da esso di interrogarlo. Questa tecnica di condivisione è chiamata CORS (Cross-Origin Resource Sharing). Il server [2] concede l'autorizzazione inviando specifici header HTTP.

Per dimostrare i problemi che possono sorgere, creeremo un'applicazione client/server in cui:

  • il server sarà il nostro server web/JSON;
  • il client sarà una semplice pagina HTML dotata di codice JavaScript che effettuerà richieste al server web/JSON;

8.4.14.1. Il progetto client

  

Il progetto è un progetto Maven con il seguente file [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
 
        <groupId>istia.st</groupId>
        <artifactId>rdvmedecins-webjson-client-cors</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
 
        <name>rdvmedecins-webjson-client-cors</name>
        <description>Client for webjson server</description>
 
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
                <relativePath /> <!-- lookup parent from repository -->
        </parent>
 
        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <start-class>istia.st.rdvmedecins.Client</start-class>
                <java.version>1.8</java.version>
        </properties>
 
        <dependencies>
                <!-- spring MVC -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
        </dependencies>
</project>
  • righe 14–19: questo è un progetto Spring Boot;
  • righe 29–32: utilizziamo la dipendenza [spring-boot-starter-web], che include un server Tomcat e Spring MVC;

La pagina HTML è la seguente:

 

È generato dal seguente codice:


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

Il codice [client.js] è il seguente:


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

Lasciamo al lettore il compito di comprendere questo codice. Tutto è già stato trattato in un momento o nell'altro. Tuttavia, alcune righe meritano una spiegazione:

  • riga 11:
    • [document] si riferisce al documento caricato dal browser, noto come DOM (Document Object Model),
    • [document.forms[0]] si riferisce al primo modulo nel documento; un documento può contenere più moduli. Qui ce n'è solo uno,
    • [document.forms[0].elements['method']] si riferisce all'elemento del modulo con l'attributo [name='method']. Ce ne sono due:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • riga 11:
    • [document.forms[0].elements['method'].value] è il valore che verrà inviato per il componente con l'attributo [name='method']. Sappiamo che il valore inviato è il valore dell'attributo [value] del pulsante di opzione selezionato. Qui, quindi, sarà una delle stringhe ['get', 'post'];
  • righe 23–25: stiamo comunicando con un server che richiede un'intestazione HTTP [Authorization: Basic code]. Creiamo questa intestazione per l'utente [admin / admin], che è l'unico autorizzato a interrogare il server;
  • riga 26: l'utente inserirà URL del tipo [/getAllDoctors, /deleteAppointment, ...]. Questi URL devono quindi essere completati;
  • riga 28: il server restituisce JSON, che è un formato di testo. Specifichiamo il tipo [text/plain] come tipo di risposta in modo che venga visualizzato esattamente come ricevuto;
  • riga 33: visualizza la risposta testuale del server;
  • riga 39: visualizza eventuali messaggi di errore in formato testo;
  • riga 52: per indicare che il client sta inviando JSON;

Nell'applicazione client/server che stiamo realizzando:

  • il client è un'applicazione web disponibile all'URL [http://localhost:8081]. Questa è l'applicazione che stiamo attualmente realizzando;
  • il server è un'applicazione web disponibile all'URL [http://localhost:8080]. Questo è il nostro server web/JSON;

Poiché il client non è in esecuzione sulla stessa porta del server, si pone il problema delle richieste cross-domain. [http://localhost:8080] e [http://localhost:8081] sono due domini diversi.

L'applicazione Spring Boot è un'applicazione console avviata dalla seguente classe eseguibile [Client]:


package istia.st.rdvmedecins;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {
 
    public static void main(String[] args) {
        SpringApplication.run(Client.class, args);
    }
 
    // static pages
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
    }
 
    // configuration dispatcherServlet
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }
 
    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }
 
    // embedded Tomcat server
    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8081);
    }
 
}
  • Riga 14: la classe [Client] è una classe di configurazione Spring;
  • riga 15: viene configurata un'applicazione Spring MVC. Questa annotazione attiva una serie di configurazioni automatiche;
  • riga 16: per sovrascrivere determinati valori predefiniti del framework Spring MVC, è necessario estendere la classe [WebMvcConfigurerAdapter];
  • righe 23–26: il metodo [addResourceHandlers] consente di specificare le directory in cui si trovano le risorse statiche dell'applicazione (HTML, CSS, JS, ecc.). Qui specifichiamo la directory [static] situata nel classpath del progetto:
  
  • righe 29–37: configurazione del bean [dispatcherServlet], che designa il servlet Spring MVC;
  • righe 40-43: il server Tomcat incorporato funzionerà sulla porta 8081;

8.4.14.2. L'URL [/getAllMedecins]

Avviamo:

  • il server web/JSON sulla porta 8080;
  • il client per questo server sulla porta 8081;

quindi richiediamo l'URL [http://localhost:8081/client.html] [1]:

  • in [2], eseguiamo una richiesta GET sull'URL [http://localhost:8080/getAllMedecins];

Non riceviamo alcuna risposta dal server. Quando controlliamo la console degli sviluppatori (Ctrl-Shift-I), vediamo un errore:

  • in [1], ci troviamo nella scheda [Rete];
  • In [2], vediamo che la richiesta HTTP effettuata non è [GET] ma [OPTIONS]. Nel caso di una richiesta cross-domain, il browser verifica con il server che determinate condizioni siano soddisfatte inviando una richiesta HTTP [OPTIONS]. In questo caso, le richieste sono quelle indicate dai cerchi [5-6];
  • In [5], il browser chiede se l'URL di destinazione sia raggiungibile con un GET. L'intestazione della richiesta [Access-Control-Request-Method] richiede una risposta con un'intestazione HTTP [Access-Control-Allow-Methods] che indichi che il metodo richiesto è accettato;
  • in [5], il browser invia l'intestazione HTTP [Origin: http://localhost:8081]. Questa intestazione richiede una risposta in un'intestazione HTTP [Access-Control-Allow-Origin] che indichi che l'origine specificata è accettata;
  • In [6], il browser chiede se le intestazioni HTTP [Accept] e [Authorization] sono accettate. L'intestazione di richiesta [Access-Control-Request-Headers] si aspetta una risposta con un'intestazione HTTP [Access-Control-Allow-Headers] che indichi che le intestazioni richieste sono accettate;
  • si verifica un errore in [3]. Facendo clic sull'icona si ottiene l'errore [4];
  • in [4], il messaggio indica che il server non ha inviato l'intestazione HTTP [Access-Control-Allow-Origin], che specifica se l'origine della richiesta è accettata;
  • In [7], possiamo vedere che il server effettivamente non ha inviato questa intestazione. Di conseguenza, il browser ha rifiutato di effettuare la richiesta HTTP GET inizialmente richiesta;

Dobbiamo modificare il server web/JSON. Apportiamo una modifica iniziale in [ApplicationModel], che è uno degli elementi di configurazione del servizio web:

 

@Component
public class ApplicationModel implements IMetier {
 
    ...
    // configuration data
    private boolean corsAllowed = true;
    private boolean secured = true;
 
...
    public boolean isCorsAllowed() {
        return corsAllowed;
}
  • riga 6: creiamo una variabile booleana che indica se i client al di fuori del dominio del server sono accettati o meno;
  • righe 10–12: il metodo per accedere a queste informazioni;

Quindi creiamo un nuovo controller Spring MVC:

  

La classe [RdvMedecinsCorsController] è la seguente:


package rdvmedecins.web.controllers;
 
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
import rdvmedecins.web.models.ApplicationModel;
 
@Controller
public class RdvMedecinsCorsController {
 
    @Autowired
    private ApplicationModel application;
 
    // sending options to the customer
    public void sendOptions(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // we authorize GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        sendOptions(origin, response);
    }
}
  • righe 12-13: la classe [RdvMedecinsCorsController] è un controller Spring;
  • righe 33–36: definiscono un'azione che gestisce l'URL [/getAllMedecins] quando viene richiesto con il metodo HTTP [OPTIONS];
  • riga 34: il metodo [getAllMedecins] accetta i seguenti parametri:
    • l'oggetto [@RequestHeader(value = "Origin", required = false)] che recupera l'intestazione HTTP [Origin] dalla richiesta. Questa intestazione è stata inviata dal mittente della richiesta:
Origin:http://localhost:8081

Specifichiamo che l'intestazione HTTP [Origin] è facoltativa [required = false]. In questo caso, se l'intestazione manca, il parametro [String origin] avrà il valore null. Con [required = true], che è il valore predefinito, viene generata un'eccezione se l'intestazione manca. Volevamo evitare questo scenario;

  • riga 34:
    • l'oggetto [HttpServletResponse response] che verrà inviato al client che ha effettuato la richiesta;

Questi due parametri vengono iniettati da Spring;

  • riga 35: deleghiamo l'elaborazione della richiesta al metodo nelle righe 19–30;
  • righe 15–16: viene iniettato l'oggetto [ApplicationModel];
  • righe 21–23: se l'applicazione è configurata per accettare richieste cross-domain, e se il mittente ha inviato l'intestazione HTTP [Origin], e se tale origine inizia con [http://localhost], allora accettiamo la richiesta cross-domain; altrimenti, la rifiutiamo;
  • riga 25: se il client si trova nel dominio [http://localhost:port], viene inviata l'intestazione HTTP:

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

il che significa che il server accetta l'origine del client;

  • Riga 25: Abbiamo specificato due header HTTP specifici nella richiesta HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

In risposta all'intestazione HTTP [Access-Control-Request-X], il server risponde con un'intestazione HTTP [Access-Control-Allow-X] che specifica ciò che è consentito. Le righe 23–26 ripetono semplicemente la richiesta del client per indicare che è stata accettata;

Ora siamo pronti per ulteriori test. Lanciamo la nuova versione del servizio web e scopriamo che il problema rimane invariato. Non è cambiato nulla. Se aggiungiamo un output della console alla riga 35 sopra, non viene mai visualizzato, indicando che il metodo [getAllMedecins] alla riga 34 non viene mai chiamato.

Dopo alcune ricerche, scopriamo che Spring MVC gestisce autonomamente le richieste HTTP [OPTIONS] utilizzando la sua gestione predefinita. Pertanto, è sempre Spring a rispondere, e mai il metodo [getAllMedecins] alla riga 34. Questo comportamento predefinito di Spring MVC può essere modificato. Modifichiamo la classe [WebConfig] esistente:

  

package rdvmedecins.web.config;
 
...
import org.springframework.web.servlet.DispatcherServlet;
 
@Configuration
public class WebConfig {
 
    // dispatcherservlet configuration for CORS headers
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
 
    // mapping jSON
...
  • righe 10-11: il bean [dispatcherServlet] viene utilizzato per definire il servlet che gestisce le richieste dei client. In questo caso, è di tipo [DispatcherServlet], il servlet del framework Spring MVC;
  • riga 12: creiamo un'istanza di tipo [DispatcherServlet];
  • riga 13: istruiamo il servlet a inoltrare le richieste HTTP [OPTIONS] all'applicazione;
  • riga 14: visualizziamo il servlet così configurato;

Eseguiamo nuovamente i test con questa nuova configurazione. Otteniamo il seguente risultato:

  • in [1], vediamo che ci sono due richieste HTTP all'URL [http://localhost:8080/getAllMedecins];
  • in [2], la richiesta [OPTIONS];
  • in [3], le tre intestazioni HTTP che abbiamo appena configurato nella risposta del server;

Ora esaminiamo la seconda richiesta:

  • in [1], la richiesta in esame;
  • in [2], questa è la richiesta GET. Grazie alla prima richiesta [OPTIONS], il browser ha ricevuto le informazioni richieste. Ora sta eseguendo la richiesta [GET] inizialmente richiesta;
  • in [3], la risposta del server;
  • in [4], il server invia JSON;
  • in [5], si è verificato un errore;
  • in [6], il messaggio di errore;

È più difficile spiegare cosa sia successo in questo caso. La risposta del server [3] è normale [HTTP/1.1 200 OK]. Dovremmo quindi avere il documento richiesto. È possibile che il server abbia effettivamente inviato il documento, ma che il browser ne impedisca l'utilizzo perché richiede che, anche per la richiesta GET, la risposta includa l'intestazione HTTP [Access-Control-Allow-Origin:http://localhost:8081].

Modifichiamo il controller [RdvMedecinsController] come segue:


    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;
...
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins(HttpServletResponse httpServletResponse,
            @RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
        // the answer
        Response<List<Medecin>> response;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
...
  • righe 1-2: viene iniettato il controller [RdvMedecinsCorsController];
  • righe 7-8: l'oggetto HttpServletResponse, che incapsula la risposta da inviare al client, e l'intestazione HTTP [Origin] vengono iniettati nei parametri del metodo [getAllMedecins];
  • riga 12: viene chiamato il metodo [sendOptions] del controller [RdvMedecinsCorsController] — lo stesso metodo che è stato chiamato per gestire la richiesta HTTP [OPTIONS]. Invierà quindi le stesse intestazioni HTTP di quella richiesta;

Dopo questa modifica, i risultati sono i seguenti:

 

Abbiamo ottenuto con successo l'elenco dei medici.

8.4.14.3. Gli altri URL [GET]

Esamineremo ora gli altri URL interrogati tramite una richiesta GET. Nei controller, il codice delle azioni che li gestiscono segue lo stesso schema delle azioni che in precedenza gestivano l'URL [/getAllMedecins]. Il lettore può verificare il codice negli esempi forniti con questo documento. Ecco un esempio:

in [RdvMedecinsCorsController]


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

in [RdvMedecinsController]


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

Ecco alcuni screenshot dell'esecuzione:

 
 
 
 
 
 

8.4.14.4. Gli URL [POST]

Esaminiamo il seguente scenario:

  • Effettuiamo un POST [1] all'URL [2];
  • in [3], il valore inviato. Si tratta di una stringa JSON;
  • In generale, stiamo cercando di eliminare l'appuntamento con [id] 100;

A questo punto non stiamo modificando alcun codice. Il risultato ottenuto è il seguente:

  • in [1], come per le richieste [GET], il browser effettua una richiesta [OPTIONS];
  • in [2], richiede l'autorizzazione di accesso per una richiesta [POST]. In precedenza era [GET];
  • In [3], richiede l'autorizzazione per inviare le intestazioni HTTP [accept, authorization, content-type]. In precedenza, avevamo solo le prime due intestazioni;

Modifichiamo il metodo [RdvMedecinsCorsController.sendOptions] come segue:


    public void sendOptions(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // we authorize GET
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
  • riga 9: abbiamo aggiunto l'intestazione HTTP [Content-Type] (non fa differenza maiuscolo/minuscolo);
  • riga 11: abbiamo aggiunto il metodo HTTP [POST];

Ciò significa che i metodi [POST] vengono gestiti allo stesso modo delle richieste [GET]. Ecco un esempio dell'URL [/deleteAppointment]:

in [RdvMedecinsController]


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

in [RdvMedecinsCorsController]


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

Il risultato è il seguente:

 

Per l'URL [/addRv], si ottiene il seguente risultato:

 

8.4.14.5. Conclusione

La nostra applicazione ora supporta le richieste cross-domain. Queste possono essere abilitate o disabilitate tramite la configurazione nella classe [ApplicationModel]:


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

8.5. Client del servizio web / JSON

Torniamo all'architettura generale dell'applicazione che vogliamo realizzare:

La parte superiore del diagramma è stata già implementata. Si tratta del server web/JSON. Ora ci occuperemo della parte inferiore, iniziando dal suo livello [DAO]. Lo implementeremo e poi lo testeremo con un client da console. L'architettura di test sarà la seguente:

8.5.1. Il progetto del client console

Il progetto STS per il client console sarà il seguente:

  

8.5.2. Configurazione di Maven

Il file [pom.xml] per il client console è il seguente:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>istia.st.rdvmedecins</groupId>
        <artifactId>rdvmedecins-webjson-client-console</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>rdvmedecins-webjson-client-console</name>
        <description>Client console du serveur web / jSON</description>
 
        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <java.version>1.8</java.version>
        </properties>
 
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
                <relativePath /> <!-- lookup parent from repository -->
        </parent>
 
        <dependencies>
                <!-- Spring -->
                <dependency>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-web</artifactId>
                </dependency>
                <!-- jSON library used by Spring -->
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-core</artifactId>
                </dependency>
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-databind</artifactId>
                </dependency>
                <!-- component used by Spring RestTemplate -->
                <dependency>
                        <groupId>org.apache.httpcomponents</groupId>
                        <artifactId>httpclient</artifactId>
                </dependency>
        </dependencies>
</project>
  • righe 15–20: il progetto Spring Boot principale;
  • righe 24–27: il client del server web/console JSON si basa su un componente chiamato [RestTemplate] fornito dalla dipendenza [spring-web];
  • righe 29–36: la serializzazione e la deserializzazione degli oggetti JSON richiedono una libreria JSON. Utilizziamo una variante della libreria Jackson utilizzata da Spring Web;
  • righe 38–41: al livello più basso, il componente [RestTemplate] comunica con il server tramite socket TCP/IP. Vogliamo impostare il [timeout] per questi, ovvero il tempo massimo di attesa per una risposta dal server. Il componente [RestTemplate] non ci permette di impostarlo. Per farlo, passeremo un componente di basso livello fornito dalla dipendenza [org.apache.httpcomponents.httpclient] al costruttore [RestTemplate]. È questa dipendenza che ci permetterà di impostare il [timeout] di comunicazione;

8.5.3. Il pacchetto [rdvmedecins.client.entities]

  

Il pacchetto [rdvmedecins.client.entities] contiene tutte le entità che il servizio web / JSON invia tramite i suoi vari URL. Non entreremo nuovamente nei dettagli al riguardo. Basti dire che alle entità JPA [Client, Slot, Doctor, Appointment, Person] sono state rimosse tutte le annotazioni JPA e quelle JSON. Ecco, ad esempio, la classe [Appointment]:


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

8.5.4. Il pacchetto [rdvmedecins.client.requests]

  

Il pacchetto [rdvmedecins.client.requests] contiene le due classi i cui valori JSON vengono inviati agli URL [/ajouterRv] e [supprimerRv]. Sono identiche alle loro controparti lato server.

8.5.5. Il pacchetto [rdvmedecins.client.responses]

  

[Response] è il tipo di tutte le risposte dei servizi web / JSON. Si tratta di un tipo generico:


package rdvmedecins.client.responses;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}
  • riga 5: il tipo [T] varia a seconda dell'URL del servizio web / JSON;

8.5.6. Il pacchetto [rdvmedecins.client.dao]

  
  • [IDao] è l'interfaccia del livello [DAO] e [Dao] ne è l'implementazione. Torneremo su questa implementazione;

8.5.7. Il pacchetto [rdvmedecins.client.config]

  

La classe [DaoConfig] configura l'applicazione. Il suo codice è il seguente:


package rdvmedecins.client.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
 
    @Bean
    public RestTemplate restTemplate() {
        // creation of the RestTemplate component
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // result
        return restTemplate;
    }
 
    // mappers jSON
 
    @Bean
    public ObjectMapper jsonMapper(){
        return new ObjectMapper();
    }
 
    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
    }
 
    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",
                creneauFilter));
        return jsonMapperLongRv;
    }
 
    @Bean
    public ObjectMapper jsonMapperShortRv() {
        ObjectMapper jsonMapperShortRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
        jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
        return jsonMapperShortRv;
    }
 
}
  • riga 13: la classe [DaoConfig] è una classe di configurazione Spring;
  • riga 14: il pacchetto [rdvmedecins.client.dao] verrà cercato per i componenti Spring. Il componente [Dao] verrà trovato lì;
  • Righe 17–24: definiscono un singleton Spring denominato [restTemplate] (il nome del metodo). Questo metodo restituisce un'istanza [RestTemplate], che è lo strumento di base fornito da Spring per comunicare con un servizio web o JSON;
  • Riga 21: potremmo scrivere [RestTemplate restTemplate = new RestTemplate();]. Questo è sufficiente nella maggior parte dei casi. Ma qui vogliamo impostare i [timeout] del client. Per farlo, iniettiamo un componente di basso livello di tipo [HttpComponentsClientHttpRequestFactory] (riga 20) nel componente [RestTemplate], il che ci permetterà di impostare questi [timeout]. La dipendenza Maven richiesta è stata fornita;
  • righe 28–57: definiscono i mappatori JSON. Questi sono i mappatori JSON utilizzati sul lato server (vedere la sezione 8.4.11.3) per serializzare il tipo T della risposta [Response<T>]. Questi stessi convertitori saranno ora utilizzati sul lato client per deserializzare il tipo T;

8.5.8. L'interfaccia [IDao]

Torniamo all'architettura dell'applicazione:

Il livello [DAO] funge da adattatore tra il livello [console] e gli URL esposti dal servizio web / JSON. La sua interfaccia [IDao] sarà la seguente:


package rdvmedecins.client.dao;
 
import java.util.List;
 
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
 
public interface IDao {
    // Web service url
    public void setUrlServiceWebJson(String url);
 
    // timeout
    public void setTimeout(int timeout);
 
    // authentication
    public void authenticate(User user);
 
    // customer list
    public List<Client> getAllClients(User user);
 
    // list of doctors
    public List<Medecin> getAllMedecins(User user);
 
    // list of physician slots
    public List<Creneau> getAllCreneaux(User user, long idMedecin);
 
    // find a customer identified by its id
    public Client getClientById(User user, long id);
 
    // find a customer identified by its id
    public Medecin getMedecinById(User user, long id);
 
    // find an Rv identified by its id
    public Rv getRvById(User user, long id);
 
    // find a time slot identified by its id
    public Creneau getCreneauById(User user, long id);
 
    // add a RV to the list
    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
 
    // delete a RV
    public void supprimerRv(User user, long idRv);
 
    // list of doctor's appointments on a given day
    public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
 
    // agenda
    public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
 
}
  • riga 14: il metodo per impostare l'URL radice del servizio web / JSON, ad esempio [http://localhost:8080];
  • riga 17: il metodo utilizzato per impostare i [timeout] lato client. Vogliamo controllare questo parametro perché alcuni client HTTP possono impiegare molto tempo ad attendere una risposta che non arriverà mai;
  • riga 20: il metodo per l'autenticazione di un utente [login, passwd]. Genera un'eccezione se l'utente non viene riconosciuto;
  • righe 22–53: ogni URL esposto dal servizio web / JSON è associato a un metodo dell'interfaccia, la cui firma deriva dalla firma del metodo lato server che gestisce l'URL esposto. Prendiamo, ad esempio, il seguente URL del server:

    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Response<String> getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin,    @PathVariable("jour") String jour, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
  • riga 1: vediamo che [idMedecin] e [jour] sono i parametri URL. Questi saranno i parametri di input per il metodo associato a questo URL sul lato client;
  • riga 2: vediamo che il metodo server restituisce un tipo [Response<String>]. Questo tipo [String] è il tipo del valore JSON di tipo [AgendaMedecinJour]. Il tipo di risultato del metodo associato a questo URL sul lato client sarà [AgendaMedecinJour];

Sul lato client, dichiariamo il seguente metodo:


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

Questa firma funziona quando il server invia una risposta di tipo [int status, List<String> messages, String body] con [status==0]. In questo caso, abbiamo [messages==null && body!=null]. Non funziona quando [status!=0]. In questo caso, abbiamo [messages!=null && body==null]. Dobbiamo segnalare in qualche modo che si è verificato un errore. Per farlo, lanceremo un'eccezione di tipo [RdvMedecinsException] come segue:


package rdvmedecins.client.dao;
 
import java.util.List;
 
public class RdvMedecinsException extends RuntimeException {
 
    private static final long serialVersionUID = 1L;
    // error code
    private int status;
    // list of error messages
    private List<String> messages;
 
    public RdvMedecinsException() {
    }
 
    public RdvMedecinsException(int code, List<String> messages) {
        super();
        this.status = code;
        this.messages = messages;
    }
 
    // getters and setters
...
}
  • righe 9 e 11: l'eccezione assumerà i valori dei campi [status, messages] dall'oggetto [Response<T>] inviato dal server;
  • riga 5: la classe [RdvMedecinsException] estende la classe [RuntimeException]. Si tratta quindi di un'eccezione non gestita, il che significa che non è necessario gestirla con un blocco try/catch né dichiararla nelle firme dei metodi dell'interfaccia;

Inoltre, tutti i metodi dell'interfaccia [IDao] che interrogano il servizio web/JSON hanno come parametro il seguente tipo [User]:


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

Infatti, ogni scambio con il servizio web / JSON deve essere accompagnato da un'intestazione di autenticazione HTTP.

8.5.9. Il pacchetto [rdvmedecins.clients.console]

Ora che abbiamo familiarizzato con l'interfaccia del livello [DAO], possiamo presentare l'applicazione console.

  

La classe [Main] è la seguente:


package rdvmedecins.clients.console;
 
import java.io.IOException;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Main {
 
    // serializer jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // connection timeout in milliseconds
    static private int TIMEOUT = 1000;
 
    public static void main(String[] args) throws IOException {
        // we retrieve a reference on the [DAO] layer
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // set the URL of the web/json service
        dao.setUrlServiceWebJson("http://localhost:8080");
        // set timeouts in milliseconds
        dao.setTimeout(TIMEOUT);
 
        // Authentication
        String message = "/authenticate [admin,admin]";
        try {
            dao.authenticate(new User("admin", "admin"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [user,user]";
        try {
            dao.authenticate(new User("user", "user"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [user,x]";
        try {
            dao.authenticate(new User("user", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [x,x]";
        try {
            dao.authenticate(new User("x", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [admin,x]";
        try {
            dao.authenticate(new User("admin", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // customer list
        message = "/getAllClients";
        try {
            showResponse(message, dao.getAllClients(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // list of doctors
        message = "/getAllMedecins";
        try {
            showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // list of slots for doctor 2
        message = "/getAllCreneaux/2";
        try {
            showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // customer no. 1
        message = "/getClientById/1";
        try {
            showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor no. 2
        message = "/getMedecinById/2";
        try {
            showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // slot no. 3
        message = "/getCreneauById/3";
        try {
            showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // rv n° 4
        message = "/getRvById/4";
        try {
            showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // adding an appointment
        message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
        long idRv = 0;
        try {
            Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
            idRv = response.getId();
            showResponse(message, response);
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor's appointment list 1 on 2015-01-08
        message = "/getRvMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor's agenda 1 on 2015-01-08
        message = "/getAgendaMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
        // delete added rv
        message = String.format("/supprimerRv [idRv=%s]", idRv);
        try {
            dao.supprimerRv(new User("admin", "admin"), idRv);
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor's appointment list 1 on 2015-01-08
        message = "/getRvMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
        // closing context
        context.close();
    }
 
    private static void showException(String message, RdvMedecinsException e) {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
        for (String msg : e.getMessages()) {
            System.out.println(msg);
        }
    }
 
    private static <T> void showResponse(String message, T response) throws JsonProcessingException {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(mapper.writeValueAsString(response));
    }
}
  • riga 19: il serializzatore JSON che ci consentirà di visualizzare la risposta del server, riga 184;
  • riga 25: il componente [AnnotationConfigApplicationContext] è un componente Spring in grado di utilizzare le annotazioni di configurazione da un'applicazione Spring. Passiamo la classe [AppConfig], che configura l'applicazione, al suo costruttore;
  • riga 26: recuperiamo un riferimento al livello [DAO];
  • righe 27–30: lo configuriamo;
  • righe 32–169: testiamo tutti i metodi dell'interfaccia [IDao];

I risultati ottenuti sono i seguenti:


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

Lasciamo al lettore il compito di mettere in relazione i risultati con il codice. Il codice mostra come chiamare ciascun metodo del livello [DAO]. Notiamo solo alcuni punti:

  • righe 2–14: mostrano che, in caso di errore di autenticazione, il server restituisce uno stato HTTP [403 Forbidden] o [401 Unauthorized], a seconda dei casi;
  • righe 30–31: viene aggiunto un appuntamento per il dottore n. 1;
  • righe 32–33: vediamo questo appuntamento. È l'unico della giornata;
  • righe 34–35: è visibile anche nel calendario del medico;
  • righe 36-37: l'appuntamento è scomparso. Il codice lo ha cancellato nel frattempo;

I log della console sono controllati dai seguenti file:

 

[application.properties]


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

[logback.xml]


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

8.5.10. Implementazione del livello [DAO]

Ora dobbiamo presentare il cuore del livello [DAO]: l'implementazione della sua interfaccia [IDao]. Lo faremo passo dopo passo.

 

L'interfaccia [IDao] è implementata dalla classe astratta [AbstractDao] e dalla sua classe figlia [Dao].

La classe padre [AbstractDao] è la seguente:


package rdvmedecins.client.dao;
 
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
 
import rdvmedecins.client.entities.User;
 
public abstract class AbstractDao implements IDao {
 
    // data
    @Autowired
    protected RestTemplate restTemplate;
    protected String urlServiceWebJson;
 
    // URL web service / jSON
    public void setUrlServiceWebJson(String url) {
        this.urlServiceWebJson = url;
    }
 
    public void setTimeout(int timeout) {
        // set the timeout for web client requests
        HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
                .getRequestFactory();
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
    }
 
    private String getBase64(User user) {
        // encodes user and password in base 64 - requires
        // java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }
 
    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
...
    }
 
}
  • riga 20: la classe è astratta, il che ci impedisce di designarla come componente Spring. La sua classe figlia sarà designata come tale;
  • righe 23–24: iniettiamo il bean [restTemplate] che abbiamo definito nella classe di configurazione [AppConfig];
  • riga 25: l'URL radice del servizio web / JSON;
  • righe 32–38: impostiamo il timeout del client in attesa di una risposta dal server;
  • riga 34: recuperiamo il componente [HttpComponentsClientHttpRequestFactory] che abbiamo iniettato nel bean [restTemplate] al momento della sua creazione (vedi [AppConfig]);
  • riga 36: impostiamo il tempo massimo di attesa per il client durante la creazione di una connessione con il server;
  • riga 37: impostiamo il tempo massimo di attesa per il client mentre attende una risposta a una delle sue richieste;

L'implementazione dei metodi per la comunicazione con il server sarà integrata nel seguente metodo generico:


    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
...
    }
  • Riga 2: I parametri di [getResponse] sono i seguenti:
    • [User user]: l'utente che effettua la connessione;
    • [String url]: l'URL da interrogare. Questa è la parte finale dell'URL; la prima parte è fornita dal campo [urlServiceWebJson] della classe,
    • [String jsonPost]: la stringa JSON da inviare. Se questo valore è presente, l'URL verrà richiesto con un POST; altrimenti, con un GET;

Continuiamo:


// generic request
    protected String getResponse(User user, String url, String jsonPost) {
        // url : URL to contact
        // jsonPost: the jSON value to be posted
        try {
            // request execution
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }
            // execute the query
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e) {
            throw new RdvMedecinsException(20, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(21, getMessagesForException(e));
        }
    }
  • Righe 23–24: L'istruzione che invia la richiesta al server e ne riceve la risposta. Il componente [RestTemplate] offre un'ampia gamma di metodi per interagire con il server. Avremmo potuto scegliere un metodo diverso da [exchange]. Il secondo parametro della chiamata specifica il tipo di risposta prevista, in questo caso una stringa JSON. Il primo parametro è la richiesta [RequestEntity] (riga 7). Il risultato del metodo [exchange] è di tipo [ResponseEntity<String>]. Il tipo [ResponseEntity] incapsula la risposta completa del server, inclusi gli header HTTP e il documento inviato dal server. Analogamente, il tipo [RequestEntity] incapsula l'intera richiesta del client, inclusi gli header HTTP e qualsiasi dato inviato;
  • riga 23: questo è il corpo dell'oggetto [ResponseEntity<String>] che viene restituito al metodo chiamante, ovvero la stringa JSON inviata dal server;
  • Righe 9–21: Dobbiamo costruire la richiesta [RequestEntity]. Essa varia a seconda che si utilizzi una richiesta GET o POST;
  • riga 9: la richiesta per un GET. La classe [RequestEntity] fornisce metodi statici per creare richieste GET, POST, HEAD e altre. Il metodo [RequestEntity.get] consente di creare una richiesta GET concatenando i vari metodi che la compongono:
    • Il metodo [RequestEntity.get] accetta l'URL di destinazione come parametro sotto forma di un'istanza URI;
    • il metodo [accept] consente di definire gli elementi dell'intestazione HTTP [Accept]. Qui, specifichiamo che accettiamo il tipo [application/json] che il server invierà;
    • il risultato di questo concatenamento di metodi è un tipo [HeadersBuilder];
  • righe 10–12: se il parametro [User user] non è nullo, includiamo l'intestazione HTTP [Authorization] nella richiesta;
  • riga 13: il metodo [HeadersBuilder.build] utilizza queste informazioni per costruire il tipo [RequestEntity] della richiesta;
  • riga 15: la richiesta è di tipo POST. Il metodo [RequestEntity.post] consente di creare una richiesta POST concatenando i vari metodi che la compongono:
    • il metodo [RequestEntity.post] accetta l'URL di destinazione come parametro sotto forma di un'istanza URI,
    • il metodo [header] consente di definire le intestazioni HTTP che si desidera utilizzare, in questo caso l'intestazione di autorizzazione,
    • il metodo [header] successivo include l'intestazione [Content-Type: application/json] nella richiesta per indicare che i dati inviati arriveranno come stringa JSON;
    • il metodo [accept] indica che accettiamo il tipo [application/json] che il server invierà;
  • righe 17–19: se il parametro [User user] non è nullo, l'intestazione HTTP [Authorization] viene inclusa nella richiesta;
  • riga 20: il metodo [BodyBuilder.body] imposta il valore inviato. Questo è il secondo parametro del metodo generico [getResponse] (riga 2);
  • righe 25–28: se si verifica un errore, viene generata un'eccezione [RdvMedecinsException];

Il metodo [getMessagesForException] alle righe 26 e 28 è il seguente:


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

Il metodo privato [getBase64] restituisce la codifica Base64 della stringa 'login:passwd' per l'intestazione di autenticazione HTTP:


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

La classe [Dao] estende la classe [AbstractDao] come segue:


package rdvmedecins.client.dao;
 
import java.io.IOException;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
 
@Service
public class Dao extends AbstractDao implements IDao {
 
    // mappers jSON
    @Autowired
    ObjectMapper jsonMapper;
 
    @Autowired
    private ObjectMapper jsonMapperShortCreneau;
 
    @Autowired
    private ObjectMapper jsonMapperLongRv;
 
    @Autowired
    private ObjectMapper jsonMapperShortRv;
 
    public List<Client> getAllClients(User user) {
        ...
    }
 
    public List<Medecin> getAllMedecins(User user) {
...
    }
...
}
  • riga 22: la classe [Dao] è un componente Spring. Qui è stata utilizzata l'annotazione [@Service]. Avremmo potuto continuare a utilizzare l'annotazione [@Component] usata fino a questo punto;
  • righe 26–36: iniezione dei quattro mappatori JSON definiti nella classe di configurazione [DaoConfig];

I metodi della classe [Dao] seguono tutti lo stesso schema. Descriveremo in dettaglio un'operazione GET e un'operazione POST.

Innanzitutto, una richiesta [GET]:


public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
        // the answer
        Response<AgendaMedecinJour> response;
        // the diary
        String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
        try {
            // diary AgendaMedecinJour
            response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
            });
        } catch (IOException e) {
            throw new RdvMedecinsException(401, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(402, getMessagesForException(e));
        }
        // response analysis
        int status = response.getStatus();
        if (status != 0) {
            throw new RdvMedecinsException(status, response.getMessages());
        } else {
            return response.getBody();
        }
}
  • Riga 5: Viene chiamato il metodo generico [getResponse]. I parametri effettivi utilizzati sono i seguenti:
    • 1: l'utente;
    • 2: l'URL di destinazione;
    • 3: il valore da inviare. In questo caso, non ce n'è nessuno;
  • riga 5: la chiamata non è stata racchiusa in un blocco try/catch. Il metodo [getResponse] potrebbe generare un'eccezione [RdvMedecinsException]. Se generata, questa eccezione si propagherà al metodo che ha chiamato il metodo [getAgendaMedecinJour] sopra;
  • riga 8: l'URL [/getAgendaMedecinJour] restituisce un [Response<AgendaMedecinJour>] che è stato serializzato in JSON sul lato server dal mappatore JSON [jsonMapperLongRv]. Usiamo questo stesso mappatore per deserializzare la stringa JSON ricevuta;
  • righe 10–13: se si verifica un errore alla riga 9, viene generata un'eccezione [RdvMedecinsException];
  • righe 16–21: la risposta inviata dal server viene analizzata;
  • righe 17–18: se il server ha segnalato un errore, viene generata un'eccezione con le informazioni fornite dal server;
  • righe 19–21: altrimenti, restituisce l'orario del medico;

La richiesta POST in esame sarà la seguente:


    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
        // the answer
        Response<Rv> response;
        try {
            // the Rv
            String jsonResponse = getResponse(user, "/ajouterRv",
                    jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
            // the Rv Rv
            response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
            });
        } catch (RdvMedecinsException e) {
            throw e;
        } catch (IOException e) {
            throw new RdvMedecinsException(381, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(382, getMessagesForException(e));
        }
        // response analysis
        int status = response.getStatus();
        if (status != 0) {
            throw new RdvMedecinsException(status, response.getMessages());
        } else {
            return response.getBody();
        }
}
  • Riga 6: Il metodo [getResponse] viene chiamato con i seguenti parametri:
    • 1: l'utente;
    • 2: l'URL di destinazione,
    • 3: il valore inviato: passiamo il valore JSON di tipo [PostAjouter] costruito con le informazioni ricevute come parametri dal metodo. Utilizziamo un mappatore JSON senza filtri;
  • riga 9: sul lato server, il mappatore JSON [jsonMapperLongRv] ha serializzato la risposta del server. Sul lato client, utilizziamo questo stesso mappatore per deserializzarla;
  • riga 6: l'URL [/ajouterRv] restituisce il valore JSON di tipo [Response<Rv>];
  • righe 4–11: qui, il metodo [getResponse] è stato inserito in un blocco try/catch perché la serializzazione del valore inviato potrebbe generare un'eccezione. Il metodo [getResponse] è suscettibile di generare un'eccezione [RdvMedecinsException]. In questo caso, lo riproviamo semplicemente (righe 11–12);

Il codice seguente (righe 13–24) è simile a quello appena discusso. L'unica differenza rispetto a un'operazione GET è quindi il secondo parametro del metodo [getResponse], che deve essere la rappresentazione JSON del valore da inviare.

Gli altri metodi sono costruiti sullo stesso modello.

8.5.11. Eccezione

Durante l'esecuzione di vari test, abbiamo riscontrato un'anomalia riassunta nella seguente classe [Anomalie]:


package rdvmedecins.clients.console;
 
import java.io.IOException;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Anomalie {
 
    // serializer jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // connection timeout in milliseconds
    static private int TIMEOUT = 1000;
 
    public static void main(String[] args) throws IOException {
        // we retrieve a reference on the [DAO] layer
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // set the URL of the web/json service
        dao.setUrlServiceWebJson("http://localhost:8080");
        // set timeouts in milliseconds
        dao.setTimeout(TIMEOUT);
 
        // Authentication
        String message = "/authenticate [admin,admin]";
        try {
            dao.authenticate(new User("admin", "admin"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // Authentication
        message = "/authenticate [admin,x]";
        try {
            dao.authenticate(new User("admin", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // Authentication
        message = "/authenticate [user,user]";
        try {
            dao.authenticate(new User("user", "user"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // closing context
        context.close();
    }
 
    private static void showException(String message, RdvMedecinsException e) {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
        for (String msg : e.getMessages()) {
            System.out.println(msg);
        }
    }
}
  • righe 31–38: l'utente [admin, admin] viene autenticato;
  • righe 40-47: autenticazione dell'utente [admin, x], che ha inserito una password errata;
  • righe 49-56: l'utente [user, user] viene autenticato; questo utente esiste ma non è autorizzato;

Ecco i risultati:

1
2
3
4
5
/authenticate [admin,admin] : OK
/authenticate [admin,x] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
  • riga 2: contrariamente alle aspettative, l'utente [admin, x] è stato accettato;

Se commentiamo le righe 33–38 del codice, otteniamo il seguente risultato:

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

che è il risultato previsto. Sembra che, una volta che l'utente [admin, admin] ha effettuato con successo il primo accesso, la sua password non sia più richiesta per gli accessi successivi. È proprio così. Per impostazione predefinita, Spring Security utilizza un meccanismo di sessione che garantisce che, una volta che un utente si è autenticato, non debba farlo nuovamente nelle richieste successive. È possibile modificare la configurazione [Spring Security] nel server web / JSON in modo che ciò non avvenga più:

  

Il file [SecurityConfig] deve essere modificato come segue:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
  • La riga 5 specifica che non deve esserci alcuna sessione di sicurezza;

Questo ha risolto il problema.

8.6. Rendering lato server con Spring / Thymeleaf

8.6.1. Introduzione

Torniamo all'architettura dell'applicazione client/server da realizzare:

  • il server web/JSON [Web2] è stato realizzato;
  • è stato realizzato il livello [DAO] del client [Web1];

La relazione tra il server [Web1] e i browser client è una relazione client/server in cui il server è un server web/JSON. Infatti, [Web1] fornirà flussi HTML incapsulati in una stringa JSON. L'architettura client/server è la seguente:

  • abbiamo un'architettura client [2] / server [1] in cui il client e il server comunicano tramite JSON;
  • In [1], il livello web Spring MVC/Thymeleaf fornisce viste, frammenti di vista e dati in JSON. Il server è quindi un server web/JSON come il server [Web1]. È inoltre stateless;
  • in [2]: il codice JavaScript incorporato nella vista caricata all'avvio dell'applicazione è strutturato a livelli:
    • Il livello [presentazione] gestisce le interazioni dell'utente,
    • il livello [DAO] gestisce l'accesso ai dati tramite il server [Web2];
  • il client [2] memorizzerà nella cache alcune viste per ridurre il carico sul server;

Realizzeremo il server web/JSON [Web1], implementato con Spring MVC/Thymeleaf, in diverse fasi:

  • esplorando il framework CSS Bootstrap;
  • scrivendo le viste;
  • scrivendo il controller;

Successivamente, separatamente, realizzeremo il client JS per il server [Web1]. Per dimostrare chiaramente che questo client ha un certo grado di indipendenza dal server [Web1], lo realizzeremo utilizzando lo strumento [WebStorm] anziché STS.

D'ora in poi, alcuni dettagli verranno tralasciati poiché potrebbero distrarci dall'argomento principale, ovvero l'organizzazione del codice. I lettori interessati possono trovare il codice completo sul sito web dedicato a questo documento.

8.6.2. Il progetto STS

  • in [1], il codice Java;
  • in [2], le viste;

La configurazione Maven in [pom.xml] è la seguente:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.rdvmedecins</groupId>
    <artifactId>rdvmedecins-springthymeleaf-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rdvmedecins-springthymeleaf-server</name>
    <description>Gestion de RV Médecins</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>istia.st.rdvmedecins</groupId>
            <artifactId>rdvmedecins-webjson-client-console</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <properties>
        <start-class>rdvmedecins.springthymeleaf.server.boot.Boot</start-class>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.7</java.version>
    </properties>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    ...
</project>
  • righe 16–19: il progetto è un progetto Thymeleaf;
  • righe 20–24: che si basa sul livello [DAO] che abbiamo appena creato;

La configurazione Java è gestita da due file:

 

Il livello [web] è configurato dal seguente file [WebConfig]:


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

Abbiamo già incontrato tutti gli elementi di questa configurazione in un momento o nell'altro. Ricordiamo che le righe 42–47 sono necessarie quando si desidera poter interrogare il server con richieste cross-origin (CORS). Questo sarà il caso in questione.

La classe [AppConfig] configura l'intera applicazione:


package rdvmedecins.springthymeleaf.server.config;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
 
import rdvmedecins.client.config.DaoConfig;
 
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
 
    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // root web service / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout in milliseconds
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;
 
    ...
 
}
  • Riga 11: [AppConfig] importa la configurazione per il livello [DAO] e il livello [web];
  • righe 15-16: le credenziali che consentiranno all'applicazione di accedere al processo di avvio dell'applicazione per memorizzare nella cache medici e clienti;
  • riga 18: l'URL del servizio web [Web1] / JSON;
  • riga 20: il timeout per le chiamate HTTP dell'applicazione;
  • riga 22: un valore booleano per abilitare o disabilitare le chiamate cross-domain;

Infine, in [application.properties], il server Tomcat è configurato per funzionare sulla porta 8081:

  

server.port=8081

8.6.3. Funzionalità dell'applicazione

Queste sono state descritte nella Sezione 8.2. Ora le esamineremo. Utilizzando un browser, inviamo una richiesta all'URL [http://localhost:8081/boot.html]:

  • in [1], la pagina di accesso dell'applicazione;
  • nei campi [2] e [3], il nome utente e la password dell'utente che desidera utilizzare l'applicazione. Sono presenti due utenti: admin/admin (nome utente/password) con il ruolo (ADMIN) e user/user con il ruolo (USER). Solo il ruolo ADMIN dispone dell'autorizzazione per utilizzare l'applicazione. Il ruolo USER è incluso esclusivamente per illustrare la risposta del server in questo caso d'uso;
  • in [4], il pulsante che consente di connettersi al server;
  • in [5], la lingua dell'applicazione. Ce ne sono due: francese (predefinita) e inglese;
  • in [6], l'URL del server [rdvmedecins-springthymeleaf-server];
  • in [1], effettui l'accesso;
  • una volta effettuato l'accesso, puoi scegliere il medico che desideri consultare [2] e la data dell'appuntamento [3]. Non appena vengono selezionati il medico e la data, viene visualizzato automaticamente il calendario:
  • una volta visualizzato il calendario del medico, è possibile prenotare una fascia oraria [5];
  • In [6], selezionare il paziente per l'appuntamento e confermare la selezione in [7];

Una volta confermato l'appuntamento, si torna automaticamente al calendario, dove il nuovo appuntamento è ora elencato. Questo appuntamento può essere cancellato in un secondo momento [8].

Le funzionalità principali sono state descritte. Sono semplici. Concludiamo con le impostazioni della lingua:

  • in [1], si passa dal francese all'inglese;
  • In [2], la visualizzazione passa all'inglese, compreso il calendario;

8.6.4. Passo 1: Introduzione al framework CSS Bootstrap

Nel client web sopra riportato, le pagine HTML utilizzeranno il framework CSS Bootstrap [http://getbootstrap.com/], che presenteremo ora.

8.6.4.1. Il progetto di esempio

Il progetto di esempio sarà il seguente:

  • in [1]: il progetto nel suo complesso;
  • in [2]: il codice Java;
  • in [3]: gli script JavaScript;
  • in [4]: le librerie JavaScript;
  • in [5]: le viste Thymeleaf;
  • in [6]: i fogli di stile;

8.6.4.1.1. Configurazione Maven

Il file [pom.xml] è per un progetto Maven Thymeleaf:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>istia.st</groupId>
    <artifactId>rdvmedecins-webjson-client-bootstrap</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>rdvmedecins-webjson-client-bootstrap</name>
    <description>Démos Bootstrap</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
        <java.version>1.7</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>

8.6.4.1.2. Configurazione Java
  

La classe [BootstrapDemo] configura l'applicazione Spring/Thymeleaf:


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

Abbiamo già incontrato questo tipo di codice.

8.6.4.1.3. Il controller Spring
  

Il [BootstrapController] è il seguente:


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

Le azioni servono solo a visualizzare le viste elaborate da Thymeleaf.

8.6.4.1.4. Il file [application.properties]

Il file [application.properties] configura il server Tomcat incorporato:


server.port=8082

8.6.4.2. Esempio n. 1: il jumbotron

L'azione [/bs-01] visualizza la seguente vista [bs-01.xml]:

La vista [bs-01.xml] è la seguente:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
    </head>
    <body id="body">
        <div class="container">
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- error -->
            <div id="erreur" class="alert alert-danger">
                <span>Ici, un texte d'erreur</span>
            </div>
        </div>
    </body>
</html>
  • riga 7: il file CSS del framework Bootstrap;
  • riga 8: un file CSS locale;
  • riga 13: displays [1];
  • righe 19–21: visualizza [2];
  • riga 11: la classe CSS [container] definisce un'area di visualizzazione all'interno del browser;
  • riga 19: la classe CSS [alert] visualizza un'area colorata. La classe [alert-danger] utilizza un colore predefinito. Ne esistono diverse [alert-info, alert-warning,...];

Il jumbotron [1] è generato dalla seguente vista [jumbotron.xml]:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <!-- Bootstrap Jumbotron -->
    <div class="jumbotron">
        <div class="row">
            <div class="col-md-2">
                <img src="resources/images/caduceus.jpg" alt="RvMedecins" />
            </div>
            <div class="col-md-10">
                <h1>
                    Les Médecins
                    <br />
                    associés
                </h1>
            </div>
        </div>
    </div>
</section>
  • riga 4: l'area ha la classe CSS [jumbotron];
  • riga 5: la classe [row] definisce una riga con 12 colonne;
  • riga 6: la classe [col-md-2] definisce un'area a due colonne all'interno della riga;
  • riga 7: un'immagine viene inserita in queste due colonne;
  • righe 9–15: il testo viene inserito nelle restanti 10 colonne;

8.6.4.3. Esempio #2: La barra di navigazione

L'azione [/bs-02] visualizza la seguente vista [bs-02.xml]:

La nuova funzionalità è la barra di navigazione [1] con il suo modulo di inserimento dati e i pulsanti:

La vista [bs-02.xml] è la seguente:


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

La barra di navigazione è generata dalla seguente vista [navbar1.xml]:


<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="navbar-collapse collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- identification form -->
                <div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
                    <div class="form-group">
                        <input type="text" placeholder="Utilisateur" class="form-control" />
                    </div>
                    <div class="form-group">
                        <input type="password" placeholder="Mot de passe" class="form-control" />
                    </div>
                    <button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
                </div>
            </div>
        </div>
    </div>
</section>
  • riga 3: la classe [navbar] definisce lo stile della barra di navigazione. La classe [navbar-inverse] le assegna uno sfondo nero. La classe [navbar-fixed-top] garantisce che, quando si scorre la pagina visualizzata dal browser, la barra di navigazione rimanga nella parte superiore dello schermo;
  • Righe 5–13: definiscono l'area [1]. Si tratta in genere di una serie di classi che non capisco. Utilizzo il componente così com'è;
  • righe 14–26: definiscono un'area “responsive” della barra di navigazione. Su uno smartphone, quest'area si riduce a un'area menu;
  • riga 15: un'immagine attualmente nascosta;
  • righe 17–25: la classe [navbar-form] applica uno stile a un modulo nella barra di navigazione. La classe [navbar-right] lo posiziona a destra della barra di navigazione;
  • righe 21–23: i due campi di input del modulo alla riga 17 [2]. Si trovano all’interno di una classe [form-group] che racchiude gli elementi di un modulo, e ciascuno di essi ha la classe [form-control];
  • riga 24: la classe [btn], che definisce un pulsante, arricchita dalla classe [btn-success], che gli conferisce il colore verde;
  • riga 24: quando si clicca sul pulsante [Login], viene eseguita la seguente funzione JS:

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

Ecco un esempio:

Image

8.6.4.4. Esempio n. 3: Il pulsante di elenco

L'azione [/bs-03] visualizza la seguente vista [bs-03.xml]:

  • La nuova funzionalità è il pulsante di elenco [1], noto anche come "menu a tendina";

Il codice per la vista [bs-03.xml] è il seguente:


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

La vista [navbar2.xml] è la seguente:


<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="navbar-collapse collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- identification form -->
                <div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
                    <div class="form-group">
                        <input type="text" placeholder="Utilisateur" class="form-control" />
                    </div>
                    <div class="form-group">
                        <input type="password" placeholder="Mot de passe" class="form-control" />
                    </div>
                    <button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
                    <!-- languages -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger">Langues</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')">Français</a>
                            </li>
                            <li>
                                <a href="javascript:setLang('en')">English</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initNavBar2();
        /*]]>*/
    </script>
</section>
  • righe 25–40: definiscono il pulsante a tendina;
  • riga 27: la classe [btn-danger] gli conferisce il colore rosso;
  • righe 32–39: le voci dell'elenco. Ciascuna è un link associato a una funzione JavaScript;
  • righe 46–51: uno script JavaScript eseguito dopo il caricamento del documento;

Lo script JS [bs-03.js] è il seguente:


function initNavBar2() {
    // dropdown des langues
    $('.dropdown-toggle').dropdown();
}
 
function connecter() {
    showInfo("Connexion demandée...");
}
 
function setLang(lang) {
    var msg;
    switch (lang) {
    case 'fr':
        msg = "Vous avez choisi la langue française...";
        break;
    case 'en':
        msg = "You have selected english language...";
        break;
    }
    showInfo(msg);
}
 
function showInfo(message) {
    $("#info").text(message);
}
  • Righe 1-4: La funzione che inizializza il [dropdown]. [$('.dropdown-toggle')] individua l'elemento con la classe [dropdown-toggle]. Questo è il pulsante a tendina (riga 28 della vista). La funzione JS [dropdown()] — definita nel file JS [bootstrap.js] — viene applicata ad esso. Solo dopo questa operazione il pulsante si comporta come un pulsante a tendina;
  • righe 10–21: la funzione eseguita quando viene selezionata una lingua;

Ecco un esempio:

Image

8.6.4.5. Esempio #4: un menu

L'azione [/bs-04] visualizza la seguente vista [bs-04.xml]:

È stato aggiunto un menu [1].

La vista [bs-04.xml] è la seguente:


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

La vista [navbar3.xml] è la seguente:


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

Lo script JS [bs-04.js] è il seguente:


...
function initNavBar3() {
    // dropdown des langues
    $('.dropdown-toggle').dropdown();
    // l'moving image
    loading = $("#loading");
    loading.hide();
}
 
function afficherAgenda() {
    showInfo("option [Agenda] cliquée...");
}
 
function retourAccueil() {
    showInfo("option [Retour accueil] cliquée...");
}
 
function retourAgenda() {
    showInfo("option [Retour agenda] cliquée...");
}
 
function validerRv() {
    showInfo("option [Valider] cliquée...");
}
 
function setMenu(show) {
    // les liens du menu
    var lnkAfficherAgenda = $("#lnkAfficherAgenda");
    var lnkAccueil = $("#lnkAccueil");
    var lnkValiderRv = $("#lnkValiderRv");
    var lnkRetourAgenda = $("#lnkRetourAgenda");
    // on les met dans un dictionnaire
    var options = {
        "lnkAccueil" : lnkAccueil,
        "lnkAfficherAgenda" : lnkAfficherAgenda,
        "lnkValiderRv" : lnkValiderRv,
        "lnkRetourAgenda" : lnkRetourAgenda
    }
    // on cache tous les liens
    for ( var key in options) {
        options[key].hide();
    }
    // on affiche ceux qui sont demandés
    for (var i = 0; i < show.length; i++) {
        var option = show[i];
        options[option].show();
    }
}
  • righe 2–18: la funzione di inizializzazione della pagina;
  • riga 4: per visualizzare il pulsante di selezione della lingua;
  • righe 6-7: l'immagine animata viene nascosta;
  • righe 26-48: una funzione [setMenu] che consente di specificare quali opzioni devono essere visibili;

Andiamo alla console degli sviluppatori (Ctrl-Shift-I) e inseriamo il seguente codice [1]:

Quindi torniamo al browser. Il menu è cambiato [2]:

8.6.4.6. Esempio n. 5: Un elenco a discesa

L'azione [/bs-05] visualizza la seguente vista [bs-05.xml]:

La nuova funzionalità si trova al punto [1]. Qui stiamo utilizzando un componente fornito al di fuori di Bootstrap, [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].

Il codice per la vista [bs-05.xml] è il seguente:


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

La vista [choixmedecin.xml] è la seguente:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info">Veuillez choisir un médecin</div>
    <div class="row">
        <div class="col-md-3">
            <h2>Médecin</h2>
            <select id="idMedecin" class="combobox" data-style="btn-primary">
                <option value="1">Mme Marie Pélissier</option>
                <option value="2">Mr Jean Pardon</option>
                <option value="3">Mlle Jeanne Jirou</option>
                <option value="4">Mr Paul Macou</option>
            </select>
        </div>
    </div>
    <!-- local script -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initChoixMedecin();
        /*]]>*/
    </script>
</section>
  • righe 7–12: Si tratta di un elemento [select] standard, ma con una classe specifica [combobox]. L'attributo [data-style="btn-primary"] conferisce al componente il colore blu;
  • righe 16–21: uno script eseguito al caricamento della pagina;

Il file JS [bs-05.js] è il seguente:


...
function afficherAgenda() {
    var idMedecin = $('#idMedecin option:selected').val();
    showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
 
function initChoixMedecin() {
    // le select des médecins
    $('#idMedecin').selectpicker();
    // le menu
    setMenu([ "lnkAfficherAgenda" ]);
}
  • righe 7–12: la funzione eseguita al caricamento della pagina;
  • riga 9: l'istruzione che trasforma il [select] della pagina in un elenco a discesa Bootstrap. [$('#idMedecin')] fa riferimento al [select] (riga 7 della vista [choixmedecin]) e la funzione JS [selectpicker] proviene dal file JS [bootstrap-select.js];
  • riga 11: viene visualizzata solo una delle opzioni del menu;
  • righe 2–5: la funzione JavaScript eseguita quando si clicca sull'opzione di menu [Agenda];
  • riga 3: recuperiamo il valore dell'opzione selezionata nell'elenco a discesa: [$('#idMedecin option:selected')] individua prima il componente [id=idMedecin] e poi, all'interno di quel componente, l'opzione selezionata. L'operazione [..].val() recupera quindi il valore dell'elemento trovato, ovvero l'attributo [value] dell'opzione selezionata;

Ecco un esempio di selezione di un medico:

 

8.6.4.7. Esempio n. 6: un calendario

L'azione [/bs-06] visualizza la seguente vista [bs-06.xml]:

Image

La selezione di un medico o di una data attiva una funzione JS che visualizza sia il medico selezionato che la data selezionata. Ecco un esempio:

 

Utilizzando il pulsante dell'elenco delle lingue, è possibile impostare il calendario (e solo il calendario) in inglese:

Image

Questo è l'esempio più complesso della serie. Il calendario è un componente [bootstrap-datepicker] [http://eternicode.github.io/bootstrap-datepicker].

La vista [bs-06.xml] è la seguente:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-06.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content" th:include="choixmedecinjour">
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • riga 8: il file CSS per il componente [bootstrap-datepicker];
  • riga 16: il file JS per il componente [bootstrap-datepicker];
  • riga 17: il file JS per la gestione di un calendario francese. Per impostazione predefinita, è in inglese;
  • riga 15: il file JS per una libreria chiamata [moment] che fornisce l'accesso a numerose funzioni di calcolo del tempo [http://momentjs.com/];
  • riga 28: la vista del calendario;

La vista [choixmedecinjour.xml] è la seguente:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info">Veuillez choisir un médecin et une date</div>
    <div class="row">
        <div class="col-md-3">
            <h2>Médecin</h2>
            <select id="idMedecin" class="combobox" data-style="btn-primary">
                <option value="1">Mme Marie Pélissier</option>
                <option value="2">Mr Jean Pardon</option>
                <option value="3">Mlle Jeanne Jirou</option>
                <option value="4">Mr Paul Macou</option>
            </select>
        </div>
        <div class="col-md-3">
            <h2>Date</h2>
            <section id="calendar_container">
                <div id="calendar" class="input-group date">
                    <input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
                        <span class="input-group-addon">
                            <i class="glyphicon glyphicon-th"></i>
                        </span>
                    </input>
                </div>
            </section>
        </div>
    </div>
    <!-- local script -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initChoixMedecinJour();
        /*]]>*/
    </script>
</section>
  • righe 17-23: il calendario;
  • riga 18: la classe [btn-primary] gli conferisce il colore blu;
  • riga 18: l'attributo [disabled="true"] impedisce l'inserimento manuale della data. È necessario utilizzare il calendario;
  • riga 16: il calendario è stato inserito in una sezione [id="calendar_container"]. Per cambiare la lingua del calendario, è necessario eliminarlo e poi rigenerarlo. Quindi, eliminare il contenuto del componente [id="calendar_container"] e poi inserirvi il nuovo calendario con la nuova lingua;
  • Righe 28–33: il codice di inizializzazione della pagina;

Il file JS [bs-06.js] è il seguente:


...
var calendar_infos = {};
 
function initChoixMedecinJour() {
    // calendrier
    var calendar_container = $("#calendar_container");
    calendar_infos = {
        "container" : calendar_container,
        "html" : calendar_container.html(),
        "today" : moment().format('YYYY-MM-DD'),
        "langue" : "fr"
    }
    // création calendrier
    updateCalendar();
    // le select des médecins
    $('#idMedecin').selectpicker();
    $('#idMedecin').change(function(e) {
        afficherAgenda();
    })
    // le menu
    setMenu([]);
}
  • Riga 2: Il calendario è gestito da diverse funzioni JS. La variabile [calendar_infos] raccoglierà le informazioni relative al calendario. È globale in modo da essere accessibile alle varie funzioni;
  • riga 6: identifichiamo il contenitore del calendario;
  • righe 7–12: le informazioni memorizzate per il calendario;
    • riga 8: un riferimento al suo contenitore,
    • riga 9: il codice HTML del calendario. Con queste due informazioni, possiamo rimuovere il calendario e rigenerarlo;
    • riga 10: la data odierna nel formato [yyyy-mm-dd],
    • riga 11: la lingua del calendario;
  • riga 14: creazione del calendario;
  • riga 16: il menu a tendina dei medici;
  • righe 17–19: ogni volta che il valore selezionato in questo menu a tendina cambia, verrà eseguito il metodo [displayCalendar];
  • riga 21: nessun menu nella barra di navigazione;

La funzione [updateCalendar] è la seguente:


function updateCalendar(renew) {
    if (renew) {
        // régénération du calendrier actuel
        calendar_infos.container.html(calendar_infos.html);
    }
    // initialisation du calendrier
    var calendar = $("#calendar");
    var settings = {
        format : "yyyy-mm-dd",
        startDate : calendar_infos.today,
        language : calendar_infos.langue,
    };
    calendar.datepicker(settings);
    // sélection de la date courante
    if (calendar_infos.date) {
        calendar.datepicker('setDate', calendar_infos.date)
    }
    // évts
    calendar.datepicker().on('hide', function(e) {
        // affichage jour sélectionné
        displayJour();
    });
    calendar.datepicker().on('changeDate', function(e) {
        // on note la nouvelle date
        calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
        // affichage infos agenda
        afficherAgenda();
        // affichage jour sélectionné
        displayJour();
    });
    // affichage jour sélectionné
    displayJour();
}
  • riga 1: la funzione [updateCalendar] accetta un parametro che può essere presente o meno. Se è presente, il calendario viene rigenerato (riga 4) in base alle informazioni contenute in [calendar_infos];
  • riga 7: viene richiamato il calendario;
  • righe 8–12: i suoi parametri di inizializzazione;
    • riga 9: il formato delle date gestite [yyyy-mm-dd],
    • riga 10: la prima data che può essere selezionata nel calendario. In questo caso, la data odierna. Le date precedenti a questa non possono essere selezionate;
    • riga 11: la lingua del calendario. Ce ne saranno due: ['en'] e ['fr'];
  • riga 13: il calendario viene configurato;
  • righe 15–17: se la data da [calendar_infos] è stata inizializzata, allora questa data viene impostata come data corrente del calendario;
  • righe 19–22: ogni volta che il calendario si chiude, verrà visualizzata la data selezionata;
  • righe 23–30: ogni volta che c'è un cambio di data nel calendario:
    • riga 25: la data selezionata viene registrata in [calendar_infos],
    • riga 27: visualizziamo le informazioni relative al calendario,
    • riga 29: visualizziamo il giorno selezionato;
  • riga 32: visualizza il giorno selezionato, se presente;

Il metodo [displayJour] che visualizza il giorno selezionato è il seguente:


// affiche le jour sélectionné
function displayJour() {
    if (calendar_infos.date) {
        var displayjour = $("#displayjour");
        moment.locale(calendar_infos.langue);
        jour = moment(calendar_infos.date).format('LL');
        displayjour.val(jour);
    }
}
  • riga 3: se è già stata selezionata una data (inizialmente, il calendario non ha alcuna data selezionata);
  • riga 4: individuiamo il componente in cui inseriremo la data;
  • riga 5: questa data può essere scritta in inglese o in francese. Impostiamo la lingua della libreria [moment];
  • riga 6: visualizza la data selezionata nella lingua scelta e in formato lungo;
  • riga 7: questa data viene visualizzata;

Ecco due esempi:

Quando cambia il medico o la data, viene eseguito il metodo [displayCalendar]:


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

8.6.4.8. Esempio #7: Una tabella HTML "responsive"

Nota: "responsive" è un termine che indica che un componente è in grado di adattarsi alle dimensioni dello schermo su cui viene visualizzato. Ne mostreremo un esempio.

L'azione [/bs-07] visualizza la seguente vista [bs-07.xml] (a schermo intero):

La nuova funzionalità è la tabella HTML [1]. Questa tabella è gestita dalla libreria JS [footable]: [https://github.com/fooplugins/FooTable].

Se ridimensioni la finestra del browser, otterrai quanto segue:

  • la tabella HTML si è adattata alle dimensioni dello schermo;
  • in [1], per visualizzare il link [Libro], è necessario cliccare sul segno [+];
  • in [2], ciò che si vede quando si clicca sul segno [+];

La vista [bs-07.xml] è la seguente:


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

La vista [agenda.xml] è la seguente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <div class="row alert alert-danger">
            <div class="col-md-6">
                <table id="creneaux" class="table">
                    <thead>
                        <tr>
                            <th data-toggle="true">
                                <span>Créneau horaire</span>
                            </th>
                            <th>
                                <span>Client</span>
                            </th>
                            <th data-hide="phone">
                                <span>Action</span>
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>
                                <span class='status-metro status-active'>
                                    9h00-9h20
                                </span>
                            </td>
                            <td>
                                <span></span>
                            </td>
                            <td>
                                <a href="javascript:reserver(14)" class="status-metro status-active">
                                    Réserver
                                </a>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                <span class='status-metro status-suspended'>
                                    9h20-9h40
                                </span>
                            </td>
                            <td>
                                <span>Mme Paule MARTIN</span>
                            </td>
                            <td>
                                <a href="javascript:supprimer(17)" class="status-metro status-suspended">
                                    Supprimer
                                </a>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
        <!-- init page -->
        <script th:inline="javascript">
            /*<![CDATA[*/
            // on initialise la page
            initAgenda();
        /*]]>*/
        </script>
    </body>
</html>
  • riga 4: inserisce la tabella in una riga [row] e in un riquadro colorato [alert alert-danger];
  • riga 5: la tabella occuperà 6 colonne [col-md-6];
  • riga 6: la tabella HTML è formattata da Bootstrap [class='table'];
  • riga 9: l'attributo [data-toggle] specifica la colonna contenente il simbolo [+/-] che espande/comprime la riga;
  • riga 15: l'attributo [data-hide='phone'] specifica che la colonna deve essere nascosta se lo schermo ha le dimensioni di uno schermo di telefono. È possibile utilizzare anche il valore 'tablet';
  • riga 31: una funzione JS è associata al link [Book];
  • riga 46: una funzione JS è associata al link [Delete];
  • righe 56–61: inizializzazione della pagina;

Numerose classi CSS utilizzate sopra provengono dal file CSS [bootstrapDemo.css]:


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

Gli stili [status-*] provengono da un esempio di utilizzo della tabella [footable] presente sul sito web della libreria.

Nel file JS [bs-07.js], la pagina viene inizializzata come segue:


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

Ecco fatto. [$("#creneaux")] si riferisce alla tabella HTML che vogliamo rendere reattiva. Inoltre, ecco le funzioni JS associate ai due link [Prenota] e [Elimina]:


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

8.6.4.9. Esempio #8: Una finestra modale

L'azione [/bs-08] visualizza la seguente vista [bs-08.xml]:

 

Image

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

Image

Il componente utilizzato è il componente [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/].

La vista [bs-08.xml] è la seguente:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-modal.js"></script>
        <script type="text/javascript" src="resources/vendor/footable.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-08.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3" />
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron" />
            <!-- content -->
            <div id="content" th:include="choixmedecinjour" />
            <div id="agenda" th:include="agenda-modal" />
            <div th:include="resa" />
            <!-- info -->
            <div class="alert alert-success">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • riga 19: il file JS necessario per le finestre modali;
  • riga 32: la vista [agenda-modal] è identica alla vista [agenda] tranne che per un dettaglio: la funzione JS che gestisce il link [Prenota]:

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

La funzione [showDialogResa] è responsabile della visualizzazione della finestra modale per la selezione di un cliente;

  • riga 33: la vista [resa.xml] è la finestra modale per la selezione di un cliente:

<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div id="resa" class="modal fade">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">
                        </span>
                    </button>
                    <!-- <h4 class="modal-title">Modal title</h4> -->
                </div>
                <div class="modal-body">
                    <div class="alert alert-info">
                        <h3>
                            <span>Prise de rendez-vous</span>
                        </h3>
                    </div>
                    <div class="row">
                        <div class="col-md-3">
                            <h2>Clients</h2>
                            <select id="idClient" class="combobox" data-style="btn-primary">
                                <option value="1">Mme Marguerite Planton</option>
                                <option value="2">Mr Maxime Franck</option>
                                <option value="3">Mlle Elisabeth Oron</option>
                                <option value="4">Mr Gaëtan Calot</option>
                            </select>
                        </div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()">Annuler</button>
                    <button type="button" class="btn btn-primary" onclick="javascript:validateResa()">Valider</button>
                </div>
            </div><!-- /.modal-content -->
        </div><!-- /.modal-dialog -->
    </div><!-- /.modal -->
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initResa();
        /*]]>*/
    </script>
</section>
  • righe 3-37: la finestra modale;
  • righe 13-30: il contenuto di questa finestra (ciò che verrà visualizzato);
  • righe 31-34: i pulsanti della finestra di dialogo;
  • riga 32: un pulsante [Annulla] gestito dalla funzione JS [cancelDialogResa];
  • riga 33: un pulsante [Conferma] gestito dalla funzione JS [validateResa];
  • righe 39–44: lo script di inizializzazione della finestra modale;

Il risultato è la seguente visualizzazione:

 

Si noti che la finestra modale non viene visualizzata per impostazione predefinita. Questo è il motivo per cui non è visibile all'avvio dell'applicazione, anche se il suo codice HTML è presente nel documento.

Il file JS [bs-08.js] è il seguente:


var idCreneau;
var idClient;
var resa;
 
function showDialogResa(idCreneau) {
    // on mémorise l'id du créneau
    this.idCreneau = idCreneau;
    // on affiche le dialogue de réservation
    var resa = $("#resa");
    resa.modal('show');
    // log
    showInfo("Réservation du créneau n° " + idCreneau);
}
 
function cancelDialogResa() {
    // on cache la boîte de dialogue
    resa.modal('hide');
}
 
// validation résa
function validateResa() {
    // on récupère les infos
    var idClient = $('#idClient option:selected').val();
    // on cache la boîte de dialogue
    resa.modal('hide');
    // infos
    showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
 
function initResa() {
    // le select des clients
    $('#idClient').selectpicker();
    // boîte modale
    resa = $("#resa");
    resa.modal({});    
}
  • righe 30–36: la funzione di inizializzazione della finestra modale;
  • riga 32: la finestra modale contiene un elenco a discesa che deve essere inizializzato;
  • righe 34-35: inizializzazione della finestra modale stessa;
  • righe 5-13: la funzione JS associata al link [Book];
  • riga 7: il parametro della funzione viene memorizzato nella variabile globale della riga 1;
  • righe 9-10: la finestra modale viene resa visibile;
  • riga 12: le informazioni vengono registrate nella casella informativa;
  • righe 15–18: gestione del pulsante [Cancel]. Semplicemente nascondiamo la finestra modale (riga 17);
  • righe 21–31: la funzione JS associata al pulsante [Invia];
  • riga 23: recupero dell'attributo [value] del client selezionato;
  • riga 25: nascondiamo la finestra di dialogo;
  • riga 27: registriamo le due informazioni: il numero dello slot prenotato e il cliente a cui è destinato;

8.6.5. Fase 2: Scrittura delle viste

Descriveremo ora le viste restituite dal server [Web1] e i relativi modelli.

  

8.6.5.1. La vista [navbar-start]

Visualizza la barra di navigazione nella pagina di avvio:

Image

Il codice per [navbar-start.xml] è il seguente:


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

Questa vista non ha alcun modello. Dispone dei seguenti gestori di eventi:

event
gestore
clic sul pulsante di accesso
connect() - riga 27
clicca sul link [Francese]
setLang('fr') - riga 37
fare clic sul link [English]
setLang('en') - riga 40

8.6.5.2. La vista [jumbotron]

Questa è la vista visualizzata sotto la barra di navigazione [navbar-start] nella pagina di avvio:

Image

Il suo codice [jumbotron.xml] è il seguente:


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

La vista [jumbotron] non ha modelli né eventi.

8.6.5.3. La vista [login]

Questa è la vista visualizzata sotto il jumbotron nella pagina di avvio:

Image

Il suo codice [login.xml] è il seguente:


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

La vista non ha né un modello né eventi.

8.6.5.4. La vista [navbar-run]

Questa è la barra di navigazione visualizzata quando l'accesso ha esito positivo:

Image

Il suo codice [navbar-run.xml] è il seguente:


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

Questa vista non ha alcun modello. Dispone dei seguenti gestori di eventi:

event
gestore
clic sul pulsante di logout
logout() - riga 19
clicca sul link [Francese]
setLang('fr') - riga 29
fare clic sul link [English]
setLang('en') - riga 32

8.6.5.5. La vista [home]

Questa è la vista visualizzata immediatamente sotto la barra di navigazione [navbar-run]:

Image

Il suo codice [home.html] è il seguente:


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

Il suo modello è il seguente:

  • [rdvmedecins.medecinItems] (riga 8): l'elenco dei medici;

Nella sua forma attuale, la vista non sembra avere alcun gestore di eventi. In realtà, questi sono definiti nella funzione [initChoixMedecinJour]. Questa funzione è stata presentata nella sezione 8.6.4.7, a pagina 466 e più specificatamente a pagina 469. Contiene i seguenti gestori di eventi:

event
gestore
selezione medico
getAgenda
seleziona una data
getAgenda

8.6.5.6. La vista [calendario]

La vista [agenda] mostra un giorno dal calendario di un medico:

Image

Il codice [agenda.xml] è il seguente:


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

Il modello per questa vista ha un solo elemento:

  • [agenda] (riga 4): un modello piuttosto complesso progettato specificamente per visualizzare il calendario;

Dispone dei seguenti gestori di eventi:

event
gestore
clic sul pulsante [Elimina]
deleteAppt(apptId) - riga 37
fare clic sul link [Prenota]
reserveSlot(idSlot) - riga 34

La vista [resa] alla riga 47 è la vista che viene visualizzata quando l'utente clicca su un link [Prenota]:

Image

Il suo codice [resa.xml] è il seguente:


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

Il suo modello ha un solo elemento:

  • [clientItems] (riga 24): l'elenco dei clienti;

Dispone dei seguenti gestori di eventi:

event
gestore
clic sul pulsante [Annulla]
cancelDialogResa() - riga 30
Clicca sul pulsante [Conferma]
validerRv() - riga 31

8.6.5.7. La vista [errori]

Questa è la vista che appare se l'azione richiesta dall'utente non ha potuto essere completata:

Image

Il codice [errors.xml] è il seguente:


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

Il suo template ha un solo elemento:

  • [errors] (riga 8): l'elenco degli errori da visualizzare;

La vista non ha alcun gestore di eventi.

La tabella seguente elenca le viste e i relativi modelli:

vista
Modello
Gestori di eventi
navbar-start

accedi, setLang
jumbotron


accedi


barra di navigazione-run

esci, setLang
home
rdvmedecins.medecinItems (elenco dei medici)
getCalendar
calendario
calendario (un giorno del calendario)
eliminaAppuntamento, prenotaSlot
prenotare
clientItems (elenco dei clienti)
finestra di dialogo per annullare la prenotazione, conferma appuntamento
errori
errori (elenco degli errori)

8.6.6. Fase 3: Scrittura delle azioni

Torniamo all'architettura del servizio web [Web1]:

Ora vedremo quali URL sono esposti da [Web1] e la loro implementazione:

8.6.6.1. Gli URL esposti dal servizio [Web1]

Sono i seguenti:

  • un URL per ciascuna delle viste precedenti o una loro combinazione;
  • un URL per aggiungere un appuntamento;
  • un URL per cancellare un appuntamento;

Tutti restituiscono una risposta di tipo [Response] come segue:


public class Reponse {
 
    // ----------------- properties
    // operation status
    private int status;
    // the navigation bar
    private String navbar;
    // the jumbotron
    private String jumbotron;
    // the body of the page
    private String content;
    // the diary
    private String agenda;
...
}
  • riga 5: uno stato di risposta: 1 (OK), 2 (errore);
  • riga 7: il flusso HTML per le viste [navbar-start] o [navbar-run], a seconda dei casi;
  • riga 9: il feed HTML per la vista [jumbotron];
  • riga 13: il feed HTML per la vista [agenda];
  • riga 9: il feed HTML per le viste [home], [errors] o [login], a seconda dei casi;

Gli URL esposti sono i seguenti

/getNavbarStart
inserisce la vista [navbar-start] in [Response.navbar]
/getNavbarRun
inserisce la vista [navbar-run] in [Response.navbar]
/getHome
inserisce la vista [home] in [Response.content]
/getJumbotron
inserisce la vista [jumbotron] in [Response.jumbotron]
/getAgenda
inserisce la vista [agenda] in [Response.agenda]
/getLogin
inserisce la vista [login] in [Response.content]
/getNavbarRunJumbotronHome
  • Se la connessione va a buon fine, inserisci la vista [navbar-run] in [Response.navbar], la vista [jumbotron] in [Response.jumbotron] e la vista [home] in [Response.content]
  • Se la connessione non va a buon fine, inserisci la vista [errors] in [Response.content] e imposta [Response.status] su 2
/getNavbarRunJumbotronHomeCalendar
inserisce la vista [navbar-run] in [Response.navbar], la vista [jumbotron] in [Response.jumbotron], la vista [home] in [Response.content] e la vista [calendar] in [Response.calendar]
/addAppointment
aggiunge l'appuntamento selezionato e inserisce la nuova agenda in [Response.agenda]
/cancellaAppuntamento
elimina l'appuntamento selezionato e inserisce il nuovo calendario in [Response.calendar]

8.6.6.2. Il singleton [ApplicationModel]

 

La classe [ApplicationModel] viene istanziata come singola istanza e iniettata nel controller dell'applicazione. Il suo codice è il seguente:


package rdvmedecins.springthymeleaf.server.models;
 
import java.util.ArrayList;
...
 
@Component
public class ApplicationModel implements IDao {
 
....
}
  • riga 6: [ApplicationModel] è un componente Spring;
  • riga 7: che implementa l'interfaccia del livello [DAO]. Lo facciamo in modo che le azioni non debbano conoscere il livello [DAO], ma solo il singleton [ApplicationModel]. L'architettura di [Web1] diventa quindi la seguente:

Torniamo al codice della classe [ApplicationModel]:


package rdvmedecins.springthymeleaf.server.models;
 
import java.util.ArrayList;
...
 
@Component
public class ApplicationModel implements IDao {
 
    // the [DAO] layer
    @Autowired
    private IDao dao;
    // configuration
    @Autowired
    private AppConfig appConfig;
 
    // data from the [DAO] layer
    private List<ClientItem> clientItems;
    private List<MedecinItem> medecinItems;
    // configuration data
    private String userInit;
    private String mdpUserInit;
    private boolean corsAllowed;
    // exception
    private RdvMedecinsException rdvMedecinsException;
 
    // manufacturer
    public ApplicationModel() {
    }
 
    @PostConstruct
    public void init() {
        // config
        userInit = appConfig.getUSER_INIT();
        mdpUserInit = appConfig.getMDP_USER_INIT();
        dao.setTimeout(appConfig.getTIMEOUT());
        dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
        corsAllowed = appConfig.isCORS_ALLOWED();
        // caching of physician and customer drop-down lists
        List<Medecin> medecins = null;
        List<Client> clients = null;
        try {
            medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
            clients = dao.getAllClients(new User(userInit, mdpUserInit));
        } catch (RdvMedecinsException ex) {
            rdvMedecinsException = ex;
        }
        if (rdvMedecinsException == null) {
            // create drop-down list items
            medecinItems = new ArrayList<MedecinItem>();
            for (Medecin médecin : medecins) {
                medecinItems.add(new MedecinItem(médecin));
            }
            clientItems = new ArrayList<ClientItem>();
            for (Client client : clients) {
                clientItems.add(new ClientItem(client));
            }
        }
    }
 
    // getters and setters
    ...
 
    // interface implementation [IDao]
    @Override
    public void setUrlServiceWebJson(String url) {
        dao.setUrlServiceWebJson(url);
    }
 
    @Override
    public void setTimeout(int timeout) {
        dao.setTimeout(timeout);
    }
 
    @Override
    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
        return dao.ajouterRv(user, jour, idCreneau, idClient);
    }
 
    ...
}
  • riga 11: inserimento del riferimento all'implementazione del livello [DAO]. Questo riferimento viene poi utilizzato per implementare l'interfaccia [IDao] (righe 64–80);
  • riga 14: inserimento della configurazione dell'applicazione;
  • righe 33–37: utilizzo di questa configurazione per configurare vari elementi dell'architettura dell'applicazione;
  • Righe 38–46: Memorizziamo nella cache le informazioni che andranno a popolare gli elenchi a discesa relativi ai medici e ai clienti. Partiamo quindi dal presupposto che, se un medico o un cliente cambia, l'applicazione debba essere riavviata. L'idea è quella di dimostrare che un singleton Spring può fungere da cache per l'applicazione web;

Le classi [MedecinItem] e [ClientItem] derivano entrambe dalla seguente classe [PersonneItem]:


package rdvmedecins.springthymeleaf.server.models;
 
import rdvmedecins.client.entities.Personne;
 
public class PersonneItem {
 
    // element of a list
    private Long id;
    private String texte;
 
    // manufacturer
    public PersonneItem() {
 
    }
 
    public PersonneItem(Personne personne) {
        id = personne.getId();
        texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
    }
 
    // getters and setters
...
}
  • riga 8: il campo [id] sarà il valore dell'attributo [value] di un'opzione dell'elenco a discesa;
  • riga 9: il campo [text] sarà il testo visualizzato da un'opzione dell'elenco a discesa;

8.6.6.3. La classe [BaseController]

 

La classe [BaseController] è la classe padre dei controller [RdvMedecinsController] e [RdvMedecinsCorsController]. Non era obbligatorio creare questa classe padre. Abbiamo raggruppato qui i metodi di utilità della classe [RdvMedecinsController], nessuno dei quali è essenziale tranne uno. Possono essere classificati in tre gruppi:

  1. metodi di utilità;
  2. metodi che rendono le viste unite ai loro modelli;
  3. il metodo per l'inizializzazione di un'azione

protected List<String>
getErrorsForException(Exception exception)

protected List<String>
getErrorsForModel(BindingResult result,
Locale locale,
WebApplicationContext ctx)
due metodi di utilità che forniscono un elenco di messaggi di errore. Li abbiamo già incontrati e utilizzati;

protected String getPartialViewHome(WebContext
thymeleafContext)
restituisce la vista [home] senza un template

protected String getPartialViewAgenda(ActionContext
actionContext,
AgendaMedecinJour,
Locale locale)
restituisce la vista [agenda] e il relativo modello

protected String getPartialViewLogin(WebContext thymeleafContext)
restituisce la vista [login] senza un modello

protected Response getViewErrors(WebContext thymeleafContext, List<String> errors)
restituisce la risposta al client quando l'azione richiesta da ha generato un errore

protected ActionContext getActionContext
(String lang, String origin,
HttpServletRequest request,
HttpServletResponse response,
BindingResult result,
RdvMedecinsCorsController rdvMedecinsCorsController)
il metodo di inizializzazione per tutte le azioni del controller [RdvMedecinsController]

Esaminiamo due di questi metodi.

Il metodo [getPartialViewAgenda] rende la vista più complessa da generare, quella del calendario. Il suo codice è il seguente:


    // feed [agenda]
    protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
        // contexts
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        // build the [agenda] page template
        ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
        // the agenda with its model
        thymeleafContext.setVariable("agenda", modelAgenda);
        thymeleafContext.setVariable("clientItems", application.getClientItems());
        return engine.process("agenda", thymeleafContext);
}
  • Righe 9–10: i due elementi del modello del calendario:
    • riga 9: il calendario visualizzato.
    • riga 10: l'elenco dei clienti visualizzato quando l'utente fissa un appuntamento;

Il metodo [setModelforAgenda] alla riga 7 è il seguente:


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

Image

oppure:

Image

Possiamo notare che il formato della data dipende dalla lingua. Recuperiamo questo formato dai file dei messaggi (riga 4).

  • righe 11–40: per ogni intervallo di tempo, dobbiamo visualizzare la vista:

Image

oppure la vista:

Image

  • Righe 19–20: visualizza la fascia oraria;
  • righe 25–28: il caso in cui la fascia oraria è disponibile. In questo caso, deve essere visualizzato il pulsante [Prenota];
  • righe 31–36: il caso in cui la fascia oraria è occupata. In questo caso, devono essere visualizzati sia il cliente che il pulsante [Elimina];

L'altro metodo di cui parleremo più nel dettaglio è il metodo [getActionContext]. Viene chiamato all'inizio di ogni azione nel [RdvMedecinsController]. La sua firma è la seguente:


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

Restituisce il seguente tipo [ActionContext]:


public class ActionContext {
 
    // data
    private WebContext thymeleafContext;
    private WebApplicationContext springContext;
    private Locale locale;
    private List<String> erreurs;
...
}
  • riga 4: il contesto Thymeleaf dell'azione;
  • riga 5: il contesto Spring dell'azione;
  • riga 6: le impostazioni locali dell'azione;
  • riga 7: un elenco di possibili messaggi di errore;

I suoi parametri sono i seguenti:

  • [lang]: la lingua richiesta per l'azione, 'en' o 'fr';
  • [origin]: l'intestazione HTTP [origin] nel caso di una richiesta cross-domain;
  • [request]: la richiesta HTTP attualmente in elaborazione, ciò che da tempo viene definito "azione";
  • [response]: la risposta che verrà inviata in risposta a questa richiesta;
  • [result]: ogni azione di [RdvMedecinsController] riceve un valore inviato la cui validità viene verificata. [result] è il risultato di questa verifica;
  • [rdvMedecinsController]: il controller che contiene le azioni;

Il metodo [getActionContext] è implementato come segue:


    // context of an action
    protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
        // language?
        if (lang == null) {
            lang = "fr";
        }
        // local
        Locale locale = null;
        if (lang.trim().toLowerCase().equals("fr")) {
            // french
            locale = new Locale("fr", "FR");
        } else {
            // everything else in English
            locale = new Locale("en", "US");
        }
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, response);
        // ActionContext
        ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
        // initialization errors
        RdvMedecinsException e = application.getRdvMedecinsException();
        if (e != null) {
            actionContext.setErreurs(e.getMessages());
            return actionContext;
        }
        // POST errors?
        if (result != null && result.hasErrors()) {
            actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
            return actionContext;
        }
        // no errors
        return actionContext;
}
  • righe 3–15: in base al parametro [lang], impostiamo le impostazioni locali dell'azione;
  • riga 17: inviamo le intestazioni HTTP necessarie per le richieste cross-domain. Non entreremo nei dettagli in questa sede. La tecnica utilizzata è quella descritta nella sezione 8.4.14;
  • riga 19: creazione di un oggetto [ActionContext] senza errori;
  • riga 21: abbiamo visto nella sezione 8.6.6.2 che il singleton [ApplicationModel] ha effettuato l'accesso al database per recuperare sia i clienti che i medici. Questo accesso potrebbe fallire. Registriamo quindi l'eccezione che si verifica. Alla riga 21, recuperiamo questa eccezione;
  • righe 22–25: se si è verificata un'eccezione durante l'avvio dell'applicazione, non è possibile alcuna azione. Restituiamo quindi un oggetto [ActionContext] per qualsiasi azione, contenente i messaggi di errore dell'eccezione;
  • righe 27–20: analizziamo il parametro [result] per determinare se il valore inviato fosse valido o meno. Se non era valido, restituiamo un oggetto [ActionContext] con i messaggi di errore appropriati;
  • riga 32: caso senza errori;

Esamineremo ora le azioni del [RdvMedecinsController]

8.6.6.4. L'azione [/getNavBarStart]

L'azione [/getNavBarStart] rende la vista [navbar-start]. La sua firma è la seguente:


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

Restituisce il seguente tipo [Response]:


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

e presenta i seguenti parametri:

  • [PostLang postlang]: il valore pubblicato successivo:

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

La classe [PostLang] è la classe padre di tutti i valori inviati. Questo perché il client deve sempre specificare la lingua in cui deve essere eseguita l'azione.

Il metodo [getNavbarStart] è implementato come segue:


    // navbar-start
    @RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // returns the [navbar-start] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
        return reponse;
}
  • riga 7: inizializzazione dell'azione;
  • righe 10–13: se il metodo di inizializzazione dell'azione ha segnalato errori, questi vengono inviati nella risposta al client (riga 12) con lo stato 2:
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • righe 15-18: invia la vista [navbar-start] con lo stato 1:
 {"status":1,"navbar": navbar-start, "jumbotron": null, "agenda":null, "content":null}

Di seguito descriveremo solo le nuove funzionalità.

8.6.6.5. L'azione [/getNavbarRun]

L'azione [/getNavBarRun] visualizza la vista [navbar-run]:


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

L'azione può restituire due tipi di risposte:

  • la risposta con un errore (righe 10–13):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • la risposta con la vista [navbar-run]:
 {"status":1,"navbar": navbar-run, "jumbotron": null, "agenda":null, "content":null}

8.6.6.6. L'azione [/getJumbotron]

L'azione [/getJumbotron] restituisce la vista [jumbotron]:


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

L'operazione può restituire due tipi di risposta:

  • la risposta con un errore (righe 10–13):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • risposta con la vista [jumbotron]:
 {"status":1,"navbar": null, "jumbotron": jumbotron, "agenda":null, "content":null}

8.6.6.7. L'azione [/getLogin]

L'azione [/getLogin] visualizza la vista [login]:


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

L'azione può restituire due tipi di risposte:

  • La risposta con un errore (righe 9–11):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • La risposta con la vista [login]:
 {"status":1,"navbar": navbar-start, "jumbotron": jumbotron, "agenda":null, "content":login}

8.6.6.8. L'azione [/getHome]

L'azione [/getHome] restituisce la vista [home]. La sua firma è la seguente:


    @RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) 
  • Riga 3: Il valore inviato è di tipo [PostUser] come segue:

public class PostUser extends PostLang {
    // data
    @NotNull
    private User user;
...
}
  • riga 1: la classe [PostUser] estende la classe [PostLang] e quindi include una lingua;
  • riga 4: l'utente che tenta di recuperare la vista;

Il codice di implementazione è il seguente:


    @RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // the [home] view is protected
        try{
            // user
            User user = postUser.getUser();
            // we check identifiers [userName, password]
            application.authenticate(user);
        }catch(RdvMedecinsException e){
            // an error is returned
            return getViewErreurs(thymeleafContext, e.getMessages());
        }
        // returns the [home] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        return reponse;
}
  • Righe 15–22: Si noti che la pagina [home] è protetta, quindi l'utente deve essere autenticato;

L'azione può restituire due tipi di risposte:

  • la risposta di errore (righe 11 e 21):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • risposta con la vista [home] (righe 24–27):
 {"status":1,"navbar": null, "jumbotron": null, "agenda":null, "content":accueil}

8.6.6.9. L'azione [/getNavbarRunJumbotronHome]

L'azione [/getNavbarRunJumbotronHome] rende le viste [navbar-run, jumbotron, home]. Ha la seguente firma:


@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser post, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) 
  • riga 3: il valore inviato è di tipo [PostUser];

L'implementazione dell'azione è la seguente:


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

L'azione può restituire due tipi di risposte:

  • la risposta con un errore (righe 13, 23):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • la risposta con le viste [navbar-run, jumbotron, home] (righe 26–31):
 {"status":1,"navbar": navbar-run, "jumbotron": jumbotron, "agenda":null, "content":accueil}

8.6.6.10. L'azione [/getAgenda]

L'azione [/getAgenda] visualizza la vista [agenda]. La sua firma è la seguente:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin)
  • Riga 3: Il valore inviato è di tipo [PostGetAgenda] come segue:

public class PostGetAgenda extends PostUser {
 
    // data
    @NotNull
    private Long idMedecin;
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date jour;
...
}
  • riga 1: la classe [PostGetAgenda] estende la classe [PostUser] e quindi include una lingua e un utente;
  • riga 5: l'ID del medico di cui si desidera il calendario;
  • riga 8: il giorno desiderato del calendario;

L'implementazione è la seguente:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result,    rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        Locale locale = actionContext.getLocale();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // check the validity of the post
        if (result != null) {
            new PostGetAgendaValidator().validate(postGetAgenda, result);
            if (result.hasErrors()) {
                // returns the [errors] view
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        ...
}
  • Fino alla riga 14, il codice è ora standard;
  • righe 16–21: eseguiamo un controllo aggiuntivo sul valore inviato. La data deve essere uguale o successiva alla data odierna. Per verificarlo, utilizziamo un validatore:

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

Torniamo al codice dell'azione [/getAgenda]:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        ...
                // action
        try {
            // doctor's diary
            AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
                    new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
            // answer
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException e1) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, e1.getMessages());
        } catch (Exception e2) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
}
  • righe 9-10: utilizzando i parametri inviati, richiediamo l'orario del medico;
  • righe 12-13: restituiamo l'orario:
 {"status":1,"navbar": null, "jumbotron": null, "agenda":agenda, "content":null}
  • righe 17, 21: restituiamo una risposta con errori:
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}

8.6.6.11. L'azione [/getNavbarRunJumbotronHomeCalendar]

L'azione [/getNavbarRunJumbotronHomeCalendar] rende le viste [navbar-run, jumbotron, home, calendar]. La sua implementazione è la seguente:


    @RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
            HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // agenda
        Reponse agenda = getAgenda(post, result, request, response, null);
        if (agenda.getStatus() != 1) {
            return agenda;
        }
        // we send the answer
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        reponse.setAgenda(agenda.getAgenda());
        return reponse;
}
  • righe 15–18: Sfruttiamo l'esistenza dell'azione [/getAgenda] per richiamarla. Quindi controlliamo lo stato della risposta (riga 16). Se viene rilevato un errore, ci fermiamo lì e restituiamo la risposta;
  • riga 20: inviamo le viste richieste:
 {"status":1,"navbar": navbar-run, "jumbotron": jumbotron, "agenda":agenda, "content":accueil}

8.6.6.12. L'azione [/supprimerRv]

L'azione [/deleteRv] consente di eliminare un appuntamento. La sua firma è la seguente:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin)
  • Riga 3: Il valore inviato è di tipo [PostSupprimerRv] come segue:

public class PostSupprimerRv extends PostUser {
 
    // data
    @NotNull
    private Long idRv;
..
}
  • riga 1: la classe [PostSupprimerRv] estende la classe [PostUser] e quindi include una lingua e un utente;
  • riga 5: il numero dell'appuntamento da eliminare;

L'implementazione dell'azione è la seguente:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
                rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // posted values
        User user = postSupprimerRv.getUser();
        long idRv = postSupprimerRv.getIdRv();
        // we delete the appointment
        AgendaMedecinJour agenda = null;
        try {
            // we get it back
            Rv rv = application.getRvById(user, idRv);
            Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
            long idMedecin = creneau.getIdMedecin();
            Date jour = rv.getJour();
            // delete the associated rv
            application.supprimerRv(user, idRv);
            // we regenerate the doctor's diary
            agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // we return the new diary
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
}
  • riga 22: recupera l'appuntamento da eliminare. Se non esiste, viene generata un'eccezione;
  • righe 23–25: in base a questo appuntamento, individuiamo il medico e il giorno corrispondente. Queste informazioni sono necessarie per rigenerare l'agenda del medico;
  • riga 27: l'appuntamento viene cancellato;
  • riga 29: richiediamo il nuovo orario del medico. Questo è importante. Oltre allo slot appena liberato, altri utenti dell’applicazione potrebbero aver apportato modifiche all’orario. È importante restituire all’utente la versione più recente dell’orario;
  • righe 31–34: viene restituito il calendario:
 {"status":1,"navbar": null, "jumbotron": null, "agenda":agenda, "content":null}

8.6.6.13. L'azione [/validerRv]

L'azione [/validerRv] aggiunge un appuntamento al calendario di un medico. La sua firma è la seguente:


@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request,    HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
  • Riga 3: Il valore inviato è di tipo [PostValiderRv] come segue:

public class PostValiderRv extends PostUser {
 
    // data
    @NotNull
    private Long idCreneau;
    @NotNull
    private Long idClient;
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date jour;
...
}
  • riga 1: la classe [PostValiderRv] estende la classe [PostUser] e quindi include una lingua e un utente;
  • riga 5: il numero della fascia oraria;
  • riga 7: l'ID del cliente per il quale è stata effettuata la prenotazione;
  • riga 10: il giorno dell'appuntamento;

L'implementazione dell'azione è la seguente:


// appointment validation
    @RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebApplicationContext springContext = actionContext.getSpringContext();
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // check the validity of the appointment date
        if (result != null) {
            new PostGetAgendaValidator().validate(postValiderRv, result);
            if (result.hasErrors()) {
                // returns the [errors] view
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        // posted values
        User user = postValiderRv.getUser();
        long idClient = postValiderRv.getIdClient();
        long idCreneau = postValiderRv.getIdCreneau();
        Date jour = postValiderRv.getJour();
        // action
        try {
            // get information on the niche
            Creneau créneau = application.getCreneauById(user, idCreneau);
            long idMedecin = créneau.getIdMedecin();
            // we add the Rv
            application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
            // we regenerate the agenda
            AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
                    new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // we return the new diary
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
    }
}

Il codice è simile a quello dell'azione [/deleteRv].

8.6.7. Passaggio 4: Test del server Spring/Thymeleaf

Ora testeremo le varie azioni descritte sopra utilizzando il plugin di Chrome [Advanced Rest Client] (vedi sezione 9.6).

8.6.7.1. Configurazione del test

Tutte le azioni richiedono un valore inviato. Invieremo diverse varianti della seguente stringa JSON:

{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Questo valore inviato include informazioni superflue per la maggior parte delle azioni. Tuttavia, queste vengono ignorate dalle azioni che le ricevono e non causano un errore. Questo valore inviato ha il vantaggio di coprire i vari valori da inviare.

8.6.7.2. L'azione [/getNavbarStart]

  • in [1], l'azione in fase di test;
  • in [2], il valore inviato;
  • in [3], il valore inviato è una stringa JSON;
  • in [4], la vista [navbar-start] viene richiesta in inglese;

Il risultato ottenuto è il seguente:

 

Abbiamo ricevuto la vista [navbar-start] in inglese (aree evidenziate).

Ora, introduciamo un errore. Impostiamo l'attributo [lang] del valore inviato su null. Otteniamo il seguente risultato:

 

Abbiamo ricevuto una risposta di errore (stato 2) che indica che il campo [lang] era obbligatorio.

8.6.7.3. L'azione [/getNavbarRun]

Richiediamo l'azione [getNavbarRun] con il seguente valore inviato:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato ottenuto è il seguente:

 

8.6.7.4. L'azione [/getJumbotron]

Richiediamo l'azione [getJumbotron] con i seguenti dati POST:


{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato ottenuto è il seguente:

 

8.6.7.5. L'azione [/getLogin]

Richiediamo l'azione [getLogin] con i seguenti dati POST:


{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato è il seguente:

 

8.6.7.6. L'azione [/getAccueil]

Richiediamo l'azione [getAccueil] con il seguente valore inviato:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato ottenuto è il seguente:

 

Proviamo di nuovo con un utente sconosciuto:


{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato è il seguente:

 

Ricominciamo con un utente esistente che non è autorizzato a utilizzare l'applicazione:


{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato è il seguente:

 

8.6.7.7. L'azione [/getAgenda]

Richiediamo l'azione [getAgenda] con il seguente valore inviato:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato ottenuto è il seguente:

 

Proviamo di nuovo con una data precedente a oggi:

 

Ricominciamo con un medico inesistente:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":11, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato è il seguente:

 

8.6.7.8. L'azione [/getNavbarRunJumbotronAccueil]

Richiediamo l'azione [getNavbarRunJumbotronAccueil] con il seguente valore inviato:


{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato è il seguente:

 

Lo stesso vale per un utente sconosciuto:

 

8.6.7.9. L'azione [/getNavbarRunJumbotronHomeCalendar]

Richiediamo l'azione [getNavbarRunJumbotronHomeCalendar] con il seguente valore inviato:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato è il seguente:

 

Inseriamo un medico che non esiste:

 

8.6.7.10. L'azione [/deleteAppointment]

Richiediamo l'azione [deleteAppointment] con il seguente valore inviato:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

L'appuntamento n. 93 non esiste. Il risultato ottenuto è il seguente:

 

Con un appuntamento esistente:

 

Possiamo verificare nel database che l'appuntamento sia stato effettivamente cancellato. Viene restituito il nuovo calendario.

8.6.7.11. L'azione [/validateAppointment]

Richiediamo l'azione [validateAppointment] con il seguente valore inviato:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

Il risultato è il seguente:

 

Possiamo verificare nel database che l'appuntamento è stato creato correttamente. Il nuovo calendario è stato restituito.

Facciamo la stessa cosa con un numero di slot inesistente:

 

Facciamo lo stesso con un ID cliente inesistente:

 

8.6.8. Passaggio 5: Scrittura del client JavaScript

Torniamo all'architettura del server [Web1]:

Il client [2] del server [Web1] è un client JavaScript di tipo SPV (Single-Page Application):

  • Il client richiede la pagina di avvio da un server web (non necessariamente [Web1]);
  • richiede le pagine seguenti dal server [Web1] tramite chiamate Ajax;

Per creare questo client, useremo lo strumento [Webstorm] (vedi sezione 9.8). Ho trovato questo strumento più pratico di STS. Il suo vantaggio principale è che offre il completamento automatico del codice e alcune opzioni di refactoring. Questo aiuta a prevenire molti errori.

8.6.8.1. Il progetto JS

Il progetto JS presenta la seguente struttura di directory:

  • in [1], il client JS nel suo complesso. [boot.html] è la pagina di avvio. Questa sarà l'unica pagina caricata dal browser;
  • in [2], i fogli di stile per i componenti Bootstrap;
  • in [3], le poche immagini utilizzate dall'applicazione;
  • in [4], gli script JS. È qui che si svolge il nostro lavoro;
  • in [5], le librerie JS utilizzate: principalmente jQuery e quelle per i componenti Bootstrap;

8.6.8.2. L'architettura del codice

Il codice è stato suddiviso in tre livelli:

  • il livello [presentazione] contiene le funzioni di inizializzazione della pagina [boot.xml] e quelle relative ai vari componenti Bootstrap. È implementato dal file [ui.js];
  • il livello [events] contiene tutti i gestori di eventi per il livello [presentation]. È implementato dal file [evts.js];
  • il livello [DAO] effettua richieste HTTP al server [Web1]. È implementato dal file [dao.js];

8.6.8.3. Il livello [presentation]

  

Il livello [presentazione] è implementato dal seguente file [ui.js]:


//la couche [présentation]
var ui = {
// variables globales;
  "agenda": "",
  "resa": "",
  "langue": "",
  "urlService": "http://localhost:8081",
  "page": "login",
  "jourAgenda": "",
  "idMedecin": "",
  "user": {},
  "login": {},
  "exceptionTitle": {},
  "calendar_infos": {},
  "erreur": "",
  "idCreneau": "",
  "done": "",
// composants de la vue
  "body": "",
  "navbar": "",
  "jumbotron": "",
  "content": "",
  "exception": "",
  "exception_text": "",
  "exception_title": "",
  "loading": ""
};
// la couche des evts
var evts = {};
// la couche [dao]
var dao = {};
 
// ------------ document ready
$(document).ready(function () {
  // initialisation document
  console.log("document.ready");
  // composants de la page
  ui.navbar = $("#navbar");
  ui.jumbotron = $("#jumbotron");
  ui.content = $("#content");
  ui.erreur = $("#erreur");
  ui.exception = $("#exception");
  ui.exception_text = $("#exception-text");
  ui.exception_title = $("#exception-title");
  // on mémorise la page de login pour pouvoir la restituer
  ui.login.lang = ui.langue;
  ui.login.navbar = ui.navbar.html();
  ui.login.jumbotron = ui.jumbotron.html();
  ui.login.content = ui.content.html();
  // URL du service
  $("#urlService").val(ui.urlService);
});
 
// ------------------------ Bootstrap component initialization functions
ui.initNavBarStart = function () {
...
};
 
ui.initNavBarRun = function () {
...
};
 
ui.initChoixMedecinJour = function () {
...
};
 
ui.updateCalendar = function (renew) {
...
};
 
// affiche le jour sélectionné
ui.displayJour = function () {
...
};
 
ui.initAgenda = function () {
...
};
 
ui.initResa = function () {
 ...
};
 
  • Per isolare i livelli l'uno dall'altro, si è deciso di inserirli in tre oggetti:
    • [ui] per il livello [presentazione] (righe 2–27),
    • [evts] per il livello di gestione degli eventi (riga 29),
    • [dao] per il livello [DAO] (riga 31);

Questa separazione dei livelli in tre oggetti aiuta a evitare una serie di conflitti tra i nomi delle variabili e delle funzioni. Ogni livello utilizza variabili e funzioni precedute dal prefisso dell'oggetto che incapsula il livello.

  • righe 38–44: memorizziamo i campi che saranno sempre presenti indipendentemente dalle viste visualizzate. Questo evita ricerche jQuery ripetitive e inutili;
  • righe 46–49: la pagina di avvio viene memorizzata localmente in modo da poter essere ripristinata quando l'utente esce dal sistema e non ha modificato la lingua;
  • righe 54–83: funzioni di inizializzazione dei componenti Bootstrap. Queste sono state tutte trattate nella discussione sui componenti Bootstrap nella sezione 8.6.4;

8.6.8.4. Funzioni di utilità del livello [events]

  

I gestori di eventi sono stati inseriti nel file [evts.js]. Diverse funzioni vengono utilizzate regolarmente dai gestori di eventi. Le presentiamo ora:


// début d'attente
evts.beginWaiting = function () {
  // début attente
  ui.loading = $("#loading");
  ui.loading.show();
  ui.exception.hide();
  ui.erreur.hide();
  evts.travailEnCours = true;
};
 
// fin d'attente
evts.stopWaiting = function () {
  // fin attente
  evts.travailEnCours = false;
  ui.loading = $("#loading");
  ui.loading.hide();
};
 
// affichage résultat
evts.showResult = function (result) {
  // on affiche les données reçues
  var data = result.data;
  // on analyse le status
  switch (result.status) {
    case 1:
      // erreur ?
      if (data.status == 2) {
        ui.erreur.html(data.content);
        ui.erreur.show();
      } else {
        if (data.navbar) {
          ui.navbar.html(data.navbar);
        }
        if (data.jumbotron) {
          ui.jumbotron.html(data.jumbotron);
        }
        if (data.content) {
          ui.content.html(data.content)
        }
        if (data.agenda) {
          ui.agenda = $("#agenda");
          ui.resa = $("#resa");
        }
      }
      break;
    case 2:
      // affichage erreur
      evts.showException(data);
      break;
  }
};
 
// ------------ fonctions diverses
evts.showException = function (data) {
  // affichage erreur
  ui.exception.show();
  ui.exception_text.html(data);
  ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
  • riga 2: la funzione [evts.beginwaiting] viene chiamata prima di qualsiasi azione [DAO] asincrona;
  • righe 4-5: viene visualizzata l'immagine animata dell'attesa;
  • righe 6-7: l'area di visualizzazione degli errori e delle eccezioni viene nascosta (non sono la stessa cosa);
  • riga 8: si nota che è in corso un'attività asincrona;
  • riga 12: la funzione [evts.stopwaiting] viene chiamata dopo che un'azione [DAO] asincrona ha restituito il proprio risultato;
  • riga 14: si nota che l'operazione asincrona è completata;
  • riga 15: l'immagine animata di attesa viene nascosta;
  • riga 20: la funzione [evts.showResult] visualizza il risultato [result] di un'azione asincrona [DAO]. Il risultato è un oggetto JS della seguente forma: {'status':status,'data':data,'sendMeBack':sendMeBack}.
  • righe 47–50: utilizzate se [result.status == 2]. Ciò si verifica quando il server [Web1] invia una risposta con un'intestazione di errore HTTP (ad es. 403 Forbidden). In questo caso, [data] è la stringa JSON inviata dal server per indicare l'errore;
  • riga 25: caso in cui sia stata ricevuta una risposta valida dal server [Web1]. Il campo [data] contiene quindi la risposta del server: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
  • riga 27: caso in cui il server [Web1] ha inviato una risposta di errore {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':errors};
  • righe 28–29: viene visualizzata la vista [errori];
  • righe 31-33: visualizzazione opzionale della barra di navigazione;
  • righe 34-36: visualizzazione opzionale del jumbotron;
  • righe 37-39: il campo [data.content] può essere visualizzato. A seconda dei casi, questo rappresenta una delle viste [home, calendar];
  • righe 40-43: se il calendario è stato rigenerato, vengono recuperati alcuni riferimenti ai suoi componenti in modo che non debbano essere ricercati ogni volta che sono necessari;
  • riga 54: la funzione [evts.showException] visualizza il testo dell'eccezione contenuto nel suo parametro [data];
  • righe 57-58: viene visualizzato il testo dell'eccezione;
  • riga 58: il titolo dell'eccezione dipende dalla lingua corrente;

Il file [evts.js] contiene oltre 300 righe di codice, che non commenterò nella loro interezza. Mi limiterò a evidenziare alcuni esempi per illustrare lo scopo di questo livello.

8.6.8.5. Accesso utente

Image

Il login dell'utente è gestito dalla seguente funzione:


// ------------------------ connexion
evts.connecter = function () {
  // retrieve the values to be posted
  var login = $("#login").val().trim();
  var passwd = $("#passwd").val().trim();
  // set the server's URL
  ui.urlService = $("#urlService").val().trim();
  dao.setUrlService(ui.urlService);
  // query parameters
  var post = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "lang": ui.langue
  };
  var sendMeBack = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "caller": evts.connecterDone
  };
  // query
  evts.execute([{
    "name": "accueil-sans-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • righe 4-5: recupera il nome utente e la password dell'utente;
  • righe 7-8: recupera l'URL del servizio [Web1]. Viene memorizzato sia nel livello [ui] che nel livello [dao];
  • righe 10-16: il valore da inviare: la lingua corrente e l'utente che sta tentando di effettuare l'accesso;
  • righe 17–23: l'oggetto [sendMeBack] viene passato alla funzione [DAO] che verrà chiamata, e questa funzione deve restituirlo alla funzione alla riga 22. Qui, l'oggetto [sendMeBack] incapsula l'utente che sta tentando di effettuare il login;
  • righe 25–29: la funzione [evts.execute] è in grado di eseguire una sequenza di azioni asincrone. Qui, passiamo una lista composta da una singola azione. I suoi campi sono i seguenti:
    • [name]: il nome dell'azione asincrona da eseguire,
    • [post]: il valore da inviare al server [Web1],
    • [sendMeBack]: il valore che l'azione asincrona deve restituire insieme al suo risultato;

Prima di entrare nei dettagli della funzione [evts.execute], diamo un'occhiata alla funzione [evts.connecterDone] alla riga 22. Questa è la funzione alla quale la funzione [DAO] asincrona chiamata deve restituire il proprio risultato:


evts.connecterDone = function (result) {
  // affichage résultat
  evts.showResult(result);
  // connexion réussie ?
  if (result.status == 1 && result.data.status == 1) {
    // page
    ui.page = "accueil-sans-agenda";
    // on note l'utilisateur
    ui.user = result.sendMeBack.user;
  }
};
  • riga 3: viene visualizzato il risultato restituito dal server [Web1];
  • riga 5: se questo risultato non contiene errori, memorizziamo il tipo della nuova pagina (riga 7) e l'utente autenticato (riga 9);

La funzione [evts.execute] esegue una sequenza di azioni asincrone:


// exécution d'une suite d'actions
evts.execute = function (actions) {
  // travail en cours ?
  if (evts.travailEnCours) {
    // on ne fait rien
    return;
  }
  // attente
  evts.beginWaiting();
  // exécution des actions
  dao.doActions(actions, evts.stopWaiting);
};
  • riga 2: il parametro [actions] è un elenco di azioni asincrone da eseguire;
  • righe 4–7: l'esecuzione è accettata solo se non è già in corso nessun'altra azione;
  • riga 9: viene avviata l'attesa;
  • riga 11: al livello [DAO] viene richiesto di eseguire la sequenza di azioni. Il secondo parametro è il nome della funzione da eseguire una volta che tutte le azioni della sequenza hanno restituito i propri risultati;

Non entreremo nei dettagli della funzione [dao.doActions] in questa sede. Esamineremo un altro evento.

8.6.8.6. Cambio di lingua

Image

Il cambio di lingua è gestito dalla seguente funzione:


// ------------------------ changement de langue
evts.setLang = function (lang) {
  // chgt de langue ?
  if (lang == ui.langue) {
    // on ne fait rien
    return;
  }
  // nouvelle langue
  ui.langue = lang;
  // quelle page faut-il traduire ?
  switch (ui.page) {
    case "login":
      evts.getLogin();
      break;
    case "accueil-sans-agenda":
      evts.getAccueilSansAgenda();
      break;
    case "accueil-avec-agenda":
      evts.getAccueilAvecAgenda(ui);
      break;
  }
};
  • riga 2: il parametro [lang] è la nuova lingua: 'fr' o 'en';
  • righe 4–7: se la nuova lingua è quella corrente, non fare nulla;
  • riga 9: la nuova lingua viene memorizzata;
  • righe 12–20: se la lingua è cambiata, la pagina attualmente visualizzata dal browser deve essere ricaricata. Ci sono tre pagine possibili:
    • quella denominata [login], in cui viene visualizzata la pagina di accesso,
    • quella denominata [home-without-calendar], che è la pagina visualizzata immediatamente dopo l'autenticazione riuscita,
    • quella denominata [home-with-calendar], che è la pagina visualizzata non appena viene visualizzato il primo calendario. Rimane quindi sullo schermo fino a quando l'utente non effettua il logout;

Affronteremo il caso della pagina [home-with-calendar]. Esistono tre versioni di questa funzione:

  
  • la versione [getAccueilAvecAgenda-one] esegue una singola azione asincrona;
  • la versione [getAccueilAvecAgenda-parallel] esegue quattro azioni asincrone in parallelo;
  • la versione [getAccueilAvecAgenda-sequence] esegue quattro azioni asincrone una dopo l'altra;

8.6.8.7. La funzione [getAccueilAvecAgenda-one]

Si tratta della seguente funzione:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // query parameters
  var post = {
    "user": ui.user,
    "lang": ui.langue,
    "idMedecin": ui.idMedecin,
    "jour": ui.jourAgenda
  };
  var sendMeBack = {
    "caller": evts.getAccueilAvecAgendaDone
  };
  // request
  evts.execute([{
    "name": "accueil-avec-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • righe 4-9: il valore da inviare incapsula l'utente connesso, la lingua desiderata, l'ID del medico di cui si desidera conoscere l'orario e il giorno dell'appuntamento desiderato;
  • righe 10–12: l'oggetto [sendMeBack] è l'oggetto che verrà restituito alla funzione alla riga 11. Qui non contiene alcuna informazione;
  • righe 14–18: esecuzione di una sequenza di azioni asincrone, in particolare quella denominata [welcome-with-calendar] (riga 15);
  • riga 11: la funzione eseguita quando l'azione asincrona [welcome-with-calendar] restituisce il suo risultato;

La funzione [evts.getAccueilAvecAgendaDone] alla riga 11 visualizza il risultato della funzione asincrona denominata [accueil-avec-agenda]:


evts.getAccueilAvecAgendaDone = function (result) {
  // affichage résultat
  evts.showResult(result);
  // nouvelle page ?
  if (result.status == 1 && result.data.status == 1) {
    ui.page = "accueil-avec-agenda";
  }
};
  • riga 1: [result] è il risultato della funzione asincrona denominata [home-with-calendar];
  • riga 3: questo risultato viene visualizzato;
  • riga 5: se si tratta di un risultato senza errori, viene caricata la nuova pagina (riga 6);

8.6.8.8. La funzione [getHomeWithCalendar-parallel]

Si tratta della seguente funzione:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // actions [navbar-run, jumbotron, home, calendar] in //
  // navbar-run
  var navbarRun = {
    "name": "navbar-run"
  };
  navbarRun.post = {
    "lang": ui.langue
  };
  navbarRun.sendMeBack = {
    "caller": evts.showResult
  };
  // jumbotron
  var jumbotron = {
    "name": "jumbotron"
  };
  jumbotron.post = {
    "lang": ui.langue
  };
  jumbotron.sendMeBack = {
    "caller": evts.showResult
  };
  // home
  var accueil = {
    "name": "accueil"
  };
  accueil.post = {
    "lang": ui.langue,
    "user": ui.user
  };
  accueil.sendMeBack = {
    "caller": evts.showResult
  };
  // agenda
  var agenda = {
    "name": "agenda"
  };
  agenda.post = {
    "user": ui.user,
    "lang": ui.langue,
    "idMedecin": ui.idMedecin,
    "jour": ui.jourAgenda
  };
  agenda.sendMeBack = {
    'idMedecin': ui.idMedecin,
    'jour': ui.jourAgenda,
    "caller": evts.getAgendaDone
  };
  // execution actions in //
  evts.execute([navbarRun, jumbotron, accueil, agenda])
};
  • riga 51: questa volta vengono eseguite quattro azioni asincrone. Verranno eseguite in parallelo;
  • righe 5–13: definizione dell'azione [navbarRun], che recupera la barra di navigazione [navbar-run];
  • riga 12: la funzione da eseguire una volta che l'azione asincrona [navbarRun] ha restituito il suo risultato;
  • righe 15–23: definizione dell'azione [jumbotron], che recupera la vista [jumbotron];
  • riga 22: la funzione da eseguire quando l'azione asincrona [jumbotron] restituisce il suo risultato;
  • righe 25–34: definizione dell'azione [home], che recupera la vista [home];
  • riga 33: la funzione da eseguire quando l'azione asincrona [home] restituisce il suo risultato;
  • righe 36–49: definizione dell'azione [agenda] che recupera la vista [jumbotron];
  • riga 48: la funzione da eseguire quando l'azione asincrona [agenda] restituisce il suo risultato;

8.6.8.9. La funzione [getHomeWithAgenda-sequence]

Si tratta della seguente funzione:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // actions [navbar-run, jumbotron, home, agenda] in order
  // agenda
  var agenda = {
    "name" : "agenda"
  };
  agenda.post = {
    "user" : ui.user,
    "lang" : ui.langue,
    "idMedecin" : ui.idMedecin,
    "jour" : ui.jourAgenda
  };
  agenda.sendMeBack = {
    'idMedecin' : ui.idMedecin,
    'jour' : ui.jourAgenda,
    "caller" : evts.getAgendaDone
  };
  // home
  var accueil = {
    "name" : "accueil"
  };
  accueil.post = {
    "lang" : ui.langue,
    "user" : ui.user
  };
  accueil.sendMeBack = {
    "caller" : evts.showResult,
    "next" : agenda
  };
  // jumbotron
  var jumbotron = {
    "name" : "jumbotron"
  };
  jumbotron.post = {
    "lang" : ui.langue
  };
  jumbotron.sendMeBack = {
    "caller" : evts.showResult,
    "next" : accueil
  };
  // navbar-run
  var navbarRun = {
    "name" : "navbar-run"
  };
  navbarRun.post = {
    "lang" : ui.langue
  };
  navbarRun.sendMeBack = {
    "caller" : evts.showResult,
    "next" : jumbotron
  };
  // execution actions in sequence
  evts.execute([ navbarRun ])
};
  • riga 54: viene eseguita l'azione [navbarRun]. Una volta terminata, si passa a quella successiva: [jumbotron], riga 51. Anche questa azione viene eseguita a sua volta. Una volta terminata, si passa a quella successiva: [home], riga 40. Anche questa viene eseguita a sua volta. Una volta terminata, si passa a quella successiva: [agenda], riga 29. Anche questa viene eseguita a sua volta. Una volta terminata, ci si ferma perché l'azione [agenda] non ha azioni successive.

8.6.8.10. Il livello [DAO]

  

Il file [dao.js] contiene tutte le funzioni del livello [DAO]. Le presenteremo gradualmente:


// URL exposed by the server
dao.urls = {
  "login": "/getLogin",
  "accueil": "/getAccueil",
  "jumbotron": "/getJumbotron",
  "agenda": "/getAgenda",
  "supprimerRv": "/supprimerRv",
  "validerRv": "/validerRv",
  "navbar-start": "/getNavbarStart",
  "navbar-run": "/getNavbarRun",
  "accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
  "accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- interface
// server url
dao.setUrlService = function (urlService) {
  dao.urlService = urlService;
};
  • righe 16–18: la funzione che imposta l'URL del servizio [Web1];
  • righe 2-13: il dizionario che associa il nome di un'azione asincrona all'URL del server [Web1] da interrogare;

// ------------------ gestion générique des actions
// exécution d'une suite d'actions asynchrones
dao.doActions = function (actions, done) {
  // traitement des actions
  dao.actionsCount = actions.length;
  dao.actionIndex = 0;
  for (var i = 0; i < dao.actionsCount; i++) {
    // requête DAO asynchrone
    var deferred = $.Deferred();
    deferred.done(dao.actionDone);
    dao.doAction(deferred, actions[i], done);
  }
};
  • riga 3: la funzione [dao.doActions] esegue una sequenza di azioni asincrone [actions]. Il parametro [done] è la funzione da eseguire una volta che tutte le azioni hanno restituito i propri risultati;
  • righe 7–12: le azioni asincrone vengono eseguite in parallelo. Tuttavia, se una di esse ha un successore, tale successore viene eseguito al termine dell'azione precedente;
  • riga 9: un oggetto [Deferred] nello stato [pending];
  • Riga 10: quando questo oggetto entra nello stato [resolved], verrà eseguita la funzione [dao.actionDone];
  • riga 11: l'azione n. i dell'elenco viene eseguita in modo asincrono. Il parametro [done] della riga 3 viene passato come argomento;

La funzione [dao.actionDone], che viene eseguita al termine di ogni azione asincrona, è la seguente:


// on a reçu un résultat
dao.actionDone = function (result) {
  // caller ?
  var sendMeBack = result.sendMeBack;
  if (sendMeBack && sendMeBack.caller) {
    sendMeBack.caller(result);
  }
  // next ?
  if (sendMeBack && sendMeBack.next) {
    // requête DAO asynchrone
    var deferred = $.Deferred();
    deferred.done(dao.actionDone);
    dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
  }
  // fini ?
  dao.actionIndex++;
  if (dao.actionIndex == dao.actionsCount) {
    // done ?
    if (sendMeBack && sendMeBack.done) {
      sendMeBack.done(result);
    }
  }
};
  • riga 2: la funzione [dao.actionDone] riceve il risultato [result] da una delle azioni asincrone presenti nell'elenco delle azioni da eseguire;
  • righe 4–7: se l'azione asincrona completata ha specificato una funzione a cui restituire il risultato, tale funzione viene chiamata;
  • righe 9–14: se l'azione asincrona completata ha un successore, allora quell'azione viene eseguita a sua volta;
  • riga 16: un'azione viene completata. Il contatore delle azioni completate viene incrementato. Un'azione che ha un numero indeterminato di azioni successive conta come una sola azione;
  • righe 19–21: se inizialmente era stata specificata una funzione [done] da eseguire una volta che tutte le azioni della sequenza avessero restituito i propri risultati, tale funzione viene ora eseguita;

Il metodo [dao.doAction] esegue un'azione asincrona:


// exécution d'une action
dao.doAction = function (deferred, action, done) {
  // fonction done à embarquer dans l'action
  if (action.sendMeBack) {
    action.sendMeBack.done = done;
  } else {
    action.sendMeBack = {
      "done": done
    };
  }
  // exécution action
  dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
  • Righe 4–10: Come abbiamo appena visto, la funzione che gestirà il risultato dell'azione asincrona da eseguire deve avere accesso alla funzione [done]. Per farlo, inseriamo la funzione [done] nell'oggetto [sendMeBack], che farà parte del risultato dell'operazione asincrona;
  • riga 12: Eseguiamo la funzione [dao.executePost], che effettua una richiesta HTTP al server [Web1]. L'URL di destinazione è l'URL associato al nome dell'azione da eseguire;

La funzione [dao.executePost] esegue una richiesta HTTP:


// requête HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
  // on fait un appel Ajax à la main
  $.ajax({
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    url: dao.urlService + url,
    type: 'POST',
    data: JSON3.stringify(post),
    dataType: 'json',
    success: function (data) {
      // on rend le résultat
      deferred.resolve({
        "status": 1,
        "data": data,
        "sendMeBack": sendMeBack
      });
    },
    error: function (jqXHR, textStatus, errorThrown) {
      var data;
      if (jqXHR.responseText) {
        data = jqXHR.responseText;
      } else {
        data = textStatus;
      }
      // on rend l'erreur
      deferred.resolve({
        "status": 2,
        "data": data,
        "sendMeBack": sendMeBack
      });
    }
  });
};

Abbiamo già incontrato e discusso questa funzione. Si noti semplicemente alla riga 9 che l'URL di destinazione è la concatenazione dell'URL del server [Web1] con l'URL associato al nome dell'azione.

8.6.8.11. La pagina di avvio

  

Image

La pagina di avvio [boot.html] mostra la schermata riportata sopra. È l'unica pagina caricata direttamente dal browser. Le altre vengono recuperate tramite chiamate Ajax. Il suo codice è il seguente:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
  <meta name="viewport" content="width=device-width"/>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title>RdvMedecins</title>
  <!-- Bootstrap core CSS -->
  <link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
  <link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
  <link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
  <link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
  <!-- Custom styles for this template -->
  <link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
  <!-- Bootstrap core JavaScript ================================================== -->
  <script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
  <script type="text/javascript" src="vendor/bootstrap.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-select.js"></script>
  <script type="text/javascript" src="vendor/moment-with-locales.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
  <script type="text/javascript" src="vendor/footable.js"></script>
  <!-- user scripts -->
  <script type="text/javascript" src="js/json3.js"></script>
  <script type="text/javascript" src="js/ui.js"></script>
  <script type="text/javascript" src="js/evts.js"></script>
  <script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
  <script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
      <div class="navbar-collapse collapse">
        <img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
        <!-- identification form -->
        <div class="navbar-form navbar-right" role="form" id="formulaire">
          <div class="form-group">
            <input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
          </div>
          <div class="form-group">
            <input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
          </div>
          <div class="form-group">
            <input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
          </div>
          <button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
          <!-- languages -->
          <div class="btn-group">
            <button type="button" class="btn btn-danger">Langue</button>
            <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
              <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
            </button>
            <ul class="dropdown-menu" role="menu">
              <li><a href="javascript:evts.setLang('fr')">Français</a></li>
              <li><a href="javascript:evts.setLang('en')">English</a></li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
<div class="container">
  <!-- Bootstrap Jumbotron -->
  <div id="jumbotron">
    <div class="jumbotron">
      <div class="row">
        <div class="col-md-2">
          <img src="images/caduceus.jpg" alt="RvMedecins"/>
        </div>
        <div class="col-md-10">
          <h1>
            Cabinet médical<br/>Les Médecins associés
          </h1>
        </div>
      </div>
    </div>
  </div>
  <!-- error panels -->
  <div id="erreur"></div>
  <div id="exception" class="alert alert-danger" style="display: none">
    <h3 id="exception-title"></h3>
    <span id="exception-text"></span>
  </div>
  <!-- content -->
  <div id="content">
    <div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
  </div>
</div>
<!-- init page -->
<script>
  // on initialise la page
  ui.langue = 'fr';
  ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
  ui.exceptionTitle['en'] = "The following server error was met:";
  ui.initNavBarStart();
</script>
</body>
</html>
  • Abbiamo già incontrato questo tipo di pagina nel capitolo su Bootstrap (sezione 8.6.4);
  • righe 99–105: inizializzazione di alcuni elementi del livello [presentazione];
  • riga 27: viene utilizzato lo script [getAccueilAvecAgenda-sequence.js]. Modificando lo script in questa riga, otteniamo tre diversi comportamenti per il recupero della pagina [accueil-avec-agenda]:
    • [getAccueilAvecAgenda-one.js] recupera la pagina con una singola richiesta HTTP,
    • [getAccueilAvecAgenda-parallel.js] recupera la pagina con quattro richieste HTTP simultanee,
    • [getAccueilAvecAgenda-sequence.js] recupera la pagina con quattro richieste HTTP successive;

8.6.8.12. Test

Esistono diversi modi per eseguire i test. In questo caso, utilizzeremo lo strumento [Webstorm]:

  • in [1] apriamo un progetto. Selezioniamo semplicemente la cartella [2] contenente la struttura delle directory statiche (HTML, CSS, JS) del sito da testare;
  • in [3], il sito statico;
  • In [4-5], carichiamo la pagina [boot.html];
  • in [5], vediamo che un server incorporato da [Webstorm] ha servito la pagina [boot.html] dalla porta [63342]. Questo è un punto importante da comprendere perché significa che gli script presenti nella pagina [boot.html] effettueranno richieste cross-domain al server [Web1], che è in esecuzione su [localhost:8081]. Il browser che ha caricato [boot.html] sa di averla caricata da [localhost:63342]. Non permetterà quindi a questa pagina di effettuare chiamate al sito [localhost:8081] poiché non si tratta della stessa porta. Applicherà quindi le regole cross-domain descritte nella sezione 8.4.14. Per questo motivo, l'applicazione [Web1] deve essere configurata per accettare queste richieste cross-domain. La configurazione avviene nel file [AppConfig] del server Spring/Thymeleaf:
 

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
 
    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // root web service / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout in milliseconds
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;
...

Lasciamo al lettore il compito di testare il client JS. Dovrebbe essere in grado di riprodurre la funzionalità descritta nella sezione 8.6.3.

Una volta convalidato il client JavaScript, è possibile distribuirlo nella cartella [Web1] del server per evitare di dover consentire le richieste cross-domain:

  

In precedenza, abbiamo copiato il sito sottoposto a test nella cartella [src/main/resources/static]. A questo punto possiamo richiedere l'URL [http://localhost:8081/boot.html]:

Image

Ora non abbiamo più bisogno di richieste cross-domain e possiamo scrivere quanto segue nel file di configurazione [AppConfig] del server [Web1]:


    // CORS
    private final boolean CORS_ALLOWED=false;

L'applicazione sopra riportata continuerà a funzionare. Se torniamo all'applicazione [WebStorm], questa non funziona più:

Image

Image

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

Image

Si tratta di un errore di richiesta cross-domain non autorizzata.

8.6.8.13. Conclusione

Abbiamo implementato la seguente architettura JS:

  • i livelli sono separati in modo abbastanza chiaro;
  • abbiamo un'applicazione a pagina singola (SPA). È questa caratteristica che ora ci consentirà di generare un'app nativa per varie piattaforme mobili (Android, iOS, Windows Phone);
  • abbiamo creato un modello in grado di eseguire azioni asincrone in parallelo, in sequenza o con una combinazione di entrambe;

8.6.9. Passaggio 6: Generazione di un'app nativa per Android

Lo strumento [Phonegap] [http://phonegap.com/] consente di produrre un eseguibile per dispositivi mobili (Android, iOS, Windows 8, ecc.) a partire da un'applicazione HTML/JS/CSS. Esistono diversi modi per ottenere questo risultato. Useremo il metodo più semplice: uno strumento online disponibile sul sito web di Phonegap [http://build.phonegap.com/apps]. Questo strumento caricherà il file ZIP del sito statico da convertire. La pagina di avvio deve essere denominata [index.html]. Quindi rinominiamo la pagina [boot.html] in [index.html]:

 

quindi comprimiamo la cartella, in questo caso [rdvmedecins-client-js-03]. Successivamente, andiamo sul sito web di Phonegap [http://build.phonegap.com/apps]:

  • Prima di [1], potrebbe essere necessario creare un account;
  • in [1], iniziamo;
  • al punto [2], scegliamo un piano gratuito che consente di creare una sola app Phonegap;
  • in [3], carichiamo l'app compressa [4];
  • in [5], assegniamo un nome all'app;
  • in [6], la compiliamo. L'operazione potrebbe richiedere 1 minuto. Attendiamo che le icone delle varie piattaforme mobili indichino che la compilazione è terminata;
  • sono stati generati solo i file binari per Android [7] e Windows [8];
  • Clicca su [7] per scaricare il file binario per Android;
  • in [9], il file binario [apk] scaricato;

Avviare un emulatore [GenyMotion] per tablet Android (vedere la sezione 9.9):

 

Sopra, avviamo un emulatore di tablet con API Android 19. Una volta avviato l'emulatore,

  • sbloccarlo trascinando il lucchetto (se presente) di lato e poi rilasciandolo;
  • Con il mouse, trascinate il file [PGBuildApp-debug.apk] che avete scaricato e rilasciatelo sull'emulatore. Verrà quindi installato ed eseguito;

È necessario modificare l'URL in [1]. Per farlo, in una finestra del prompt dei comandi, digita il comando [ipconfig] (riga 1 qui sotto), che visualizzerà i vari indirizzi IP del tuo computer:


C:\Users\Serge Tahé>ipconfig
 
Configuration IP de Windows
 
 
Carte réseau sans fil Connexion au réseau local* 15 :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :
 
Carte Ethernet Connexion au réseau local :
 
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
   Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
   Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
   Masque de sous-réseau. . . . . . . . . : 255.255.0.0
   Passerelle par défaut. . . . . . . . . : 172.19.0.254
 
Carte réseau sans fil Wi-Fi :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :
 
...

Annotare l'indirizzo IP Wi-Fi (righe 6–9) o l'indirizzo IP della rete locale (righe 11–17). Quindi utilizzare questo indirizzo IP nell'URL del server web:

Una volta fatto ciò, connettiti al servizio web:

Prova l'applicazione sull'emulatore. Dovrebbe funzionare. Sul lato server, puoi scegliere se consentire o meno le intestazioni CORS nella classe [ApplicationModel]:


    // CORS
    private final boolean CORS_ALLOWED=false;

Questo non ha importanza per l'app Android. Non viene eseguita in un browser. Il requisito relativo alle intestazioni CORS proviene dal browser, non dal server.

8.6.10. Conclusione del caso di studio

Abbiamo sviluppato la seguente architettura:

Si tratta di un'architettura complessa a 3 livelli. È stata progettata per riutilizzare il livello [Web2], che era il livello server dell'applicazione [AngularJS-Spring MVC] descritta nel documento [Tutorial AngularJS / Spring 4] all'URL [http://tahe.developpez.com/angularjs-spring4/]. Questo è l'unico motivo per cui abbiamo un'architettura a tre livelli. Mentre nell'applicazione [AngularJS-Spring MVC] il client di [Web2] era un client [AngularJS], qui il client di [Web2] è un'architettura a due livelli [jQuery] / [Spring MVC / Thymeleaf]. Abbiamo aumentato il numero di livelli, quindi perderemo un po' di prestazioni.

L'applicazione qui discussa è stata sviluppata nel corso del tempo in tre diversi documenti:

  1. [Introduzione a JSF2, PrimeFaces e PrimeFaces Mobile] all'URL [http://tahe.developpez.com/java/primefaces/]. Il caso di studio è stato poi sviluppato utilizzando i framework JSF2 / PrimeFaces. PrimeFaces è una libreria di componenti abilitati per AJAX che elimina la necessità di scrivere JavaScript. L'applicazione sviluppata in quel momento era meno complessa di quella studiata qui. Aveva una versione web classica per computer e una versione mobile per telefoni;
  2. [Tutorial AngularJS / Spring 4] all'URL [http://tahe.developpez.com/angularjs-spring4/]. L'applicazione sviluppata all'epoca aveva le stesse caratteristiche di quella discussa in questo documento. L'applicazione era stata inoltre portata su Android;
  3. questo documento;

Da questo lavoro, mi sembrano degni di nota i seguenti punti:

  • l'applicazione [Primefaces] era di gran lunga la più semplice da scrivere e la sua versione web mobile si è dimostrata altamente performante. Non richiede alcuna conoscenza di JavaScript. Non è possibile portarla in modo nativo sui sistemi operativi dei vari dispositivi mobili, ma è davvero necessario? Sembra difficile modificare lo stile dell'applicazione. Stiamo, infatti, lavorando con i fogli di stile di Primefaces. Questo potrebbe essere uno svantaggio;
  • L'applicazione [AngularJS-Spring MVC] era complessa da scrivere. Il framework [AngularJS] sembrava piuttosto difficile da comprendere una volta che si voleva padroneggiarlo. L'architettura [client Angular] / [servizio web / JSON implementato da Spring MVC] è particolarmente pulita e ad alte prestazioni. Questa architettura è replicabile per qualsiasi applicazione web. È l'architettura che mi sembra più promettente perché coinvolge diverse competenze sul lato client e sul lato server (JS+HTML+CSS sul lato client, Java o altro sul lato server), il che permette di sviluppare client e server in parallelo;
  • Per l'applicazione sviluppata in questo documento utilizzando un'architettura a 3 livelli [client jQuery] / [server Web1 / Spring MVC / Thymeleaf] / [server Web2 / Spring MVC], alcuni potrebbero trovare la tecnologia [jQuery+Spring MVC+Thymeleaf] più facile da comprendere rispetto a quella di [AngularJS]. Il livello [DAO] del client JavaScript che abbiamo scritto è riutilizzabile in altre applicazioni;