Skip to content

8. Caso di studio

8.1. Introduction

Ci proponiamo di scrivere un'applicazione web per la prenotazione di appuntamenti per uno studio medico. Questo problema è stato trattato nel documento «Tutorial AngularJS / Spring 4» alle sezioni URL e [http://tahe.developpez.com/angularjs-spring4/]. L'architettura di questa applicazione era la seguente:

  • in [1], un server web fornisce pagine statiche a un browser. Queste pagine contengono un’applicazione AngularJS basata sul modello MVC (Modello – Vista – Controller). Il modello in questo caso è sia quello delle viste sia quello del dominio, rappresentato qui dal livello [Services];
  • l’utente interagirà con le viste che gli vengono presentate nel browser. Le sue azioni richiederanno talvolta l’interrogazione del server Spring 4 [2]. Quest’ultimo elaborerà la richiesta e restituirà una risposta jSON (JavaScript Object Notation) [3]. Questa verrà utilizzata per aggiornare la vista presentata all’utente.

Ci proponiamo di riprendere questa applicazione e di implementarla dall’inizio alla fine con Spring MVC. L’architettura diventa quindi la seguente:

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

8.2. Funzionalità dell’applicazione

Il lettore è invitato a scoprire le funzionalità dell’applicazione provandola. Carichiamo in STS i progetti Maven presenti nella cartella [etude-de-cas]:

Per prima cosa creeremo il database MySQL 5 [dbrdvmedecins] con lo strumento [Wamp Server] (cfr. paragrafo 9.5):

  • in [1], si seleziona lo strumento [phpMyAdmin] da WampServer;
  • in [2], si sceglie l'opzione [Importer];
  • in [3], selezionare il file [database/dbrdvmedecins.sql];
  • in [4], lo si esegue;
  • in [5], viene creato il database.

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

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

  

server.port=8080

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

  

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

Se si accede a SGBD MySQL con altre credenziali, è qui che avviene l'operazione.

Si avvia quindi, allo stesso modo del server precedente, il server [rdvmedecins-springthymeleaf-server]:

 

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


server.port=8081

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


    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // radice servizio web / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout in millisecondi
    private final int TIMEOUT = 5000;
    // CORS
private final boolean CORS_ALLOWED=true;

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

Successivamente, tramite un browser, si richiede l'accesso a URL e [http://localhost:8081/boot.html]:

  • in [1], la pagina di accesso all'applicazione;
  • in [2] e [3], rispettivamente l'ID e la password di chi desidera utilizzare l'applicazione. Sono presenti due utenti: admin/admin (login/password) con un ruolo (ADMIN) e user/user con un ruolo (USER). Solo il ruolo ADMIN ha il diritto di utilizzare l'applicazione. Il ruolo USER serve solo a mostrare la risposta del server in questo caso d'uso;
  • in [4], il pulsante che consente di connettersi al server;
  • in [5], la lingua dell’applicazione. Ce ne sono due: il francese (predefinito) e l’inglese;
  • in [6], l'URL del server [rdvmedecins-springthymeleaf-server];
  • in [1], si effettua l'accesso;
  • una volta effettuato l'accesso, è possibile scegliere il medico con cui si desidera fissare un appuntamento [2] e il giorno dell'appuntamento [3]. Non appena vengono inseriti il medico e il giorno, viene visualizzata automaticamente l'agenda:
  • una volta visualizzato il calendario del medico, è possibile prenotare una fascia oraria [5];
  • in [6], si seleziona il paziente per l’appuntamento e si conferma la scelta in [7];

Una volta confermato l'appuntamento, si torna automaticamente all'agenda dove il nuovo appuntamento è ora registrato. Questo appuntamento potrà essere successivamente cancellato con [8].

Le funzionalità principali sono state descritte. Sono semplici. Concludiamo con la gestione della lingua:

1

Image

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

8.3. Il database

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

  

Gli appuntamenti sono gestiti dalle seguenti tabelle:

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

Le tabelle [roles], [users] e [users_roles] sono tabelle relative all’autenticazione. Per il momento non ce ne occuperemo. Le relazioni tra le tabelle che gestiscono gli appuntamenti sono le seguenti:

 
  • una fascia oraria appartiene a un medico – un medico ha 0 o più fasce orarie;
  • un appuntamento riunisce sia un cliente che un medico tramite una fascia oraria di quest’ultimo;
  • un cliente ha 0 o più appuntamenti;
  • a una fascia oraria sono associati 0 o più appuntamenti (in giorni diversi).

8.3.1. La tabella [MEDECINS]

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

  • ID: numero identificativo del medico - chiave primaria della tabella
  • VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
  • NOM: il cognome del medico
  • PRENOM: il suo nome
  • TITRE: il suo titolo (Sig.na, Sig.ra, Sig.)

8.3.2. La tabella [CLIENTS]

I pazienti dei diversi medici sono registrati nella tabella [CLIENTS]:

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

8.3.3. La tabella [CRENEAUX]

Elenca le fasce orarie in cui sono possibili i RV:

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

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

8.3.4. La tabella [RV]

Elenca i RV assegnati a ciascun medico:

  • ID: numero che identifica in modo univoco il RV – chiave primaria
  • JOUR: giorno del RV
  • ID_CRENEAU: fascia oraria del RV – chiave esterna sul campo [ID] della tabella [CRENEAUX] – determina sia la fascia oraria che il medico interessato.
  • ID_CLIENT: numero del cliente per il quale è stata effettuata la prenotazione – chiave esterna sul campo [ID] della tabella [CLIENTS]

Questa tabella presenta un vincolo di unicità sui valori delle colonne collegate (JOUR, ID_CRENEAU):

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

Se una riga della tabella [RV] presenta il valore (JOUR1, ID_CRENEAU1) per le colonne (JOUR, ID_CRENEAU), tale valore non può trovarsi in nessun altro punto. Altrimenti, ciò significherebbe che due RV sono stati registrati contemporaneamente per lo stesso medico. Dal punto di vista della programmazione Java, il driver JDBC del database avvia un SQLException quando si verifica questo caso.

La riga di id pari a 3 (cfr. [1] sopra) indica che un RV è stato prenotato per la fascia oraria n. 20 e il cliente n. 4 il 23/08/2006. La tabella [CRENEAUX] ci indica che la fascia n. 20 corrisponde alla fascia oraria 16:20 - 16:40 e appartiene al medico n. 1 (la sig.ra Marie PELISSIER). La tabella [CLIENTS] ci indica che il cliente n. 4 è la signorina Brigitte BISTROU.

8.3.5. Creazione del database

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

Utilizziamo lo strumento [PhpMyAdmin] di WampServer:

  • in [1], si seleziona lo strumento [phpMyAdmin] da WampServer;
  • in [2], si sceglie l'opzione [Importer];
  • in [3], selezionare il file [database/dbrdvmedecins.sql];
  • in [4], lo si esegue;
  • in [5], viene creato il database.

8.4. Il servizio web / jSON

Nell'architettura sopra descritta, passiamo ora alla realizzazione del servizio web / jSON, sviluppato con il framework Spring MVC. Lo scriveremo in più fasi:

  • innanzitutto i livelli [métier] e [DAO] (Data Access Object). Qui utilizzeremo Spring Data;
  • poi il servizio web jSON senza autenticazione. Qui useremo Spring MVC;
  • poi aggiungeremo la parte relativa all’autenticazione con Spring Security.

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

8.4.1. Introduzione a Spring Data

Implementeremo il livello [DAO] del progetto con Spring Data, un ramo dell’ecosistema Spring.

Sul sito di Spring sono disponibili numerosi tutorial per iniziare con Spring [http://spring.io/guides]. Ne utilizzeremo uno per introdurre Spring Data. A tal fine utilizzeremo Spring Tool Suite (STS).

  • in [1], importiamo uno dei tutorial da [spring.io/guides];
  • in [2], si sceglie il tutorial [Accessing Data Jpa] che mostra come accedere a un database con Spring Data;
  • in [3], si sceglie un progetto configurato da Maven;
  • in [4], il tutorial può essere fornito in due forme: [initial], che è una versione vuota da compilare seguendo il tutorial, oppure [complete], che è la versione finale del tutorial. Scegliamo quest'ultima;
  • in [5], è possibile scegliere di visualizzare il tutorial in un browser;
  • in [6], il progetto finale.

8.4.1.1. La configurazione Maven del progetto

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


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- utilizzare UTF-8 per tutto -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • righe 5-9: definiscono un progetto Maven padre. È questo che definisce la maggior parte delle dipendenze del progetto. Possono essere sufficienti, nel qual caso non se ne aggiungono altre, oppure no, nel qual caso si aggiungono le dipendenze mancanti;
  • righe 12-15: definiscono una dipendenza da [spring-boot-starter-data-jpa]. Questo artefatto contiene le classi di Spring Data;
  • righe 16-19: definiscono una dipendenza da SGBD e H2, che consentono di creare e gestire database in memoria.

Diamo un'occhiata alle classi fornite da queste dipendenze:

Sono davvero numerose:

  • alcune appartengono all'ecosistema Spring (quelle che iniziano con spring);
  • altre appartengono all'ecosistema Hibernate (hibernate, jboss), di cui qui si utilizza l'implementazione JPA;
  • altre sono librerie di test (junit, hamcrest);
  • altre sono librerie di log (log4j, logback, slf4j);

Le manterremo tutte. Per un'applicazione in produzione, bisognerebbe mantenere solo quelle necessarie.

Alla riga 26 del file [pom.xml] si trova la riga:


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

Questa riga è collegata alle righe seguenti:


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

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

8.4.1.2. Il livello [JPA]

L’accesso al database avviene tramite un livello [JPA], Java Persistence API:

  

L'applicazione è semplice e gestisce i clienti [Customer]. La classe [Customer] fa parte del livello [JPA] ed è la seguente:


package hello;

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

@Entity
public class Customer {

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

    protected Customer() {
    }

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

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

}

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

  • riga 8: annotazione JPA che fa sì che la persistenza delle istanze [Customer] (Create, Read, Update, Delete) venga gestita da un'implementazione JPA. In base alle dipendenze Maven, si vede che viene utilizzata l’implementazione JPA / Hibernate;
  • righe 11-12: annotazioni JPA che associano il campo [id] alla chiave primaria della tabella [Customer]. La riga 12 indica che l'implementazione JPA utilizzerà il metodo di generazione della chiave primaria specifico del SGBD utilizzato, in questo caso H2;

Non sono presenti altre annotazioni relative a JPA. Verranno quindi utilizzati i valori predefiniti:

  • la tabella di [Customer] porterà il nome della classe, ovvero [Customer];
  • le colonne di questa tabella porteranno il nome dei campi della classe: [id, firstName, lastName], tenendo presente che nel nome di una colonna della tabella non viene fatta distinzione tra maiuscole e minuscole;

Si noti che in nessun momento viene menzionata l’implementazione JPA utilizzata.

8.4.1.3. Il livello [DAO]

  

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


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

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


package org.springframework.data.repository;

import java.io.Serializable;

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);

    <S extends T> Iterable<S> save(Iterable<S> entities);

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

    Iterable<T> findAll(Iterable<ID> ids);

    long count();

    void delete(ID id);

    void delete(T entity);

    void delete(Iterable<? extends T> entities);

    void deleteAll();
}

Questa interfaccia definisce le operazioni CRUD (Create – Read – Update – Delete) che è possibile eseguire su un tipo JPA T:

  • riga 8: il metodo save consente di salvare un'entità T nel database. L'entità viene salvata con la chiave primaria assegnatale da SGBD. Consente inoltre di aggiornare un'entità T identificata dalla sua chiave primaria id. La scelta tra l'una o l'altra azione dipende dal valore della chiave primaria id: se questa è null, viene eseguita l'operazione di persistenza, altrimenti viene eseguita l'operazione di aggiornamento;
  • riga 10: lo stesso, ma per un elenco di entità;
  • riga 12: il metodo findOne consente di recuperare un'entità T identificata dalla sua chiave primaria id;
  • riga 22: il metodo delete consente di eliminare un'entità T identificata dalla sua chiave primaria id;
  • righe 24-28: varianti del metodo [delete];
  • riga 16: il metodo [findAll] consente di recuperare tutte le entità T persistenti;
  • riga 18: idem, ma limitato alle entità di cui è stato passato l’elenco degli identificatori;

Torniamo all'interfaccia [CustomerRepository]:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • la riga 9 consente di recuperare un [Customer] tramite il suo nome [lastName];

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


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

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

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

È quindi necessario che il tipo T disponga di un campo denominato [something]. Pertanto, il metodo

List<Customer> findByLastName(String lastName);

verrà implementato con un codice simile al seguente:

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

dove [em] indica il contesto di persistenza JPA. Ciò è possibile solo se la classe [Customer] dispone di un campo denominato [lastName], come effettivamente avviene.

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

8.4.1.4. Il livello [console]

  

La classe [Application] è la seguente:


package hello;

import java.util.List;

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

@Configuration
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(Application.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // salva un paio di clienti
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // recupera tutti i clienti
        Iterable<Customer> customers = repository.findAll();
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : customers) {
            System.out.println(customer);
        }
        System.out.println();

        // recupera un singolo cliente tramite ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // recupera i clienti in base al cognome
        List<Customer> bauers = repository.findByLastName("Bauer");
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : bauers) {
            System.out.println(bauer);
        }

        context.close();
    }

}
  • la riga 10: indica che la classe serve a configurare Spring. Le versioni recenti di Spring possono infatti essere configurate in Java anziché in XML. I due metodi possono essere utilizzati contemporaneamente. Nel codice di una classe con l’annotazione [Configuration] si trovano normalmente dei bean Spring, ovvero definizioni di classi da istanziare. In questo caso non è definito alcun bean. È importante ricordare che quando si lavora con un SGBD, devono essere definiti diversi bean Spring:
    • un [EntityManagerFactory] che definisce l’implementazione JPA da utilizzare,
    • un [DataSource] che definisce la fonte dati da utilizzare,
    • un [TransactionManager] che definisce il gestore delle transazioni da utilizzare;

Qui nessuno di questi bean è definito.

  • alla riga 11: l'annotazione [EnableAutoConfiguration] è un'annotazione proveniente dal progetto [Spring Boot] (righe 5-6). Questa annotazione richiede a Spring Boot, tramite la classe [SpringApplication] (riga 16), di configurare l’applicazione in base alle librerie presenti nel suo Classpath. Poiché le librerie Hibernate si trovano nel Classpath, il bean [entityManagerFactory] verrà implementato con Hibernate. Poiché la libreria SGBD si trova nel Classpath, il bean H2 verrà implementato con [dataSource]. Nel bean [dataSource], è necessario definire anche l'utente e la relativa password. In questo caso Spring Boot utilizzerà l’amministratore predefinito di H2, che non richiede password. Poiché la libreria [spring-tx] si trova nel Classpath, verrà utilizzato il gestore delle transazioni di Spring.

Inoltre, la cartella in cui si trova la classe [Application] verrà analizzata alla ricerca di bean riconosciuti implicitamente da Spring o definiti esplicitamente tramite annotazioni Spring. Verranno quindi ispezionate le classi [Customer] e [CustomerRepository]. Poiché la prima presenta l’annotazione [@Entity], verrà catalogata come entità da gestire da Hibernate. Poiché la seconda estende l’interfaccia [CrudRepository], verrà registrata come bean Spring.

Esaminiamo le righe 16-17 del codice:


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

Le operazioni che seguono si limitano a utilizzare i metodi del bean che implementa l'interfaccia [CustomerRepository]. Si noti, alla riga 50, che il contesto è chiuso. I risultati visualizzati in console sono i seguenti:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 dalla classe [LocalContainerEntityManagerFactory], una classe di Spring;
  • riga 15: compare [Hibernate]. È stata scelta proprio questa implementazione, JPA;
  • riga 19: un dialetto Hibernate è la variante SQL da utilizzare con SGBD. Qui il dialetto [H2Dialect] indica che Hibernate opererà con SGBD e H2;
  • righe 21-22: viene creato il database. Viene creata la tabella [CUSTOMER]. Ciò significa che Hibernate è stato configurato per generare le tabelle a partire dalle definizioni JPA; in questo caso, la definizione JPA della classe [Customer];
  • righe 27-31: i cinque clienti inseriti;
  • righe 33-635: risultato del metodo [findOne] dell'interfaccia;
  • righe 37-40: risultati del metodo [findByLastName];
  • righe 41 e seguenti: log relativi alla chiusura del contesto Spring.

8.4.1.5. Configurazione manuale del progetto Spring Data

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

  

In questo nuovo progetto non ci affideremo alla configurazione automatica effettuata da Spring Boot. La eseguiremo manualmente. Ciò può essere utile se le configurazioni predefinite non ci soddisfano.

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


...
    <dependencies>
        <!-- Spring Core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Transazioni Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring ORM -->        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.7.1.RELEASE</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>1.1.10.RELEASE</version>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.4.Final</version>
        </dependency>
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.178</version>
        </dependency>
        <!-- Commons DBCP -->
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
            <version>1.6</version>
        </dependency>
    </dependencies>
...

</project>
  • righe 2-18: le librerie di base di Spring;
  • righe 19-29: le librerie di Spring per la gestione delle transazioni con un database;
  • righe 30-35: la libreria di Spring per lavorare con un ORM (Object Relational Mapper);
  • righe 36-41: Spring Data utilizzato per accedere al database;
  • righe 42-47: Spring Boot per avviare l'applicazione;
  • righe 54-59: il SGBD H2;
  • righe 60-70: i database vengono spesso utilizzati con pool di connessioni aperte che evitano ripetute aperture e chiusure di connessione. In questo caso, l’implementazione utilizzata è quella di [commons-dbcp];

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


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

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

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

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


package demo.console;

import java.util.List;

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

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

public class Main {

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
...

        context.close();
    }

}
  • riga 12: la classe [Main] non ha più annotazioni di configurazione;
  • riga 16: l'applicazione viene avviata con Spring Boot. Il parametro [Config.class] è la nuova classe di configurazione del progetto;

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


package demo.config;

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

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

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

    // il provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("demo.entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Gestore delle transazioni
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • riga 22: l'annotazione [@Configuration] rende la classe [Config] una classe di configurazione Spring;
  • riga 21: l’annotazione [@EnableJpaRepositories] consente di indicare le cartelle in cui si trovano le interfacce Spring Data [CrudRepository]. Queste interfacce diventeranno componenti Spring e saranno disponibili nel relativo contesto;
  • riga 20: l'annotazione [@EnableTransactionManagement] indica che i metodi delle interfacce [CrudRepository] devono essere eseguiti all'interno di una transazione;
  • riga 19: l’annotazione [@EntityScan] consente di specificare le cartelle in cui devono essere ricercate le entità JPA. In questo caso è stata messa tra commenti, poiché tale informazione è stata fornita esplicitamente alla riga 50. Questa annotazione dovrebbe essere presente se si utilizza la modalità [@EnableAutoConfiguration] e le entità JPA non si trovano nella stessa cartella della classe di configurazione;
  • riga 18: l'annotazione [@ComponentScan] consente di elencare le cartelle in cui devono essere cercati i componenti Spring. I componenti Spring sono classi contrassegnate con annotazioni Spring quali @Service, @Component, @Controller, ... In questo caso non ce ne sono altre oltre a quelle definite all’interno della classe [Config], pertanto l’annotazione è stata commentata;
  • righe 25-33: definiscono la fonte dati, il database H2. È l’annotazione @Bean alla riga 25 che rende l’oggetto creato da questo metodo un componente gestito da Spring. Il nome del metodo può essere qualsiasi in questo caso. Tuttavia, deve essere denominato [dataSource] se il metodo EntityManagerFactory della riga 47 è assente e viene definito tramite autoconfigurazione;
  • riga 29: il database si chiamerà [demo] e verrà generato nella cartella del progetto;
  • righe 36-43: definiscono l’implementazione JPA utilizzata, in questo caso un’implementazione Hibernate. Il nome del metodo può essere qualsiasi;
  • riga 39: nessun log SQL;
  • riga 30: il database verrà creato se non esiste;
  • righe 46-54: definiscono l'EntityManagerFactory che gestirà la persistenza di JPA. Il metodo deve chiamarsi obbligatoriamente [entityManagerFactory];
  • riga 47: il metodo riceve due parametri del tipo dei due bean definiti in precedenza. Questi verranno quindi creati e poi iniettati da Spring come parametri del metodo;
  • riga 49: specifica l’implementazione JPA utilizzata;
  • riga 50: specifica le cartelle in cui si trovano le entità JPA;
  • riga 51: specifica la fonte dati da gestire;
  • righe 57-62: il gestore delle transazioni. Il metodo deve chiamarsi obbligatoriamente [transactionManager]. Riceve come parametro il bean delle righe 46-54;
  • riga 60: il gestore delle transazioni è associato a EntityManagerFactory;

I metodi precedenti possono essere definiti in qualsiasi ordine.

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

  

Infine, è possibile fare a meno di Spring Boot. Si crea una seconda classe eseguibile [Main2]:

  

La classe [Main2] presenta il seguente codice:


package demo.console;

import java.util.List;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class Main2 {

    public static void main(String[] args) {

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

        context.close();
    }

}
  • riga 15: la classe di configurazione [Config] viene ora utilizzata dalla classe Spring [AnnotationConfigApplicationContext]. Alla riga 5 si può notare che ora non ci sono più dipendenze da Spring Boot.

L'esecuzione produce gli stessi risultati di prima.

8.4.1.6. Creazione di un archivio eseguibile

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

  • in [1]: si crea una configurazione di esecuzione;
  • in [2]: di tipo [Java Application]
  • in [3]: si indica il progetto da eseguire (utilizzare il pulsante Browse);
  • in [4]: indica la classe da eseguire;
  • in [5]: il nome della configurazione di esecuzione – può essere qualsiasi;
  • in [6]: si esporta il progetto;
  • in [7]: sotto forma di archivio eseguibile JAR;
  • in [8]: indica il percorso e il nome del file eseguibile da creare;
  • in [9]: il nome della configurazione di esecuzione creata in [5];

Fatto ciò, si apre una console nella cartella contenente l'archivio eseguibile:

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

L'archivio viene eseguito nel modo seguente:


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

I risultati ottenuti 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 per ulteriori dettagli.
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. Creare un nuovo progetto Spring Data

Per creare uno scheletro di progetto Spring Data, è possibile procedere come segue:

  • in [1], si crea un nuovo progetto;
  • in [2]: di tipo [Spring Starter Project];
  • il progetto generato sarà un progetto Maven. In [3], si indica il nome del gruppo del progetto;
  • in [4]: si indica il nome dell'artefatto (in questo caso un jar) che verrà creato durante la compilazione del progetto;
  • in [5]: si indica il pacchetto della classe eseguibile che verrà creata nel progetto;
  • in [6]: il nome Eclipse del progetto – può essere qualsiasi nome (non deve necessariamente coincidere con [4]);
  • in [7]: si indica che si intende creare un progetto con un livello [JPA]. Le dipendenze necessarie per tale progetto saranno quindi incluse nel file [pom.xml];
  • in [8]: il progetto creato;

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


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath/> <!-- ricerca del genitore dal repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
  • righe 9-12: le dipendenze necessarie per JPA – includeranno [Spring Data];
  • righe 13-17: le dipendenze necessarie per i test JUnit integrati con Spring;

La classe eseguibile [Application] non esegue alcuna operazione, ma è preconfigurata:


package istia.st;

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

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {

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

La classe di test [ApplicationTests] non esegue alcuna operazione ma è preconfigurata:


package istia.st;

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

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

    @Test
    public void contextLoads() {
    }

}
  • riga 9: l'annotazione [@SpringApplicationConfiguration] consente di utilizzare il file di configurazione [Application]. La classe di test beneficerà così di tutti i bean definiti da questo file;
  • riga 8: l’annotazione [@RunWith] consente l’integrazione di Spring con JUnit: la classe potrà essere eseguita come un test JUnit. [@RunWith] è un'annotazione JUnit (riga 4), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 6);

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

8.4.2. Il progetto Eclipse del server

  

Gli elementi principali del progetto sono i seguenti:

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

8.4.3. La configurazione Maven

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


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

8.4.4. Le entità JPA

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

  

La classe [AbstractEntity] è la classe padre delle entità [Personne, Creneau, Rv]. La sua definizione è la seguente:


package rdvmedecins.entities;

import java.io.Serializable;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

@MappedSuperclass
public class AbstractEntity implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;
    @Version
    protected Long version;

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    // inizializzazione
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }

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


    // getters e setters
    ..
}
  • riga 11: l’annotazione [@MappedSuperclass] indica che la classe annotata è la classe padre delle entità JPA e [@Entity];
  • righe 15-17: definiscono la chiave primaria [id] di ciascuna entità. È l’annotazione [@Id] a rendere il campo [id] una chiave primaria. L'annotazione [@GeneratedValue(strategy = GenerationType.IDENTITY)] indica che il valore di questa chiave primaria è generato da SGBD e che viene imposta la modalità di generazione [IDENTITY]. Per il SGBD MySQL, ciò significa che le chiavi primarie saranno generate dal SGBD con l’attributo [AUTO_INCREMENT]
  • righe 18-19: definiscono la versione di ciascuna entità. L'implementazione JPA incrementerà questo numero di versione ogni volta che l'entità verrà modificata. Questo numero serve a impedire l’aggiornamento simultaneo dell’entità da parte di due utenti diversi: due utenti, U1 e U2, leggono l’entità E con un numero di versione pari a V1. U1 modifica E e salva tale modifica nel database: il numero di versione passa quindi a V1+1. U2 a sua volta modifica E e salva tale modifica nel database: riceverà un'eccezione poiché possiede una versione (V1) diversa da quella presente nel database (V1+1);
  • righe 29-33: il metodo [build] consente di inizializzare i due campi di [AbstractEntity]. Questo metodo rende il riferimento all’istanza [AbstractEntity] così inizializzato;
  • righe 36-44: il metodo [equals] della classe viene ridefinito: due entità saranno considerate uguali se hanno lo stesso nome di classe e lo stesso identificatore id;
  • righe 21-26: quando si ridefinisce il metodo [equals] di una classe, è necessario ridefinire anche il suo metodo [hashCode] (righe 21-26). La regola prevede che due entità considerate uguali dal metodo [equals] debbano quindi avere lo stesso [hashCode]. In questo caso, il [hashCode] di un'entità è uguale alla sua chiave primaria [id]. Il [hashCode] di una classe viene utilizzato in particolare nella gestione dei dizionari i cui valori sono istanze della classe;

L'entità [Personne] è la classe padre delle entità [Medecin] e [Client]:


package rdvmedecins.entities;

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

@MappedSuperclass
public class Personne extends AbstractEntity {
    private static final long serialVersionUID = 1L;
    // attributi di una persona
    @Column(length = 5)
    private String titre;
    @Column(length = 20)
    private String nom;
    @Column(length = 20)
    private String prenom;

    // costruttore predefinito
    public Personne() {
    }

    // costruttore con parametri
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }

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

    // getter e setter
    ...
}
  • riga 6: l'annotazione [@MappedSuperclass] indica che la classe annotata è la classe padre delle entità JPA e [@Entity];
  • righe 10-15: una persona ha un titolo (Melle), un nome (Jacqueline) e un cognome (Tatou). Non vengono fornite informazioni sulle colonne della tabella. Per impostazione predefinita, avranno quindi gli stessi nomi dei campi;

L'entità [Medecin] è la seguente:


package rdvmedecins.entities;

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

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

    private static final long serialVersionUID = 1L;

    // costruttore predefinito
    public Medecin() {
    }

    // costruttore con parametri
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

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

}
  • riga 6: la classe è un'entità JPA;
  • riga 7: associata alla tabella [MEDECINS] del database;
  • riga 8: l'entità [Medecin] deriva dall'entità [Personne];

Un medico potrà essere inizializzato nel modo seguente:

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

Se, inoltre, si desidera assegnargli un identificativo e una versione, si potrà 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;

    // costruttore predefinito
    public Client() {
    }

    // costruttore con parametri
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }

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

}
  • riga 6: la classe è un'entità JPA;
  • riga 7: associata alla tabella [CLIENTS] del database;
  • riga 8: l'entità [Client] deriva dall'entità [Personne];

L'entità [Creneau] è la seguente:


package rdvmedecins.entities;

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

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

    private static final long serialVersionUID = 1L;
    // caratteristiche di una fascia oraria di RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // un turno è associato a un medico
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

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

    // costruttore predefinito
    public Creneau() {
    }

    // costruttore con parametri
    public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
        this.medecin = medecin;
        this.hdebut = hdebut;
        this.mdebut = mdebut;
        this.hfin = hfin;
        this.mfin = mfin;
    }

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

    // chiave esterna
    public long getIdMedecin() {
        return idMedecin;
    }

    // setter - getter
    ...
}
  • riga 10: la classe è un'entità JPA;
  • riga 11: associata alla tabella [CRENEAUX] del database;
  • riga 12: l'entità [Creneau] deriva dall'entità [AbstractEntity] e eredita quindi l'identificativo [id] e la versione [version];
  • riga 16: ora di inizio della fascia oraria (14);
  • riga 17: minuti di inizio della fascia oraria (20);
  • riga 18: ora di fine della fascia oraria (14);
  • riga 19: minuti di fine della fascia oraria (40);
  • righe 22-24: il medico titolare della fascia oraria. La tabella [CRENEAUX] ha una chiave esterna sulla tabella [MEDECINS]. Questa relazione è rappresentata dalle righe 22-24;
  • riga 22: l’annotazione [@ManyToOne] indica una relazione da molti (fasce orarie) a uno (medico). L'attributo [fetch=FetchType.LAZY] indica che quando si richiede un'entità [Creneau] al contesto di persistenza e questa deve essere ricercata nel database, l'entità [Medecin] non viene recuperata insieme ad essa. Il vantaggio di questa modalità è che l’entità [Medecin] viene ricercata solo se lo sviluppatore lo richiede. In questo modo si risparmia memoria e si migliorano le prestazioni;
  • riga 23: indica il nome della colonna chiave esterna nella tabella [CRENEAUX];
  • righe 27-28: la chiave esterna nella tabella [MEDECINS];
  • riga 27: la colonna [ID_MEDECIN] è già stata utilizzata alla riga 23. Ciò significa che può essere modificata in due modi diversi, cosa che lo standard JPA non ammette. Si aggiungono quindi gli attributi [insertable = false, updatable = false], in modo che la colonna sia di sola lettura;

L'entità [Rv] è la seguente:


package rdvmedecins.entities;

import java.util.Date;

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

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

    // caratteristiche di un Rv
    @Temporal(TemporalType.DATE)
    private Date jour;

    // un RV è associato a un cliente
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // un RV è collegato a una fascia oraria
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

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

    // Costruttore predefinito
    public Rv() {
    }

    // con parametri
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }

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

    // chiavi esterne
    public long getIdCreneau() {
        return idCreneau;
    }

    public long getIdClient() {
        return idClient;
    }

    // getter e setter
...
}
  • riga 14: la classe è un'entità JPA;
  • riga 15: associata alla tabella [RV] del database;
  • riga 16: l'entità [Rv] deriva dall'entità [AbstractEntity] e eredita quindi l'identificativo [id] e la versione [version];
  • riga 21: la data dell'appuntamento;
  • riga 20: il tipo [Date] di Java contiene sia una data che un'ora. Qui si specifica che viene utilizzata solo la data;
  • righe 24-26: il cliente per il quale è stato fissato questo appuntamento. La tabella [RV] ha una chiave esterna sulla tabella [CLIENTS]. Questa relazione è rappresentata dalle righe 24-26;
  • righe 29-31: la fascia oraria dell’appuntamento. La tabella [RV] ha una chiave esterna sulla tabella [CRENEAUX]. Questa relazione è rappresentata dalle righe 29-31;
  • righe 34-35: la chiave esterna [idClient];
  • righe 36-37: la chiave esterna [idCreneau];

8.4.5. Il livello [DAO]

Implementeremo il livello [DAO] con Spring Data:

  

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

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

L'interfaccia [MedecinRepository] è la seguente:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Medecin;

public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
  • riga 7: l'interfaccia [MedecinRepository] si limita a ereditare i metodi dell'interfaccia [CrudRepository] senza aggiungerne altri;

L'interfaccia [ClientRepository] è la seguente:


package rdvmedecins.repositories;

import org.springframework.data.repository.CrudRepository;

import rdvmedecins.entities.Client;

public interface ClientRepository extends CrudRepository<Client, Long> {
}
  • riga 7: l'interfaccia [ClientRepository] si limita a ereditare i metodi dell'interfaccia [CrudRepository] senza aggiungerne altri;

L'interfaccia [CreneauRepository] è la seguente:


package rdvmedecins.repositories;

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

import rdvmedecins.entities.Creneau;

public interface CreneauRepository extends CrudRepository<Creneau, Long> {
    // elenco delle fasce orarie di un medico
    @Query("select c from Creneau c where c.medecin.id=?1")
    Iterable<Creneau> getAllCreneaux(long idMedecin);
}
  • riga 8: l'interfaccia [CreneauRepository] eredita i metodi dell'interfaccia [CrudRepository];
  • righe 10-11: il metodo [getAllCreneaux] consente di ottenere le fasce orarie di un medico;
  • riga 11: il parametro è l’identificativo del medico. Il risultato è un elenco di fasce orarie sotto forma di oggetto [Iterable<Creneau>];
  • riga 10: l'annotazione [@Query] consente di specificare la query JPQL (Java Persistence Query Language) che implementa il metodo. Il parametro [?1] verrà sostituito dal parametro [idMedecin] del metodo;

L'interfaccia [RvRepository] è la seguente:


package rdvmedecins.repositories;

import java.util.Date;

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

import rdvmedecins.entities.Rv;

public interface RvRepository extends CrudRepository<Rv, Long> {

    @Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
    Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
  • riga 10: l'interfaccia [RvRepository] eredita i metodi dall'interfaccia [CrudRepository];
  • righe 12-13: il metodo [getRvMedecinJour] consente di ottenere gli appuntamenti di un medico per un determinato giorno;
  • riga 13: i parametri sono l’identificativo del medico e il giorno. Il risultato è un elenco di appuntamenti sotto forma di oggetto [Iterable<Rv>];
  • riga 12: l'annotazione [@Query] consente di specificare la query JPQL che implementa il metodo. Il parametro [?1] verrà sostituito dal parametro [idMedecin] del metodo e il parametro [?2] verrà sostituito dal parametro [jour] del metodo. Non è sufficiente la seguente query JPQL:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

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

8.4.6. Il livello [métier]

  
  • [IMetier] è l'interfaccia del livello [métier] e [Metier] ne costituisce l'implementazione;
  • [AgendaMedecinJour] e [CreneauMedecinJour] sono due entità di business;

8.4.6.1. Le entità

L'entità [CreneauMedecinJour] associa una fascia oraria e l'eventuale appuntamento fissato in tale fascia:


package rdvmedecins.domain;

import java.io.Serializable;

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

public class CreneauMedecinJour implements Serializable {

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

    // costruttori
    public CreneauMedecinJour() {

    }

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

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

    // getter e setter
...
}
  • riga 12: la fascia oraria;
  • riga 13: l'eventuale appuntamento – null in caso contrario;

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


package rdvmedecins.domain;

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

import rdvmedecins.entities.Medecin;

public class AgendaMedecinJour implements Serializable {

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

    // costruttori
    public AgendaMedecinJour() {

    }

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

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

    // getter e setter
...
}
  • riga 13: il medico;
  • riga 14: il giorno nell'agenda;
  • linea 15: gli orari di apertura con o senza appuntamento;

8.4.6.2. Il servizio

L'interfaccia del livello [métier] è la seguente:


package rdvmedecins.metier;

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

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

public interface IMetier {

    // elenco dei clienti
    public List<Client> getAllClients();

    // elenco dei medici
    public List<Medecin> getAllMedecins();

    // elenco delle fasce orarie di un medico
    public List<Creneau> getAllCreneaux(long idMedecin);

    // elenco degli appuntamenti di un medico in un determinato giorno
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour);

    // trovare un cliente identificato dal suo ID
    public Client getClientById(long id);

    // trovare un cliente identificato dal suo ID
    public Medecin getMedecinById(long id);

    // trovare un appuntamento identificato dal proprio ID
    public Rv getRvById(long id);

    // trovare una fascia oraria identificata dal proprio ID
    public Creneau getCreneauById(long id);

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

    // eliminare un RV
    public void supprimerRv(Rv rv);

    // professione
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);

}

I commenti spiegano la funzione di ciascuno dei metodi.

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


package rdvmedecins.metier;

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

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

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

import com.google.common.collect.Lists;

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

    // repository
    @Autowired
    private MedecinRepository medecinRepository;
    @Autowired
    private ClientRepository clientRepository;
    @Autowired
    private CreneauRepository creneauRepository;
    @Autowired
    private RvRepository rvRepository;

    // implementazione dell'interfaccia
    @Override
    public List<Client> getAllClients() {
        return Lists.newArrayList(clientRepository.findAll());
    }

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

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

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

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

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

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

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

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

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

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

}
  • riga 24: l'annotazione [@Service] è un'annotazione Spring che rende la classe annotata un componente gestito da Spring. È possibile assegnare o meno un nome a un componente. Questo è denominato [métier];
  • riga 25: la classe [Metier] implementa l'interfaccia [IMetier];
  • riga 28: l'annotazione [@Autowired] è un'annotazione Spring. Il valore del campo così annotato verrà inizializzato (iniettato) da Spring con il riferimento a un componente Spring del tipo o del nome specificati. In questo caso l’annotazione [@Autowired] non specifica alcun nome. Verrà quindi effettuata un’iniezione per tipo;
  • riga 29: il campo [medecinRepository] verrà inizializzato con il riferimento a un componente Spring di tipo [MedecinRepository]. Si tratterà del riferimento alla classe generata da Spring Data per implementare l’interfaccia [MedecinRepository] che abbiamo già presentato;
  • righe 30-35: questo processo viene ripetuto per le altre tre interfacce esaminate;
  • righe 39-41: implementazione del metodo [getAllClients];
  • riga 40: utilizziamo il metodo [findAll] dell’interfaccia [ClientRepository]. Questo metodo restituisce un tipo [Iterable<Client>] che trasformiamo in [List<Client>] con il metodo statico [Lists.newArrayList]. La classe [Lists] è definita nella libreria Google Guava. In [pom.xml] è stata importata questa dipendenza:

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

Solo il metodo alla riga 88 è specifico del livello [métier]. È stato inserito qui perché esegue un'elaborazione di business che non si limita a un semplice accesso ai dati. Senza questo metodo, non ci sarebbe stato alcun motivo per creare un livello [métier]. Il metodo [getAgendaMedecinJour] è il seguente:


public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        // elenco delle fasce orarie del medico
        List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
        // elenco delle prenotazioni dello stesso medico per lo stesso giorno
        List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
        // si crea un dizionario a partire dagli appuntamenti fissati
        Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
        for (Rv resa : reservations) {
            hReservations.put(resa.getCreneau().getId(), resa);
        }
        // si crea l'agenda per il giorno richiesto
        AgendaMedecinJour agenda = new AgendaMedecinJour();
        // il medico
        agenda.setMedecin(getMedecinById(idMedecin));
        // il giorno
        agenda.setJour(jour);
        // le fasce orarie di prenotazione
        CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
        agenda.setCreneauxMedecinJour(creneauxMedecinJour);
        // compilazione delle fasce orarie di prenotazione
        for (int i = 0; i < creneauxHoraires.size(); i++) {
            // riga i dell'agenda
            creneauxMedecinJour[i] = new CreneauMedecinJour();
            // fascia oraria
            Creneau créneau = creneauxHoraires.get(i);
            long idCreneau = créneau.getId();
            creneauxMedecinJour[i].setCreneau(créneau);
            // la fascia oraria è libera o prenotata?
            if (hReservations.containsKey(idCreneau)) {
                // la fascia oraria è occupata - si registra la prenotazione
                Rv resa = hReservations.get(idCreneau);
                creneauxMedecinJour[i].setRv(resa);
            }
        }
        // si restituisce il risultato
        return agenda;
    }

Si invita il lettore a leggere i commenti. L’algoritmo è il seguente:

  • si recuperano tutte le fasce orarie del medico indicato;
  • si recuperano tutti i suoi appuntamenti per il giorno indicato;
  • con queste due informazioni, si è in grado di stabilire se una fascia oraria è libera o occupata;

8.4.7. La configurazione del progetto Spring

  

La classe [DomainAndPersistenceConfig] configura l’intero progetto:


package rdvmedecins.config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {

    // pacchetti delle entità JPA
    public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };

    // la fonte dei dati MySQL
    @Bean
    public DataSource dataSource() {
        // fonte dati TomcatJdbc
        DataSource dataSource = new DataSource();
        // configurazione JDBC
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        // connessioni inizialmente aperte
        dataSource.setInitialSize(5);
        // risultato
        return dataSource;
    }

    // il provider JPA è Hibernate
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }


    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(ENTITIES_PACKAGES);
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Gestore delle transazioni
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • riga 17: la classe è una classe di configurazione Spring;
  • riga 18: i pacchetti in cui si trovano le interfacce [CrudRepository] di Spring Data. Queste saranno aggiunte al contesto Spring;
  • riga 19: aggiunge al contesto Spring tutte le classi del pacchetto [rdvmedecins] e dei suoi sottoclassi che presentano un'annotazione Spring. Nel pacchetto [rdvmdecins.metier], la classe [Metier] con la sua annotazione [@Service] verrà individuata e aggiunta al contesto Spring;
  • righe 26-39: configurano il pool di connessioni Tomcat JDBC (riga 5);
  • riga 36: il pool di connessioni avrà di default 5 connessioni aperte. Questa riga è riportata a titolo di esempio. Nel nostro caso, sarebbe sufficiente una sola connessione. Nel caso in cui il livello [DAO] venisse utilizzato da più thread, questa riga sarebbe necessaria. Ciò avverrà in seguito, quando il livello [DAO] fungerà da supporto per un’applicazione web che, per sua natura, supporta più utenti serviti contemporaneamente;
  • righe 42-49: l’implementazione JPA utilizzata è un’implementazione Hibernate;
  • riga 45: nessun log SQL;
  • riga 46: nessuna rigenerazione delle tabelle;
  • riga 47: il SGBD utilizzato è MySQL;
  • righe 53-61: definiscono l'EntityManagerFactory del livello JPA. Da questo oggetto si ottiene l’oggetto [EntityManager] che consente di eseguire le operazioni JPA;
  • riga 57: si indicano i pacchetti in cui si trovano le entità JPA;
  • riga 58: indica la fonte dati da collegare al livello JPA;
  • righe 64-69: il gestore delle transazioni associato al precedente EntityManagerFactory. Per impostazione predefinita, i metodi delle interfacce [CrudRepository] di Spring Data vengono eseguiti all'interno di una transazione. La transazione viene avviata prima dell’ingresso nel metodo e viene terminata (con un commit o un rollback) dopo l’uscita dallo stesso;

8.4.8. I test del livello [métier]

  

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


package rdvmedecins.tests;

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

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

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

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

    @Autowired
    private IMetier métier;

    @Test
    public void test1(){
        // Visualizzazione clienti
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // Visualizzazione medici
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // Visualizzazione delle fasce orarie di un medico
        Medecin médecin = medecins.get(0);
        List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
        display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
        // Elenco degli appuntamenti di un medico in un determinato giorno
        Date jour = new Date();
        display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // aggiungere un RV
        Rv rv = null;
        Creneau créneau = creneaux.get(2);
        Client client = clients.get(0);
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        rv = métier.ajouterRv(jour, créneau, client);
        // verifica
        Rv rv2 = métier.getRvById(rv.getId());
        Assert.assertEquals(rv, rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // aggiungere un RV nella stessa fascia oraria dello stesso giorno
        // dovrebbe generare un'eccezione
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        Boolean erreur = false;
        try {
            rv = métier.ajouterRv(jour, créneau, client);
            System.out.println("Rv ajouté");
        } catch (Exception ex) {
            Throwable th = ex;
            while (th != null) {
                System.out.println(ex.getMessage());
                th = th.getCause();
            }
            // si registra l'errore
            erreur = true;
        }
        // si verifica che si sia verificato un errore
        Assert.assertTrue(erreur);
        // elenco dei RV
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // visualizzazione calendario
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // eliminare un RV
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // verifica
        rv2 = métier.getRvById(rv.getId());
        Assert.assertNull(rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
    }

    // metodo di utilità - visualizza gli elementi di una collezione
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }

}
  • riga 22: l'annotazione [@SpringApplicationConfiguration] consente di utilizzare il file di configurazione [DomainAndPersistenceConfig] esaminato in precedenza. La classe di test beneficia così di tutti i bean definiti da questo file;
  • riga 23: l'annotazione [@RunWith] consente l'integrazione di Spring con JUnit: la classe potrà essere eseguita come un test JUnit. [@RunWith] è un'annotazione JUnit (riga 9), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 12);
  • righe 26-27: iniezione nella classe di test di un riferimento al livello [métier];
  • molti test sono semplici test visivi:
    • righe 32-33: elenco dei clienti;
    • righe 35-36: elenco dei medici;
    • righe 39-40: elenco degli slot di un medico;
    • riga 43: elenco degli appuntamenti di un medico;
  • riga 50: aggiunta di un nuovo appuntamento. Il metodo [ajouterRv] restituisce l'appuntamento con un'informazione aggiuntiva, ovvero la sua chiave primaria id;
  • riga 53: si utilizza questa chiave primaria per cercare l'appuntamento nel database;
  • riga 54: si verifica che l'appuntamento cercato e quello trovato siano lo stesso. Si ricorda che il metodo [equals] dell’entità [Rv] è stato ridefinito: due appuntamenti sono uguali se hanno lo stesso ID. In questo caso, ciò dimostra che l’appuntamento aggiunto è stato effettivamente inserito nel database;
  • righe 61-73: si tenta di aggiungere una seconda volta lo stesso appuntamento. Ciò deve essere rifiutato dal SGBD poiché esiste un vincolo di unicità:

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

La riga 8 sopra indica che la combinazione [JOUR, ID_CRENEAU] deve essere unica, il che impedisce di inserire due appuntamenti nello stesso giorno e nella stessa fascia oraria.

  • riga 73: si verifica che si sia effettivamente verificata un'eccezione;
  • riga 77: si richiede l’agenda del medico per il quale è stato appena aggiunto un appuntamento;
  • riga 79: si verifica che l'appuntamento aggiunto sia effettivamente presente nel suo calendario;
  • riga 82: si elimina l’appuntamento aggiunto;
  • riga 84: si recupera dal database l’appuntamento eliminato;
  • riga 85: si verifica di aver recuperato un puntatore null, a dimostrazione che l'appuntamento cercato non esiste;

L'esecuzione del test ha esito positivo:

 

8.4.9. Il programma da console

  

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


package rdvmedecins.boot;

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

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

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

public class Boot {
    // avvio
    public static void main(String[] args) {
        // si prepara la configurazione
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // si avvia
        ConfigurableApplicationContext context = app.run(args);
        // attività
        IMetier métier = context.getBean(IMetier.class);
        try {
            // aggiungere un RV
            Date jour = new Date();
            System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
            Client client = (Client) new Client().build(1L, 1L);
            Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
            Rv rv = métier.ajouterRv(jour, créneau, client);
            System.out.println(String.format("Rv ajouté = %s", rv));
            // verifica
            créneau = métier.getCreneauById(1L);
            long idMedecin = créneau.getIdMedecin();
            display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
        } catch (Exception ex) {
            System.out.println("Exception : " + ex.getCause());
        }
        // chiusura del contesto Spring
        context.close();
    }

    // metodo di utilità - visualizza gli elementi di una collezione
    private static <T> void display(String message, Iterable<T> elements) {
        System.out.println(message);
        for (T element : elements) {
            System.out.println(element);
        }
    }

}

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

  • riga 19: la classe [SpringApplication] utilizzerà la classe di configurazione [DomainAndPersistenceConfig];
  • riga 20: eliminazione dei log di avvio dell’applicazione;
  • riga 22: viene eseguita la classe [SpringApplication]. Restituisce un contesto Spring, ovvero l'elenco dei bean registrati;
  • riga 24: si recupera un riferimento al bean che implementa l'interfaccia [IMetier]. Si tratta quindi di un riferimento al livello [métier];
  • righe 27-31: aggiunta di un nuovo appuntamento per oggi, per il cliente n. 1 nella fascia oraria n. 1. Il cliente e la fascia oraria sono stati creati ad hoc per dimostrare che vengono utilizzati solo gli identificativi. Qui è stata inizializzata la versione, ma si sarebbe potuto inserire qualsiasi valore. In questo caso non viene utilizzata;
  • riga 34: si vuole conoscere il medico associato alla fascia n. 1. A tal fine è necessario recuperare dalla banca dati la fascia n. 1. Poiché ci si trova in modalità [FetchType.LAZY], il medico non viene restituito insieme alla fascia. Tuttavia, abbiamo provveduto a prevedere un campo [idMedecin] nell’entità [Creneau] per recuperare la chiave primaria del medico;
  • riga 35: si recupera la chiave primaria del medico;
  • riga 36: viene visualizzato l'elenco degli appuntamenti del medico;

I risultati della console sono i seguenti:

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. È possibile definire numerosi parametri per modificare i valori predefiniti utilizzati da Spring Boot (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Ecco il suo contenuto:


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

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


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- agli encoder viene assegnato per impostazione predefinita il tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- controllo del livello dei log -->
        <root level="info"> <!-- disattivato, info, debug, avviso -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>
  • il livello generale dei log è controllato dalla riga 9 - qui i log sono di livello [info];

Ciò produce il seguente risultato:

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 si imposta il livello di log di Hibernate su [info] (senza modificare nient’altro):


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

si ottiene il seguente risultato:

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 si imposta il livello di log su [debug] (senza modificare nient'altro):


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

si ottiene il seguente risultato:


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

8.4.11. Il livello [web / jSON]

  

Costruiremo il livello [web / jSON] in diverse fasi:

  • fase 1: un livello web operativo senza autenticazione;
  • fase 2: implementazione dell’autenticazione con Spring Security;
  • fase 3: implementazione di CORS e [Cross-Origin Resource Sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]. Il client del nostro servizio web sarà un client web Angular che non apparterrà necessariamente allo stesso dominio del nostro servizio web. Per impostazione predefinita, non potrà quindi accedervi a meno che il servizio web non glielo consenta. Vedremo come;

8.4.11.1. Configurazione Maven

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


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>istia.st.spring4.mvc</groupId>
        <artifactId>rdvmedecins-webjson-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>

        <name>rdvmedecins-webjson-server</name>
        <description>Gestion de RV Médecins</description>
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
        </parent>
        <dependencies>
                <!-- livello web Spring MVC -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <!-- livello di test -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <!-- livello DAO -->
                <dependency>
                        <groupId>istia.st.spring4.rdvmedecins</groupId>
                        <artifactId>rdvmedecins-metier-dao</artifactId>
                        <version>0.0.1-SNAPSHOT</version>
                </dependency>
        </dependencies>
...
</project>
  • righe 12-15: il progetto Maven padre;
  • righe 19-22: le dipendenze per un progetto Spring MVC;
  • righe 24-28: le dipendenze per i test JUnit / Spring;
  • righe 30-34: dipendenze relative al progetto dei livelli [métier, DAO, JPA];

8.4.11.2. L'interfaccia del servizio web

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

Le risposte del nostro servizio web avranno tutte la stessa forma, corrispondente alla trasformazione jSON di un oggetto di tipo [Response] come segue:


package rdvmedecins.web.models;

import java.util.List;

public class Response<T> {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // eventuali messaggi di errore
    private List<String> messages;
    // il corpo della risposta
    private T body;

    // costruttori
    public Response() {

    }

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

    // getter e setter
    ...
}
  • riga 7: codice di errore della risposta 0: OK, altro: KO;
  • riga 11: un elenco di messaggi di errore, se presenti;
  • riga 13: il corpo della risposta;

Di seguito presentiamo le schermate che illustrano l’interfaccia del servizio web / jSON:

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

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

Elenco delle fasce orarie di un medico [/getAllCreneaux/{idMedecin}]

Elenco degli appuntamenti di un medico [/getRvMedecinJour/{idMedecin}/{aaaa-mm-jj}

Agenda di un medico [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]

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

Aggiungere un appuntamento [/ajouterRv]

  • in [0], l'URL del servizio web;
  • in [1], viene utilizzato il metodo POST;
  • in [2], il testo jSON delle informazioni trasmesse al servizio web nella forma {giorno, idClient, idCreneau};
  • in [3], il cliente specifica al servizio web che gli invia informazioni nel formato jSON;

La risposta è quindi la seguente:

  • in [4]: il client invia l'intestazione che indica che i dati che sta inviando sono in formato jSON;
  • in [5]: il servizio web risponde che anche lui invia jSON;
  • in [6]: la risposta jSON del servizio web. Il campo [body] contiene la forma jSON dell'appuntamento aggiunto;

È possibile verificare la presenza del nuovo appuntamento:

Si noti l’ID [50] dell’appuntamento. Procederemo alla sua eliminazione.

Eliminare un appuntamento [/supprimerRv]

  • in [1], l'URL del servizio web;
  • in [2], viene utilizzato il metodo POST;
  • in [3], il testo jSON delle informazioni trasmesse al servizio web nella forma {idRv};
  • in [4], il cliente specifica al servizio web che gli invia le informazioni jSON;

La risposta è quindi la seguente:

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

È possibile verificare l’eliminazione dell’appuntamento:

Come si vede sopra, l'appuntamento del paziente [Mme GERMAIN] non è più presente.

Il servizio web consente inoltre di recuperare le entità tramite il loro identificativo:

Tutte queste entità URL vengono gestite dal controller [RdvMedecinsController], che presenteremo prossimamente.

8.4.11.3. Configurazione del servizio web

  

La classe di configurazione [AppConfig] è la seguente:


package rdvmedecins.web.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import rdvmedecins.config.DomainAndPersistenceConfig;

@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {

}
  • riga 12: la classe [AppConfig] configura l’intera applicazione;
  • riga 9: la classe [AppConfig] è una classe di configurazione Spring;
  • riga 10: si richiede che i componenti Spring vengano ricercati nel pacchetto [rdvmedecins.web] e nei suoi sottocomponenti. In questo modo verranno individuati i componenti:
    • [@RestController RdvMedecinsController] nel pacchetto [rdvmedecins.web.controllers];
    • [@Component ApplicationModel] nel pacchetto [rdvmedecins.web.models];
  • riga 11: si importa la classe [DomainAndPersistenceConfig] che configura il progetto [rdvmedecins-metier-dao] per poter accedere ai bean di tale progetto;
  • riga 11: la classe [SecurityConfig] configura la sicurezza dell'applicazione web. Per ora la ignoreremo;
  • riga 11: la classe [WebConfig] configura il livello [web / jSON];

La classe [WebConfig] è la seguente:


package rdvmedecins.web.config;

import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@Configuration
@EnableWebMvc
public class WebConfig {

    // Configurazione del DispatcherServlet per le intestazioni CORS
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }

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

    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8080);
    }

    // mappatori jSON
    @Bean
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

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

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

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

}
  • righe 20-25: definiscono il bean [dispatcherServlet]. La classe [DispatcherServlet] è il servlet del framework Spring MVC. Svolge la funzione di [FrontController]: intercetta le richieste indirizzate al sito Spring MVC e le fa elaborare da uno dei controller (Controller) del sito;
  • riga 22: istanziazione della classe;
  • riga 23: questa riga può essere ignorata per il momento;
  • righe 27-30: il servlet [dispatcherServlet] gestisce tutte le richieste URL;
  • righe 27-30: attivano il server Tomcat integrato nelle dipendenze del progetto. Funzionerà sulla porta 8080;
  • righe 38-67: quattro mappatori jSON configurati con filtri jSON diversi;
  • righe 38-41: un mappatore jSON senza filtri;
  • righe 43-49: il mappatore jSON [jsonMapperShortCreneau] serializza/deserializza un oggetto [Creneau] ignorando il campo [Creneau.medecin];
  • righe 51-59: il mappatore jSON [jsonMapperLongRv] serializza/deserializza un oggetto [Rv] ignorando il campo [Rv.creneau.medecin];
  • righe 61-67: il mappatore jSON [jsonMapperShortRv] serializza / deserializza un oggetto [Rv] ignorando i campi [Rv.creneau] e [Rv.client];

8.4.11.4. La classe [ApplicationModel]

  

La classe [ApplicationModel] ci servirà per due scopi:

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

package rdvmedecins.web.models;

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

import javax.annotation.PostConstruct;

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

import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;

@Component
public class ApplicationModel implements IMetier {

    // il livello [métier]
    @Autowired
    private IMetier métier;

    // dati provenienti dal livello [métier]
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    // dati di configurazione
    private boolean CORSneeded = false;
    private boolean secured = false;
    
    @PostConstruct
    public void init() {
        // si recuperano i medici e i clienti
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }

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

    // ------------------------- interfaccia del livello [métier]
    @Override
    public List<Client> getAllClients() {
        return clients;
    }

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

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

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

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

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

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

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

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

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

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

     // getter e setter
public boolean isCORSneeded() {
        return CORSneeded;
    }

    public boolean isSecured() {
        return secured;
    }

}
  • riga 19: l'annotazione [@Component] rende la classe [ApplicationModel] un componente Spring. Come tutti i componenti Spring visti finora (ad eccezione di @Controller), verrà istanziato un solo oggetto di questo tipo (singleton);
  • riga 20: la classe [ApplicationModel] implementa l'interfaccia [IMetier];
  • righe 23-24: Spring inietta un riferimento al livello [métier];
  • riga 34: l'annotazione [@PostConstruct] fa sì che il metodo [init] venga eseguito subito dopo l'istanziazione della classe [ApplicationModel];
  • righe 38-39: si recuperano gli elenchi dei medici e dei clienti dal livello [métier];
  • riga 41: se si verifica un'eccezione, i messaggi dello stack delle eccezioni vengono memorizzati nel campo della riga 17;

L'architettura del livello web si evolve come segue:

  • in [2b], i metodi del o dei controller comunicano con il singleton [ApplicationModel];

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

8.4.11.5. La classe Static

La classe [Static] raggruppa una serie di metodi statici di utilità che non hanno alcuna attinenza con il «business» o il «web»:

  

Il suo codice è il seguente:


package rdvmedecins.web.helpers;

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

public class Static {

    public Static() {
    }

    // elenco dei messaggi di errore di un'eccezione
    public static List<String> getErreursForException(Exception exception) {
        // si recupera l'elenco dei messaggi di errore dell'eccezione
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }
}
  • riga 12: il metodo [Static.getErreursForException] che è stato utilizzato (riga 8 qui sotto) nel metodo [init] della classe [ApplicationModel]:

    @PostConstruct
    public void init() {
        // si recuperano i medici e i clienti
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
}

Il metodo crea un oggetto [List<String>] con i messaggi di errore [exception.getMessage()] di un'eccezione [exception] e di quelle in essa contenute [exception.getCause()].

8.4.11.6. Lo scheletro del controller [RdvMedecinsController]

  

Ora descriveremo in dettaglio l’elaborazione dei URL del servizio web. In questa elaborazione sono coinvolte tre classi principali:

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

Il controller [RdvMedecinsController] è il seguente:


package rdvmedecins.web.controllers;

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

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;

@Controller
public class RdvMedecinsController {

    @Autowired
    private ApplicationModel application;

    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;

    // elenco dei messaggi
    private List<String> messages;

    // mappatori jSON
    @Autowired
    private ObjectMapper jsonMapper;

    @Autowired
    private ObjectMapper jsonMapperShortCreneau;

    @Autowired
    private ObjectMapper jsonMapperLongRv;

    @Autowired
    private ObjectMapper jsonMapperShortRv;

    @PostConstruct
    public void init() {
        // messaggi di errore dell'applicazione
        messages = application.getMessages();
    }

    // elenco dei medici
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {...}

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

    // elenco degli orari di visita di un medico
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}

    // elenco degli appuntamenti di un medico
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
                    throws JsonProcessingException {...}

    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}

    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}

    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}

    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}

    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}

    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
                    throws JsonProcessingException {...}

    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {...}
}
  • riga 35: l'annotazione [@Controller] rende la classe [RdvMedecinsController] un controller Spring, il C di MVC;
  • righe 38-39: un oggetto di tipo [ApplicationModel] verrà iniettato qui da Spring. Lo abbiamo già presentato;
  • righe 41-42: qui Spring inietterà un oggetto di tipo [RdvMedecinsCorsController]. Presenteremo questo oggetto solo in seguito;
  • righe 48-58: i mappatori jSON definiti nella classe di configurazione [WebConfig];
  • riga 60: l'annotazione [@PostConstruct] contrassegna un metodo da eseguire subito dopo l'istanziazione della classe. Quando questo viene eseguito, gli oggetti iniettati da Spring sono disponibili;
  • riga 63: si recuperano eventuali messaggi di errore dall’oggetto [ApplicationModel]. Questo oggetto è stato istanziato all’avvio dell’applicazione e ha tentato di memorizzare nella cache i medici e i clienti. Se l’operazione non è andata a buon fine, si ottiene [messages!=null]. Ciò consentirà ai metodi del controller di verificare se l’applicazione si è inizializzata correttamente;
  • righe 67-118: i metodi URL esposti dal servizio [web / jSON]. Tutti i metodi restituiscono la stringa jSON di un oggetto di tipo [Response<T>] come segue:
 

package rdvmedecins.web.models;

import java.util.List;

public class Response<T> {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // eventuali messaggi di errore
    private List<String> messages;
    // il corpo della risposta
    private T body;

    // costruttori
    public Response() {

    }

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

    // getter e setter
    ...
}
  • riga 9: un codice di errore: 0 significa nessun errore;
  • riga 11: se [status!=0], allora [messages] è un elenco di messaggi di errore;
  • riga 13: un oggetto T incapsulato nella risposta. T assume il valore null in caso di errore;

Questo oggetto viene serializzato in jSON prima di essere inviato al browser del cliente;

  • riga 67: l'oggetto URL esposto è [/getAllMedecins]. Il client deve utilizzare un metodo [GET] per effettuare la richiesta (method = RequestMethod.GET). Se questo URL fosse richiesto da un POST, verrebbe rifiutato e Spring MVC invierebbe un codice di errore HTTP al client web. Il metodo stesso restituisce la risposta al client (riga 68). Si tratterà di una stringa di caratteri (riga 67). L’intestazione HTTP [Content-type : application/json; charset=UTF-8] verrà inviata al client per indicargli che riceverà una stringa jSON (riga 67);
  • riga 77: il parametro URL è impostato da {idMedecin}. Questo parametro viene recuperato con l’annotazione [@PathVariable] alla riga 79;
  • riga 79: il parametro [long idMedecin] riceve il proprio valore dal parametro {idMedecin} di URL [@PathVariable("idMedecin")]. Il parametro in URL e quello del metodo possono avere nomi diversi. Va notato che [@PathVariable("idMedecin")] è di tipo String (tutto il URL è una String), mentre il parametro [long idMedecin] è di tipo [long]. La conversione del tipo viene effettuata automaticamente. Se la conversione non va a buon fine, viene restituito un codice di errore HTTP;
  • riga 105: l’annotazione [@RequestBody] indica il corpo della richiesta. In una richiesta GET, non c'è quasi mai un corpo (ma è possibile inserirne uno). In una richiesta POST, il più delle volte c'è (ma è possibile non inserirlo). Per quanto riguarda URL e [ajouterRv], il client web invia nel proprio POST la seguente stringa jSON:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

La sintassi [@RequestBody PostAjouterRv post] (riga 105) , unita al fatto che il metodo si aspetta il jSON [consumes = "application/json; charset=UTF-8"] alla riga 103, farà sì che la stringa jSON inviata dal client web venga deserializzata in un oggetto di tipo [PostAjouterRv]. Quest’ultimo è il seguente:


package rdvmedecins.web.models;

public class PostAjouterRv {

    // dati del post
    private String jour;
    private long idClient;
    private long idCreneau;

    // getter e setter
    ...
}

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

  • nelle righe 107-109 si trova un meccanismo simile per URL e [/supprimerRv]. La stringa jSON inviata è la seguente:
{"idRv":116}

e il tipo [PostSupprimerRv] è il seguente:


package rdvmedecins.web.models;

public class PostSupprimerRv {

    // dati del post
    private long idRv;

    // getter e setter
    ...
}

8.4.11.7. L'URL [/getAllMedecins]

Il codice URL e [/getAllMedecins] viene elaborato dal metodo seguente del controller [RdvMedecinsController]:


// elenco dei medici
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {
        // la risposta
        Response<List<Medecin>> response;
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // elenco dei medici
            try {
                response = new Response<>(0, null, application.getAllMedecins());
            } catch (RuntimeException e) {
                response = new Response<>(1, Static.getErreursForException(e), null);
            }
        }
        // risposta
        return jsonMapper.writeValueAsString(response);
    }
  • righe 9-10: si verifica se l'applicazione si è inizializzata correttamente (messages==null). In caso contrario, viene restituita una risposta con status=-1 e body=messages;
  • riga 13: in caso contrario, si richiede l'elenco dei medici alla classe [ApplicationModel];
  • riga 19: si invia la stringa jSON della risposta con il mappatore jSON [jsonMapper] poiché la classe [Medecin]dispone di un filtro jSON. La risposta può essere priva di errori (riga 14) o contenere un errore (riga 16). Il metodo [application.getAllMedecins()] non genera un'eccezione poiché si limita a restituire un elenco presente nella cache. Tuttavia, manterremo questa gestione delle eccezioni nel caso in cui i medici non fossero più memorizzati nella cache;

Non abbiamo ancora illustrato il caso in cui l’applicazione non si sia inizializzata correttamente. Interrompiamo i metodi SGBD e MySQL5, avviamo il servizio web e quindi richiediamo i metodi URL e [/getAllMedecins]:

Image

Si ottiene effettivamente un errore. In condizioni normali, si ottiene la seguente visualizzazione:

8.4.11.8. L'URL [/getAllClients]

L'URL [/getAllClients] viene elaborato dal seguente metodo del controller [RdvMedecinsController]:


// elenco dei clienti
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllClients() throws JsonProcessingException {
        // la risposta
        Response<List<Client>> response;
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        }
        // elenco dei clienti
        try {
            response = new Response<>(0, null, application.getAllClients());
        } catch (RuntimeException e) {
            response = new Response<>(1, Static.getErreursForException(e), null);
        }
        // risposta
        return jsonMapper.writeValueAsString(response);
    }

È analogo al metodo [getAllMedecins] già esaminato. I risultati ottenuti sono i seguenti:

8.4.11.9. L'URL [/getAllCreneaux/{idMedecin}]

Il metodo URL [/getAllCreneaux/{idMedecin}] viene elaborato dal seguente metodo del controller [RdvMedecinsController]:


// elenco degli orari disponibili di un medico
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
        // la risposta
        Response<List<Creneau>> response;
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        }
        // si recupera il medico
        Response<Medecin> responseMedecin = getMedecin(idMedecin);
        if (responseMedecin.getStatus() != 0) {
            response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
        } else {
            Medecin médecin = responseMedecin.getBody();
            // fasce orarie del medico
            try {
                response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // risposta
        return jsonMapperShortCreneau.writeValueAsString(response);
    }
  • riga 12: il medico identificato dal parametro [id] viene richiesto a un metodo locale:

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

Da questo metodo si ritorna con un status in [0,1,2]. Torniamo al codice del metodo [getAllCreneaux]:

  • righe 13-14: se status!=0, si costruisce una risposta con errore;
  • riga 16: si recupera il medico;
  • riga 19: si recuperano gli slot di quel medico;
  • riga 25: si invia come risposta un oggetto [List<Creneau>]. Ricordiamo la definizione della classe [Creneau]:

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

    private static final long serialVersionUID = 1L;
    // caratteristiche di una fascia oraria di RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // una fascia oraria è collegata a un medico
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // chiave esterna
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • riga 13: si cerca il medico in modalità [FetchType.LAZY];

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


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

La notazione [c.medecin.id] impone il join tra le tabelle [CRENEAUX] e [MEDECINS]. Pertanto, la query restituisce tutte le fasce orarie del medico, indicando in ciascuna di esse il nome del medico. Quando si serializzano questi appuntamenti in jSON, in ciascuno di essi compare la stringa jSON relativa al medico. Ciò è superfluo. Per controllare la serializzazione, sono necessarie due cose:

  1. avere accesso all’oggetto che viene serializzato;
  2. configurare l’oggetto da serializzare;

Il punto 1 viene verificato con l'iniezione del convertitore jSON appropriato per l'oggetto nel controller:


@Autowired
private ObjectMapper jsonMapperShortCreneau;

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

  

@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
  • riga 3: un'annotazione della libreria jSON Jackson. Essa crea un filtro denominato [creneauFilter]. Con l'aiuto di questo filtro, potremo definire a livello di programmazione i campi che devono essere serializzati e quelli che non devono esserlo;

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


        // risposta
        return jsonMapperShortCreneau.writeValueAsString(response);

Il mappatore jSON [jsonMapperShortCreneau] è stato definito nella classe [WebConfig] nel modo seguente:


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

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

I risultati ottenuti sono i seguenti:

oppure questi, se la fascia oraria non esiste:

Da questo esempio ricaviamo la seguente regola:

  • i metodi del server web / jSON restituiscono un oggetto di tipo [Response<T>] che viene serializzato in jSON;
  • se il tipo T ha uno o più filtri jSON, per serializzarlo si utilizzerà un mappatore con questi stessi filtri;

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

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


// elenco degli appuntamenti di un medico
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
                    throws JsonProcessingException {
        // la risposta
        Response<List<Rv>> response=null;
        boolean erreur = false;
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // si verifica la data
        Date jourAgenda = null;
        if (!erreur) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<List<Rv>>(3, messages, null);
                erreur = true;
            }
        }
        Response<Medecin> responseMedecin = null;
        if (!erreur) {
            // si recupera il medico
            responseMedecin = getMedecin(idMedecin);
            if (responseMedecin.getStatus() != 0) {
                response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            Medecin médecin = responseMedecin.getBody();
            // elenco dei suoi appuntamenti
            try {
                response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
            } catch (RuntimeException e1) {
                response = new Response<>(4, Static.getErreursForException(e1), null);
            }
        }
        // risposta
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • è necessario convertire la stringa jSON in un tipo [Response<List<Rv>>]. La classe [Rv] presenta un campo [Rv.creneau]. Se tale campo viene serializzato, si incontrerà il filtro jSON [creneauFilter];
  • riga 47: l’oggetto di tipo [Response<List<Rv>>] della riga 7 viene serializzato in jSON;

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


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

    // caratteristiche di un appuntamento
    @Temporal(TemporalType.DATE)
    private Date jour;

    // un appuntamento è collegato a un cliente
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;

    // un appuntamento è collegato a una fascia oraria
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;

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

...

}
  • riga 11: il cliente viene ricercato con la modalità [FetchType.LAZY];
  • riga 18: la fascia oraria viene ricercata con la modalità [FetchType.LAZY];

Ricordiamo la query JPQL che recupera gli appuntamenti:


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

Vengono effettuati esplicitamente dei join per recuperare i campi [client] e [creneau]. Inoltre, a causa del join [cr.medecin.id=?1], avremo anche il medico. Il medico apparirà quindi nella stringa jSON di ogni appuntamento. Tuttavia, questa informazione duplicata è inoltre superflua. Abbiamo visto come risolvere questo problema utilizzando un filtro jSON sull’oggetto [Creneau]. A causa delle modalità [FetchType.LAZY] dei campi [client] e [creneau] della classe [Rv], scopriremo presto la necessità di applicare un filtro jSON alla classe [RV] del progetto [rdvmedecins-metier-dao]:


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

Verificheremo la serializzazione dell’oggetto [Rv] con il filtro [rvFilter]. A quanto pare, in questo caso non è necessario applicare alcun filtro poiché ci servono tutti i campi dell’oggetto di tipo [Rv]. Tuttavia, poiché abbiamo specificato che la classe dispone di un filtro jSON, dobbiamo definirlo per ogni serializzazione di un oggetto di tipo [Rv], altrimenti si verifica un'eccezione. A tal fine, utilizziamo il seguente mappatore jSON definito nella classe [rdvMedecinsController]:


    @Autowired
    private ObjectMapper jsonMapperLongRv;

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


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

I risultati ottenuti sono i seguenti:

oppure questi, con un giorno senza appuntamenti:

oppure questi con un giorno errato:

oppure questi con un medico errato:

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

L'URL [/getAgendaMedecinJour/{idMedecin}/{jour}] viene elaborato con il seguente metodo del controller [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
                    throws JsonProcessingException {
        // la risposta
        Response<AgendaMedecinJour> response = null;
        boolean erreur = false;
        // Stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // si verifica la data
        Date jourAgenda = null;
        if (!erreur) {
            // si verifica la data
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                erreur = true;
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<>(3, messages, null);
            }
        }
        // si recupera il medico
        Medecin médecin = null;
        if (!erreur) {
            // si recupera il medico
            Response<Medecin> responseMedecin = getMedecin(idMedecin);
            if (responseMedecin.getStatus() != 0) {
                response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
            } else {
                médecin = responseMedecin.getBody();
            }
        }
        // si recupera la sua agenda
        if (!erreur) {
            try {
                response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
            } catch (RuntimeException e1) {
                erreur = true;
                response = new Response<>(4, Static.getErreursForException(e1), null);
            }
        }
        // risposta
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • righe 6, 49: si restituisce la stringa jSON di tipo [AgendaMedecinJour] incapsulata in un oggetto [Response];

Il tipo [AgendaMedecinJour] è il seguente:


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

Il tipo [CreneauMedecinJour] è il seguente:


public class CreneauMedecinJour implements Serializable {

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

I campi [creneau] e [rv] dispongono di filtri jSON che devono essere configurati. È ciò che fa la riga 49 del metodo [getAgendaMedecinJour], che utilizza il mappatore jSON [jsonMapperLongRv] già incontrato:


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

I risultati ottenuti sono i seguenti:

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

oppure questi, se la data è errata:

oppure questi, se il numero del medico non è valido:

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

L'URL [/getMedecinById/{id}] viene elaborato con il seguente metodo del controllore [RdvMedecinsController]:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
        // la risposta
        Response<Medecin> response;
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<Medecin>(-1, messages, null);
        } else {
            response = getMedecin(id);
        }
        // risposta
        return jsonMapper.writeValueAsString(response);
}
  • righe 5, 13: il metodo converte la stringa jSON in un tipo [Medecin]. Questo tipo non presenta alcuna annotazione di filtro jSON. Pertanto, alla riga 14, si utilizza il mappatore jSON senza filtri;

Alla riga 10, il metodo [getMedecin] è il seguente:


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

I risultati ottenuti sono i seguenti:

oppure questi, se il numero del medico non è corretto:

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

L'URL [/getClientById/{id}] viene elaborata con il seguente metodo del controller [RdvMedecinsController]:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
        // la risposta
        Response<Client> response;
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            response = getClient(id);
        }
        // risposta
        return jsonMapper.writeValueAsString(response);
}
  • righe 5, 13: il metodo converte la stringa jSON in un tipo [Client]. Questo tipo non presenta annotazioni relative ai filtri jSON. Pertanto, alla riga 13, si utilizza il mappatore jSON senza filtri;

Alla riga 11, il metodo [getClient] è il seguente:


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

I risultati ottenuti sono i seguenti:

oppure questi, se il numero del cliente non è corretto:

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

L'URL [/getCreneauById/{id}] viene elaborata dal controller [RdvMedecinsController] con il seguente metodo:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
        // la risposta
        Response<Creneau> response;
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // si restituisce la fascia oraria
            response = getCreneau(id);
        }
        // risposta
        return jsonMapperShortCreneau.writeValueAsString(response);
}
  • righe 5, 14: il metodo restituisce la stringa jSON di tipo [Response<Creneau>];

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


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

Ricordiamo il codice dell'entità [Creneau]:


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

    private static final long serialVersionUID = 1L;
    // caratteristiche di una fascia oraria di RV
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;

    // una fascia oraria è associata a un medico
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;

    // chiave esterna
    @Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
  • righe 14-16: poiché il campo [medecin] è in modalità [fetch = FetchType.LAZY], non viene recuperato quando si cerca una fascia oraria tramite il suo [id]. È quindi necessario escluderlo dalla serializzazione. Senza questa esclusione, si verifica un'eccezione. Ciò è dovuto al fatto che l'oggetto di serializzazione [mapper] chiamerà il metodo [getMedecin] per ottenere il campo [medecin]. Tuttavia, con un’implementazione JPA / Hibernate, il metodo [fetch = FetchType.LAZY] del campo [medecin] ha restituito un oggetto [Creneau] il cui metodo [getMedecin] è programmato per recuperare il medico nel contesto JPA. Questo viene definito un oggetto [proxy]. Ricordiamo però l’architettura dell’applicazione web:

Il controller si trova nel blocco [Contrôleurs / Actions]. Quando ci si trova in questo blocco, non esiste più il concetto di contesto JPA. Quest’ultimo viene creato durante le operazioni del livello [DAO]. Non sopravvive oltre. Pertanto, quando il controller tenta di accedere al contesto JPA, si verifica un'eccezione che indica che quest'ultimo è chiuso. Per evitare questa eccezione, è necessario impedire la serializzazione del campo [medecin] della classe [Rv]. È ciò che fa il mappatore jSON [jsonMapperShortCreneau]:


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

I risultati ottenuti sono i seguenti:

oppure questi, se il numero della fascia oraria non è corretto:

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

L'URL [/getRvById/{id}] viene elaborata dal controller [RdvMedecinsController] con il seguente metodo:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
        // la risposta
        Response<Rv> response;
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // si recupera l'appuntamento
            response = getRv(id);
        }
        // risposta
        return jsonMapperShortRv.writeValueAsString(response);
}
  • righe 5, 14: il metodo restituisce la stringa jSON di tipo [Response<Rv>];

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


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

La classe [Rv] presenta due campi con l'annotazione [fetch = FetchType.LAZY], ovvero i campi [creneau] e [client]. Questi campi non vengono quindi recuperati quando si cerca un [Rv] tramite la sua chiave primaria. È quindi necessario, per gli stessi motivi di prima, escluderli dalla serializzazione. È ciò che fa il mappatore [jsonMapperShortRv] definito nella classe [WebConfig]:


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

I risultati ottenuti sono i seguenti:

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

8.4.11.16. L'URL [/ajouterRv]

L'URL [/ajouterRv] viene elaborata dal metodo seguente del controller [RdvMedecinsController]:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
        // la risposta
        Response<Rv> response = null;
        boolean erreur = false;
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // si recuperano i valori inviati
        String jour;
        long idCreneau = -1;
        long idClient = -1;
        Date jourAgenda = null;
        if (!erreur) {
            // si recuperano i valori inviati
            jour = post.getJour();
            idCreneau = post.getIdCreneau();
            idClient = post.getIdClient();
            // si controlla la data
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<>(6, messages, null);
                erreur = true;
            }
        }
        // si recupera la fascia oraria
        Response<Creneau> responseCréneau = null;
        if (!erreur) {
            // si recupera la fascia oraria
            responseCréneau = getCreneau(idCreneau);
            if (responseCréneau.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
            }
        }
        // si recupera il cliente
        Response<Client> responseClient = null;
        Creneau créneau = null;
        if (!erreur) {
            créneau = (Creneau) responseCréneau.getBody();
            // si recupera il cliente
            responseClient = getClient(idClient);
            if (responseClient.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
            }
        }
        if (!erreur) {
            Client client = responseClient.getBody();
            // si aggiunge l'appuntamento
            try {
                response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
            } catch (RuntimeException e1) {
                erreur = true;
                response = new Response<>(5, Static.getErreursForException(e1), null);
            }
        }
        // risposta
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • righe 5, 67: il metodo deve restituire la stringa jSON di tipo [Response<Rv>];
  • riga 3: l'annotazione [@RequestBody PostAjouterRv post] recupera il corpo di POST e lo inserisce nel parametro [PostAjouterRv post]. Questo corpo proviene da jSON [consumes = "application/json; charset=UTF-8"] e verrà deserializzato automaticamente nel seguente tipo [PostAjouterRv]:

public class PostAjouterRv {

    // dati del post
    private String jour;
    private long idClient;
    private long idCreneau;
...
  • poi c’è del codice che è già stato incontrato in una forma o nell’altra;
  • riga 67: l’impostazione dei filtri jSON, [creneauFilter] e [rvFilter]. Il metodo converte la stringa jSON in un tipo [Response<Rv>], dove Rv è stato ottenuto alla riga 61. L'oggetto [Rv] incapsula un oggetto [Creneau] e un oggetto [Client]. L'oggetto [Creneau] ha una dipendenza [FetchType.LAZY] da un oggetto [Medecin] ed è stato recuperato alle righe 36-44. È stato ricercato nel contesto JPA tramite la sua chiave primaria ed è stato recuperato senza la sua dipendenza [FetchType.LAZY]. Infine,
    • l'oggetto [Rv] possiede tutte le sue dipendenze. Queste possono essere serializzate;
    • l’oggetto [Creneau] non possiede la sua dipendenza [medecin]. È quindi necessario che quest’ultima non venga serializzata;

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


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

I risultati ottenuti con il client [Advanced Rest Client] sono simili a questi:

  • in [1], l’URL del POST;
  • in [2], il POST;
  • in [3], il valore pubblicato;
  • in [4a], tale valore inserito proviene da jSON;
  • in [4b], il cliente indica che sta inviando jSON;
  • in [5], il server indica che sta restituendo jSON;
  • in [6], la risposta jSON del server che rappresenta l'appuntamento aggiunto. Vi si vede l'identificativo [id] dell'appuntamento aggiunto;

Si ottiene quanto segue con un numero di fascia oraria inesistente:

8.4.11.17. L'URL [/supprimerRv]

L'URL [/supprimerRv] viene elaborato dal metodo seguente del controller [RdvMedecinsController]:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
        // la risposta
        Response<Void> response = null;
        boolean erreur = false;
        // intestazioni CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // stato dell'applicazione
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // si recuperano i valori inviati
        long idRv = post.getIdRv();
        // si recupera il rv
        if (!erreur) {
            Response<Rv> responseRv = getRv(idRv);
            if (responseRv.getStatus() != 0) {
                response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            // eliminazione del rv
            try {
                application.supprimerRv(idRv);
                response = new Response<Void>(0, null, null);
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // risposta
        return jsonMapper.writeValueAsString(response);
    }
  • riga 5: il tipo [Void] è la classe corrispondente al tipo primitivo [void];
  • righe 5, 34: il metodo restituisce la stringa jSON di tipo [Response<Void>], che non presenta filtri jSON. Pertanto, alla riga 34, si utilizza il mappatore jSON senza filtri;
  • riga 3: il metodo ha come parametro il corpo del POST, ovvero il valore inviato. Questo viene ricevuto in formato jSON [consumes = "application/json; charset=UTF-8"] e deserializzato automaticamente nel seguente tipo [PostSupprimerRv]:

public class PostSupprimerRv {

    // dati del post
    private long idRv;

  • riga 28: quando l’eliminazione va a buon fine, viene inviata una risposta con [status=0];

I risultati ottenuti sono i seguenti:

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

Con un numero di appuntamento inesistente, si ottiene quanto segue:

Abbiamo terminato con il controller. Vediamo ora come eseguire il progetto.

8.4.11.18. La classe eseguibile del servizio web

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


package rdvmedecins.web.boot;

import org.springframework.boot.SpringApplication;

import rdvmedecins.web.config.AppConfig;

public class Boot {

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

Alla riga 10, il metodo statico [SpringApplication.run] viene eseguito con come primo parametro la classe di configurazione del progetto [AppConfig]. Questo metodo provvederà all'autoconfigurazione del progetto, avvierà il server Tomcat integrato nelle dipendenze e vi distribuirà il controller [RdvMedecinsController].

I log sono gestiti dai seguenti file [2]:

[logback.xml]


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- agli encoder viene assegnato per impostazione predefinita il tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- controllo del livello dei log -->
        <root level="info"> <!-- disattivato, info, debug, avviso -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>
  • riga 9: il livello generale dei log è impostato su [info];

[application.properties]


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

Le righe 1-2 consentono di impostare un livello di log specifico per alcuni elementi dell'applicazione:

  • riga 1: si desiderano i log del livello [web];
  • riga 2: non si desiderano i log del livello [JPA];
  • riga 3: nessun banner Spring Boot;

I log durante l'esecuzione sono i seguenti:


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

11:06:04.732 [main] INFO  rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 420 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:06:04.775 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:05.538 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:06:05.688 [main] INFO  o.a.catalina.core.StandardService - Starting service Tomcat
11:06:05.689 [main] INFO  o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:06:05.833 [localhost-startStop-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:06:05.833 [localhost-startStop-1] INFO  o.s.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1061 ms
11:06:06.231 [localhost-startStop-1] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:06:09.234 [localhost-startStop-1] INFO  o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@12d14fa, org.springframework.security.web.context.SecurityContextPersistenceFilter@29823fb6, org.springframework.security.web.header.HeaderWriterFilter@662d93b2, org.springframework.security.web.authentication.logout.LogoutFilter@2d81ee0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52aa47ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@60bd7a74, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a374232, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ddb4452, org.springframework.security.web.session.SessionManagementFilter@2cd9855f, org.springframework.security.web.access.ExceptionTranslationFilter@2263f0a2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@192ce7f6]
11:06:09.255 [localhost-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:06:09.255 [localhost-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Medecin> rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Client> rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Client>> rdvmedecins.web.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Medecin>> rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
11:06:09.677 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:09.770 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:06:09.786 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:06:09.802 [main] INFO  o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:06:09.817 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:06:09.817 [main] INFO  rdvmedecins.web.boot.Boot - Started Boot in 5.319 seconds (JVM running for 6.053)
  • riga 18: il server Tomcat è attivo;
  • riga 21: il contesto Spring è in fase di inizializzazione;
  • righe 27-38: vengono individuati i URL esposti dal servizio web;
  • riga 44: il server Tomcat è pronto e attende richieste sulla porta 8080;

Se si modifica il file [application.properties] nel modo seguente:


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

si ottengono i seguenti log:

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)

Se inoltre si modifica il file [logback.xml] come segue:


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- agli encoder viene assegnato per impostazione predefinita il tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- controllo del livello dei log -->
        <root level="off"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>

si ottengono i seguenti log:

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

Si nota quindi che è possibile esercitare un certo controllo sui log che compaiono nella console. Il livello [info] è spesso il livello di log corretto.

Ora disponiamo di un servizio web operativo consultabile tramite un client web. Passiamo ora alla messa in sicurezza di questo servizio: vogliamo che solo determinate persone possano gestire gli appuntamenti dei medici. A tal fine utilizzeremo il framework Spring Security, un ramo dell’ecosistema Spring.

8.4.12. Introduzione a Spring Security

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

  

Il progetto è composto dai seguenti elementi:

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

8.4.12.1. Configurazione Maven

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


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-securing-web</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- tag::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- end::security[] -->
    </dependencies>

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

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

</project>
  • righe 10-14: il progetto è un progetto Spring Boot;
  • righe 17-20: dipendenza dal framework [Thymeleaf];
  • righe 22-25: dipendenza dal framework Spring Security;

8.4.12.2. Le viste Thymeleaf

  

La vista [home.html] è la seguente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>

    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • riga 12: l'attributo [th:href="@{/hello}"] genererà l'attributo [href] del tag <a>. Il valore [@{/hello}] genererà il percorso [<context>/hello], dove [context] è il contesto dell'applicazione web;

Il codice HTML generato è il seguente:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>

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

La vista [hello.html] è la seguente:

  

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

Il codice HTML generato è il seguente:


<!DOCTYPE html>

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

L'ultima vista [login.html] è la seguente:

  

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

Il codice HTML generato è il seguente:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>

        <div>
            You have been logged out.
        </div>
        <form method="post" action="/login">
            <div>
                <label>
                    User Name :
                    <input type="text" name="username" />
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" name="password" />
                </label>
            </div>
            <div>
                <input type="submit" value="Sign In" />
            </div>
            <input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
        </form>
    </body>
</html>

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

8.4.12.3. Configurazione Spring MVC

  

La classe [MvcConfig] configura il framework Spring MVC:


package hello;

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

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

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

}
  • riga 7: l'annotazione [@Configuration] rende la classe [MvcConfig] una classe di configurazione;
  • riga 8: la classe [MvcConfig] estende la classe [WebMvcConfigurerAdapter] per ridefinirne alcuni metodi;
  • riga 10: ridefinizione di un metodo della classe padre;
  • righe 11-16: il metodo [addViewControllers] consente di associare URL a viste HTML. Vengono effettuate le seguenti associazioni:
URL
vista
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

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

Sopra [1], le cartelle [java] e [resources] sono entrambe cartelle sorgente (source folders). Ciò implica che il loro contenuto si troverà nella radice del Classpath del progetto. Pertanto, in [2], le cartelle [hello] e [templates] si troveranno alla radice del Classpath.

8.4.12.4. Configurazione di Spring Security

  

La classe [WebSecurityConfig] configura il framework Spring Security:


package hello;

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

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • riga 9: l'annotazione [@Configuration] rende la classe [WebSecurityConfig] una classe di configurazione;
  • riga 10: l'annotazione [@EnableWebSecurity] trasforma la classe [WebSecurityConfig] in una classe di configurazione di Spring Security;
  • riga 11: la classe [WebSecurity] estende la classe [WebSecurityConfigurerAdapter] per ridefinirne alcuni metodi;
  • riga 12: ridefinizione di un metodo della classe padre;
  • righe 13-16: il metodo [configure(HttpSecurity http)] viene ridefinito per definire i diritti di accesso alle diverse classi URL dell’applicazione;
  • riga 14: il metodo [http.authorizeRequests()] consente di associare i URL ai diritti di accesso. Vengono effettuate le seguenti associazioni:
URL
regola
codice
/, /home
accesso senza autenticazione

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
accesso solo con autenticazione
http.anyRequest().authenticated();
  • riga 15: definisce il metodo di autenticazione. L'autenticazione avviene tramite un modulo accessibile a tutti: URL [/login] [http.formLogin().loginPage("/login").permitAll()]. Anche la disconnessione (logout) è accessibile a tutti;
  • righe 19-21: ridefiniscono il metodo [configure(AuthenticationManagerBuilder auth)] che gestisce gli utenti;
  • riga 20: l’autenticazione avviene tramite utenti predefiniti [auth.inMemoryAuthentication()]. Un utente è qui definito con il login [user], la password [password] e il ruolo [USER]. È possibile assegnare gli stessi diritti agli utenti che hanno lo stesso ruolo;

8.4.12.5. Classe eseguibile

  

La classe [Application] è la seguente:


package hello;

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

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

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

}
  • riga 8: l'annotazione [@EnableAutoConfiguration] richiede a Spring Boot (riga 3) di effettuare la configurazione che lo sviluppatore non avrà effettuato esplicitamente;
  • riga 9: rende la classe [Application] una classe di configurazione Spring;
  • riga 10: richiede la scansione della cartella della classe [Application] per individuare i componenti Spring. Le due classi [MvcConfig] e [WebSecurityConfig] verranno così individuate poiché presentano l'annotazione [@Configuration];
  • riga 13: il metodo [main] della classe eseguibile;
  • riga 14: il metodo statico [SpringApplication.run] viene eseguito con come parametro la classe di configurazione [Application]. Abbiamo già incontrato questo processo e sappiamo che il server Tomcat incluso nelle dipendenze Maven del progetto verrà avviato e il progetto verrà distribuito su di esso. Abbiamo visto che quattro URL erano gestiti da [/, /home, /login, /hello] e che alcuni erano protetti da diritti di accesso.

8.4.12.6. Test dell’applicazione

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

 

La URL richiesta, [/], è accessibile a tutti. Ecco perché l’abbiamo ottenuta. Il link [here] è il seguente:

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

Il file URL [/hello] verrà richiesto quando si cliccherà sul link. Questo è protetto:

URL
regola
codice
/, /home
accesso senza autenticazione

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
accesso riservato agli utenti autenticati
http.anyRequest().authenticated();

È necessario effettuare l'autenticazione per ottenerlo. Spring Security reindirizzerà quindi il browser del cliente alla pagina di autenticazione. In base alla configurazione visualizzata, si tratta della pagina URL [/login]. Questa è accessibile a tutti:


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

Otteniamo quindi [1]:

Il codice sorgente della pagina ottenuta è il seguente:

<!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, denominato CSRF (Cross Site Request Forgery), ha lo scopo di eliminare una vulnerabilità di sicurezza. Questo token deve essere rinviato a Spring Security insieme all’autenticazione affinché quest’ultima venga accettata;

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


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

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

  • in [4], effettuiamo l’autenticazione;
  • in [5], Spring Security ci reindirizza verso URL [/hello] poiché è URL che avevamo richiesto quando siamo stati reindirizzati alla pagina di login. L’identità dell’utente è stata visualizzata dalla seguente riga di [hello.html]:
    <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 [Sign Out], verrà eseguito un POST su URL [/logout]. Quest’ultimo, così come il URL e il [/login], è accessibile a tutti:


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

Nella nostra associazione URL / viste, non abbiamo definito nulla per URL e [/logout]. Cosa succederà? Proviamo:

  • in [6], clicchiamo sul pulsante [Sign Out];
  • da [7], vediamo che siamo stati reindirizzati a URL [http://localhost:8080/login?logout]. È stato Spring Security a richiedere questo reindirizzamento. La presenza del parametro [logout] in URL ha fatto visualizzare la seguente riga nella vista:

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

8.4.12.7. Conclusion

Nell'esempio precedente, avremmo potuto scrivere prima l'applicazione web e poi renderla sicura. Spring Security non è invasivo. È possibile implementare la sicurezza di un'applicazione web già scritta. Inoltre, abbiamo scoperto quanto segue:

  • è possibile definire una pagina di autenticazione;
  • l'autenticazione deve essere accompagnata dal token CSRF emesso da Spring Security;
  • se l’autenticazione fallisce, si viene reindirizzati alla pagina di autenticazione con l’aggiunta di un parametro «error» nel token URL;
  • se l’autenticazione va a buon fine, si viene reindirizzati alla pagina richiesta al momento dell’autenticazione. Se si richiede direttamente la pagina di autenticazione senza passare da una pagina intermedia, Spring Security reindirizza alla pagina URL [/] (questo caso non è stato presentato);
  • ci si disconnette richiedendo la pagina URL [/logout] con un POST. Spring Security ci reindirizza quindi alla pagina di autenticazione con il parametro logout nel URL;

Tutte queste conclusioni si basano sui comportamenti predefiniti di Spring Security. Tali comportamenti possono essere modificati tramite configurazione, ridefinendo alcuni metodi della classe [WebSecurityConfigurerAdapter].

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

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

Esistono pochi tutorial relativi a ciò che vogliamo realizzare in questa sede. La soluzione che verrà proposta è un insieme di codici trovati qua e là.

8.4.13. Implementazione della sicurezza sul servizio web di prenotazione appuntamenti

8.4.13.1. Il database

Il database [rdvmedecins] viene aggiornato per includere gli utenti, le loro password e i loro ruoli. Vengono aggiunte tre nuove tabelle:

Image

Tabella [USERS]: gli utenti

  • ID: chiave primaria;
  • VERSION: colonna di versioning della riga;
  • IDENTITY: un'identità descrittiva dell'utente;
  • LOGIN: il nome utente;
  • PASSWORD: la sua password;

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

 

L'algoritmo che crittografa le password è l'algoritmo BCRYPT.

Tabella [ROLES]: i ruoli

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

Tabella [USERS_ROLES]: tabella di join USERS / ROLES

Un utente può avere più ruoli, un ruolo può raggruppare più utenti. Si ha una relazione molti-a-molti rappresentata dalla tabella [USERS_ROLES].

  • ID: chiave primaria;
  • VERSION: colonna di versioning della riga;
  • USER_ID: identificativo di un utente;
  • ROLE_ID: identificativo di un ruolo;
 

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

8.4.13.2. Il nuovo progetto STS derivato da [métier, DAO, JPA]

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

  • in [1]: il nuovo progetto;
  • in [2]: le modifiche apportate per motivi di sicurezza sono state raggruppate in un unico pacchetto [rdvmedecins.security]. Questi nuovi elementi appartengono ai livelli [JPA] e [DAO], ma per semplicità sono stati raggruppati in un unico pacchetto.

8.4.13.3. Le nuove entità [JPA]

Il livello JPA definisce tre nuove entità:

  

La classe [User] è l'immagine della tabella [USERS]:


package rdvmedecins.entities;

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

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

    // caratteristiche
    private String identity;
    private String login;
    private String password;

    // costruttore
    public User() {
    }

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

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

    // getter e setter
....
}
  • riga 9: la classe estende la classe [AbstractEntity] già utilizzata per le altre entità;
  • righe 13-15: non viene specificato alcun nome per le colonne poiché hanno lo stesso nome dei campi ad esse associati;

La classe [Role] è il riflesso della tabella [ROLES]:


package rdvmedecins.entities;

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

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

    private static final long serialVersionUID = 1L;

    // proprietà
    private String name;

    // costruttori
    public Role() {
    }

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

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

    // getter e setter
...
}

La classe [UserRole] è l'immagine della tabella [USERS_ROLES]:


package rdvmedecins.entities;

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

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

    private static final long serialVersionUID = 1L;

    // un UserRole fa riferimento a un User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // un UserRole fa riferimento a un ruolo
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // getter e setter
...
}
  • righe 15-17: definiscono la chiave esterna dalla tabella [USERS_ROLES] alla tabella [USERS];
  • righe 19-21: definiscono la chiave esterna della tabella [USERS_ROLES] verso la tabella [ROLES];

8.4.13.4. Modifiche al livello [DAO]

Il livello [DAO] viene arricchito con tre nuovi [Repository]:

  

L'interfaccia [UserRepository] gestisce gli accessi alle entità [User]:


package rdvmedecins.repositories;

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

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

public interface UserRepository extends CrudRepository<User, Long> {

    // elenco dei ruoli di un utente identificato dal proprio ID
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // elenco dei ruoli di un utente identificato tramite login e password
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // ricerca di un utente tramite il suo nome utente
    User findUserByLogin(String login);
}
  • riga 9: l'interfaccia [UserRepository] estende l'interfaccia [CrudRepository] di Spring Data (riga 4);
  • righe 12-13: il metodo [getRoles(User user)] consente di ottenere tutti i ruoli di un utente identificato tramite il suo [id]
  • righe 16-17: lo stesso, ma per un utente identificato tramite login e password;
  • riga 20: per trovare un utente tramite il suo nome utente;

L'interfaccia [RoleRepository] gestisce gli accessi alle entità [Role]:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // ricerca di un ruolo tramite il suo nome
    Role findRoleByName(String name);

}
  • riga 5: l'interfaccia [RoleRepository] estende l'interfaccia [CrudRepository];
  • riga 8: è possibile cercare un ruolo tramite il suo nome;

L'interfaccia [userRoleRepository] gestisce gli accessi alle entità [UserRole]:


package rdvmedecins.security;

import org.springframework.data.repository.CrudRepository;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • riga 5: l'interfaccia [UserRoleRepository] si limita a estendere l'interfaccia [CrudRepository] senza aggiungere nuovi metodi;

8.4.13.5. Le classi di gestione degli utenti e dei ruoli

  

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

 

Tale interfaccia è qui implementata dalla classe [AppUserDetails]:


package rdvmedecins.security;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // proprietà
    private User user;
    private UserRepository userRepository;

    // costruttori
    public AppUserDetails() {
    }

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

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

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

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

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

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

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

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

    // getter e setter
    ...
}
  • riga 10: la classe [AppUserDetails] implementa l'interfaccia [UserDetails];
  • righe 15-16: la classe incapsula un utente (riga 15) e il repository che consente di ottenere i dettagli di tale utente (riga 16);
  • righe 22-25: il costruttore che istanzia la classe con un utente e il relativo repository;
  • righe 28-35: implementazione del metodo [getAuthorities] dell’interfaccia [UserDetails]. Deve costruire una collezione di elementi di tipo [GrantedAuthority] o derivati. Qui utilizziamo il tipo derivato [SimpleGrantedAuthority] (riga 32) che incapsula il nome di uno dei ruoli dell’utente della riga 15;
  • righe 31-33: si percorre l’elenco dei ruoli dell’utente della riga 15 per costruire un elenco di elementi di tipo [SimpleGrantedAuthority];
  • righe 38-40: implementano il metodo [getPassword] dell’interfaccia [UserDetails]. Viene restituita la password dell’utente della riga 15;
  • righe 38-40: implementano il metodo [getUserName] dell'interfaccia [UserDetails]. Viene restituito il nome utente della riga 15;
  • righe 47-50: l'account dell'utente non scade mai;
  • righe 52-55: l'account dell'utente non viene mai bloccato;
  • righe 57-60: le credenziali dell'utente non scadono mai;
  • righe 62-65: l'account dell'utente è sempre attivo;

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

 

Questa interfaccia è implementata dalla seguente classe [AppUserDetailsService]:


package rdvmedecins.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // ricerca dell'utente tramite login
        User user = userRepository.findUserByLogin(login);
        // Trovato?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // vengono restituiti i dettagli dell'utente
        return new AppUserDetails(user, userRepository);
    }

}
  • riga 9: la classe sarà un componente Spring, quindi disponibile nel proprio contesto;
  • righe 12-13: il componente [UserRepository] verrà iniettato qui;
  • righe 16-25: implementazione del metodo [loadUserByUsername] dell'interfaccia [UserDetailsService] (riga 10). Il parametro è il nome utente;
  • riga 18: si cerca l'utente in base al suo nome utente;
  • righe 20-22: se non viene trovato, viene generata un'eccezione;
  • riga 24: viene creato e visualizzato un oggetto [AppUserDetails]. È effettivamente di tipo [UserDetails] (riga 16);

8.4.13.6. Test del livello [DAO]

  

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


package rdvmedecins.security;

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

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

public class CreateUser {

    public static void main(String[] args) {
        // sintassi: login password roleName

        // sono necessari tre parametri
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // si recuperano i parametri
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // contesto Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // il ruolo esiste già?
        Role role = roleRepository.findRoleByName(roleName);
        // se non esiste, lo si crea
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // L'utente esiste già?
        User user = userRepository.findUserByLogin(login);
        // se non esiste, lo creiamo
        if (user == null) {
            // si esegue l'hash della password con bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // si salva l'utente
            user = userRepository.save(new User(login, login, crypt));
            // si crea il collegamento con il ruolo
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // l'utente esiste già: possiede il ruolo richiesto?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // se non viene trovato, si crea la relazione con il ruolo
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // chiusura del contesto Spring
        context.close();
    }

}
  • riga 17: la classe richiede tre argomenti che definiscono un utente: il suo login, la sua password e il suo ruolo;
  • righe 25-27: i tre parametri vengono recuperati;
  • riga 29: il contesto Spring viene costruito a partire dalla classe di configurazione [DomainAndPersistenceConfig]. Questa classe era già presente nel progetto iniziale. Deve essere modificata come segue:

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

Torniamo al codice per la creazione di un utente:

  • righe 30-32: si recuperano i riferimenti dei tre [Repository] che possono esserci utili per creare l'utente;
  • riga 34: si verifica se il ruolo esiste già;
  • righe 36-38: se non esiste, lo si crea nel database. Avrà un nome del tipo [ROLE_XX];
  • riga 40: si verifica se il login esiste già;
  • righe 42-49: se il nome utente non esiste, lo creiamo nel database;
  • riga 44: si crittografa la password. Qui si utilizza la classe [BCrypt] di Spring Security (riga 4). Sono quindi necessari i file di questo framework. Il file [pom.xml] include una nuova dipendenza:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • riga 46: l'utente viene salvato nel database;
  • riga 48: così come la relazione che lo collega al suo ruolo;
  • righe 51-57: nel caso in cui l’account esista già, si verifica se tra i suoi ruoli è già presente quello che si desidera assegnargli;
  • righe 59-61: se il ruolo cercato non è stato trovato, si crea una riga nella tabella [USERS_ROLES] per collegare l'utente al suo ruolo;
  • non ci si è protetti da eventuali eccezioni. Si tratta di una classe di supporto per creare rapidamente un utente con un ruolo.

Quando si esegue la classe con gli argomenti [x x guest], si ottengono in linea di massima i seguenti risultati:

Tabella [USERS]

Tabella [ROLES]

 

Tabella [USERS_ROLES]

 

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

  

package rdvmedecins.security;

import java.util.List;

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

import rdvmedecins.config.DomainAndPersistenceConfig;

import com.google.common.collect.Lists;

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

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

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

    @Test
    public void findUserByLogin() {
        // si recupera l'utente [admin]
        User user = userRepository.findUserByLogin("admin");
        // si verifica che la sua password sia [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // si verifica il ruolo di admin / admin
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        // si recupera l'utente [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // si verifica che la sua password sia [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // si verifica il ruolo di admin / admin
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // metodo di utilità - visualizza gli elementi di una collezione
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • righe 27-34: test visivo. Vengono visualizzati tutti gli utenti con i rispettivi ruoli;
  • righe 36-46: si verifica che l’utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando il repository [UserRepository];
  • riga 41: [admin] è la password in chiaro. Nella base dati, è crittografata secondo l’algoritmo BCrypt. Il metodo [BCrypt.checkpw] consente di verificare che la password in chiaro, una volta crittografata, sia effettivamente uguale a quella presente nella base dati;
  • righe 48-59: si verifica che l'utente [admin] abbia la password [admin] e il ruolo [ROLE_ADMIN] utilizzando il servizio [appUserDetailsService];

L'esecuzione dei test ha esito positivo con i seguenti log:

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 intermedia

L'aggiunta delle classi necessarie a Spring Security è stata possibile con poche modifiche al progetto originale. Ricordiamole:

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

Questo caso particolarmente favorevole deriva dal fatto che le tre tabelle aggiunte al database sono indipendenti da quelle esistenti. Si sarebbe potuto persino inserirle in un database separato. Ciò è stato possibile perché si è deciso che un utente avesse un’esistenza indipendente dai medici e dai clienti. Se questi ultimi fossero stati utenti potenziali, sarebbe stato necessario creare collegamenti tra la tabella [USERS] e le tabelle [MEDECINS] e [CLIENTS]. Ciò avrebbe quindi avuto un impatto significativo sul progetto esistente.

8.4.13.8. Il progetto STS del livello [web]

Il progetto [rdvmedecins-webjson] si evolve come segue da [1]:

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


package hello;

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

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

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

Seguiremo la stessa procedura:

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

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


package rdvmedecins.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;
    @Autowired
    private ApplicationModel application;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // l'autenticazione viene effettuata dal bean [appUserDetailsService]
        // la password è crittografata tramite l'algoritmo di hash BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // applicazione sicura?
        if (application.isSecured()) {
            // la password viene trasmessa tramite l'intestazione Authorization: Basic xxxx
            http.httpBasic();
            // il metodo HTTP OPTIONS deve essere autorizzato per tutti
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // solo il ruolo ADMIN può utilizzare l'applicazione
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // tutte le URL
                    .hasRole("ADMIN");
            // nessuna sessione
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • riga 15: la classe [SecurityConfig] è una classe di configurazione Spring;
  • riga 16: per configurare la sicurezza del progetto;
  • righe 19-20: viene iniettata la classe [AppUserDetails] che consente l'accesso agli utenti dell'applicazione;
  • righe 21-22: viene iniettata la classe [ApplicationModel], che funge da cache per l'applicazione web. Si decide qui di utilizzarla anche per configurare l’applicazione web in un unico punto. È questa classe che definisce il valore booleano [isSecured] della riga 36. Questo valore booleano protegge (true) o meno (false) l’applicazione web;
  • righe 25-29: il metodo [configure(HttpSecurity http)] definisce gli utenti e i loro ruoli. Riceve come parametro un tipo [AuthenticationManagerBuilder]. Questo parametro viene arricchito con due informazioni (riga 28):
    • un riferimento al servizio [appUserDetailsService] della riga 20 che consente l’accesso agli utenti registrati. Si noti qui che il fatto che siano registrati in un database non risulta esplicito. Potrebbero quindi trovarsi in una cache, essere forniti da un servizio web, ...
    • il tipo di crittografia utilizzato per la password. Ricordiamo che abbiamo utilizzato l’algoritmo BCrypt;
  • righe 38-47: il metodo [configure(HttpSecurity http)] definisce i diritti di accesso ai URL del servizio web;
  • riga 34: abbiamo visto nel progetto introduttivo che, per impostazione predefinita, Spring Security gestiva un token CSRF (Cross Site Request Forgery) che l’utente che voleva autenticarsi doveva rinviare al server. Qui questo meccanismo è disattivato. Ciò, insieme al valore booleano (isSecured=false), consente di utilizzare l’applicazione web senza sicurezza;
  • riga 38: si attiva la modalità di autenticazione tramite l'intestazione HTTP. Il client dovrà inviare la seguente intestazione HTTP:
Authorization:Basic code

dove «code» è la codifica della stringa «login:password» tramite l’algoritmo Base64. Ad esempio, la codifica Base64 della stringa admin:admin è YWRtaW46YWRtaW4=. Pertanto, l'utente con nome utente [admin] e password [admin] invierà la seguente intestazione HTTP per autenticarsi:

Authorization:Basic YWRtaW46YWRtaW4=
  • righe 40-42: indicano che tutti i URL del servizio web sono accessibili agli utenti con il ruolo [ROLE_ADMIN]. Ciò significa che un utente che non possiede tale ruolo non può accedere al servizio web;
  • riga 47: la password dell’utente può essere memorizzata o meno in una sessione. Se viene memorizzata, l’utente deve autenticarsi solo la prima volta. Nelle volte successive, non gli verranno richiesti i dati di accesso. In questo caso, è stata scelta una modalità senza sessione. Ogni richiesta dovrà essere accompagnata dai dati di sicurezza;

La classe [AppConfig], che configura l’intera applicazione, viene modificata come segue:

  

package rdvmedecins.web.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import rdvmedecins.config.DomainAndPersistenceConfig;

@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {

}
  • la modifica avviene alla riga 11: si aggiunge la classe di configurazione [SecurityConfig];

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


@Component
public class ApplicationModel implements IMetier {

...
    // dati di configurazione
    private boolean secured = false;
    
    public boolean isSecured() {
        return secured;
}
  • riga 6: si imposta il valore booleano [secured] su [true / false] a seconda che si desideri o meno attivare la protezione.

8.4.13.9. Test del servizio web

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

Authorization:Basic code

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

  

package rdvmedecins.helpers;

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

public class Base64Encoder {

    public static void main(String[] args) {
        // sono richiesti due argomenti: login e password
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // si recuperano i due argomenti
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // si codifica la stringa
        byte[] data = Base64.encode(chaîne.getBytes());
        // si visualizza la sua codifica Base64
        System.out.println(new String(data));
    }

}

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

  

si ottiene il seguente risultato:

YWRtaW46YWRtaW4=

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


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

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

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

La risposta del server è la seguente:

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

Si ottiene effettivamente l'elenco dei medici:

 

Proviamo ora a effettuare una richiesta HTTP con un'intestazione di autenticazione errata. La risposta è quindi la seguente:

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

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

  

otteniamo il seguente risultato:

dXNlcjp1c2Vy
  • in [1] e [3]: l’intestazione di autenticazione HTTP;
  • in [2]: la risposta del servizio web. È diversa dalla precedente, che era [401 Unauthorized]. In questo caso, l’utente si è autenticato correttamente ma non dispone dei diritti sufficienti per accedere a URL;

Un servizio web protetto è ora operativo. Lo completeremo in modo che autorizzi le richieste interdominio. Questa esigenza è emersa nel documento [Tutoriel AngularJS / Spring 4] e, sebbene qui non sussista, provvederemo comunque a soddisfarla.

8.4.14. Implementazione delle richieste tra domini

Esaminiamo il problema delle richieste tra domini. Nel documento [Tutoriel AngularJS / Spring 4] viene sviluppata un'applicazione client/server in cui il client è un'applicazione AngularJS:

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

In realtà, non è esatto affermare che il browser impedisca all’applicazione Angular di interrogare il server [2]. In realtà, l’applicazione lo interroga per chiedergli se autorizza un client che non proviene dal suo stesso dominio a interrogarlo. Questa tecnica di condivisione è denominata CORS (Cross-Origin Resource Sharing). Il server [2] dà il proprio consenso inviando specifici header HTTP.

Per illustrare i problemi che si possono incontrare, creeremo un’applicazione client/server in cui:

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

8.4.14.1. Il progetto del client

  

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


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>

        <groupId>istia.st</groupId>
        <artifactId>rdvmedecins-webjson-client-cors</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>

        <name>rdvmedecins-webjson-client-cors</name>
        <description>Client for webjson server</description>

        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
                <relativePath /> <!-- ricerca il genitore nel repository -->
        </parent>

        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <start-class>istia.st.rdvmedecins.Client</start-class>
                <java.version>1.8</java.version>
        </properties>

        <dependencies>
                <!-- spring MVC -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
        </dependencies>
</project>
  • righe 14-19: si tratta di un progetto Spring Boot;
  • righe 29-32: si utilizza la dipendenza [spring-boot-starter-web] che include un server Tomcat e Spring MVC;

La pagina HTML è la seguente:

 

Viene generata dal codice seguente:


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

Il codice [client.js] è il seguente:


// dati globali
var url;
var posted;
var response;
var method;

function requestServer() {
    // si recuperano le informazioni del modulo
    var urlValue = url.val();
    var postedValue = posted.val();
    method = document.forms[0].elements['method'].value;
    // si effettua una chiamata Ajax manualmente
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}

function doGet(url) {
    // si effettua una chiamata Ajax manualmente
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8080' + url,
        type : 'GET',
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // risultato testo
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // errore di sistema
            response.text(jqXHR.responseText);
        }
    })
}

function doPost(url, posted) {
    // si effettua manualmente una chiamata Ajax
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8080' + url,
        type : 'POST',
        contentType : 'application/json',
        data : posted,
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // risultato testo
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // errore di sistema
            response.text(jqXHR.responseText);
        }
    })
}

// durante il caricamento del documento
$(document).ready(function() {
    // si recuperano i riferimenti dei componenti della pagina
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});

Lasciamo che sia il lettore a comprendere questo codice. Tutto è già stato incontrato prima o poi. Alcune righe meritano tuttavia una spiegazione:

  • riga 11:
    • [document] indica il documento caricato dal browser, ovvero il cosiddetto DOM (Document Object Model),
    • [document.forms[0]] indica il primo modulo del documento; un documento può contenerne diversi. In questo caso ce n’è solo uno,
    • [document.forms[0].elements['method']] indica l'elemento del modulo che possiede l'attributo [name='method']. Ce ne sono due:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • riga 11:
    • [document.forms[0].elements['method'].value] è il valore che verrà inviato per il componente che possiede l'attributo [name='method']. Si sa che il valore inviato è il valore dell’attributo [value] del pulsante di opzione selezionato. In questo caso, sarà quindi una delle stringhe ['get', 'post'];
  • righe 23-25: ci si rivolge a un server che richiede un'intestazione HTTP [Authorization: Basic code]. Creiamo questa intestazione per l'utente [admin / admin], l'unico autorizzato a interrogare il server;
  • riga 26: l’utente inserirà dei URL del tipo [/getAllMedecins, /supprimerRv, ...]. È quindi necessario completare questi URL;
  • riga 28: il server restituisce un jSON che è una forma di testo. Si indica il tipo [text/plain] come tipo di risultato per visualizzarlo così come è stato ricevuto;
  • riga 33: visualizzazione della risposta testuale del server;
  • riga 39: visualizzazione di un eventuale messaggio di errore in formato testo;
  • riga 52: per indicare che il client invia jSON;

Nell'applicazione client/server realizzata:

  • il client è un'applicazione web disponibile all'indirizzo URL [http://localhost:8081]. È l'applicazione che stiamo realizzando;
  • il server è un'applicazione web disponibile all'indirizzo URL [http://localhost:8080]. È il nostro server web / jSON;

Poiché il client non si collega dalla stessa porta del server, si presenta il problema delle richieste interdominio. [http://localhost:8080] e [http://localhost:8081] sono due domini diversi.

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


package istia.st.rdvmedecins;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {

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

    // pagine statiche
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
    }

    // configurazione dispatcherServlet
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

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

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

}
  • riga 14: la classe [Client] è una classe di configurazione Spring;
  • riga 15: si configura un'applicazione Spring MVC. Questa annotazione comporta una serie di configurazioni automatiche;
  • riga 16: per ridefinire alcuni valori predefiniti del framework Spring MVC, è necessario estendere la classe [WebMvcConfigurerAdapter];
  • righe 23-26: il metodo [addResourceHandlers] consente di specificare le cartelle in cui si trovano le risorse statiche (html, css, js, ...) dell'applicazione. In questo caso, si indica la cartella [static] situata nel Classpath del progetto:
  
  • righe 29-37: configurazione del bean [dispatcherServlet] che indica il servlet Spring MVC;
  • righe 40-43: il server Tomcat integrato funzionerà sulla porta 8081;

8.4.14.2. URL [/getAllMedecins]

Avviamo:

  • il server web/json sulla porta 8080;
  • il client di questo server sulla porta 8081;

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

  • in [2], eseguiamo un GET su URL [http://localhost:8080/getAllMedecins];

Non riceviamo alcuna risposta dal server. Osservando la console di sviluppo (Ctrl-Maiusc-I) si nota un errore:

  • in [1], ci troviamo nella scheda [Network];
  • In [2], si vede che la richiesta HTTP che è stata effettuata non è [GET], bensì [OPTIONS]. Nel caso di una richiesta interdominio, il browser verifica con il server che siano soddisfatte determinate condizioni inviandogli una richiesta HTTP [OPTIONS]. In questo caso, le richieste sono quelle indicate dai simboli [5-6];
  • in [5], il browser chiede se la destinazione URL sia raggiungibile tramite un GET. L’intestazione della richiesta [Access-Control-Request-Method] richiede una risposta con un’intestazione HTTP [Access-Control-Allow-Methods] che indichi che il metodo richiesto è accettato;
  • in [5], il browser invia l'intestazione HTTP [Origin: http://localhost:8081]. Questa intestazione richiede una risposta in un'intestazione HTTP [Access-Control-Allow-Origin] che indichi che l'origine specificata è accettata;
  • in [6], il browser chiede se le intestazioni HTTP, [accept] e [authorization] sono accettate. L'intestazione della richiesta [Access-Control-Request-Headers] attende una risposta con un'intestazione HTTP [Access-Control-Allow-Headers] che indichi che le intestazioni richieste sono accettate;
  • si verifica un errore in [3]. Cliccando sull’icona, si ottiene l’errore [4];
  • in [4], il messaggio indica che il server non ha inviato l’intestazione HTTP [Access-Control-Allow-Origin] che specifica se l’origine della richiesta è accettata;
  • in [7], si può constatare che il server non ha effettivamente inviato tale intestazione. Di conseguenza, il browser ha rifiutato di eseguire la richiesta HTTP GET inizialmente richiesta;

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

 

@Component
public class ApplicationModel implements IMetier {

    ...
    // dati di configurazione
    private boolean corsAllowed = true;
    private boolean secured = true;
    
...
    public boolean isCorsAllowed() {
        return corsAllowed;
}
  • riga 6: creiamo un valore booleano che indica se si accettano o meno i client esterni al dominio del server;
  • righe 10-12: il metodo di accesso a questa informazione;

Quindi creiamo un nuovo controller Spring MVC:

  

La classe [RdvMedecinsCorsController] è la seguente:


package rdvmedecins.web.controllers;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import rdvmedecins.web.models.ApplicationModel;

@Controller
public class RdvMedecinsCorsController {

    @Autowired
    private ApplicationModel application;

    // invio delle opzioni al client
    public void sendOptions(String origin, HttpServletResponse response) {
        // CORS consentito?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // si imposta l'intestazione CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // si autorizzano determinati header
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // si autorizza il GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }

    // elenco dei medici
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        sendOptions(origin, response);
    }
}
  • righe 12-13: la classe [RdvMedecinsCorsController] è un controller Spring;
  • righe 33-36: definiscono un'azione che gestisce URL [/getAllMedecins] quando viene richiesta con il comando HTTP [OPTIONS];
  • riga 34: il metodo [getAllMedecins] accetta come parametri:
    • l'oggetto [@RequestHeader(value = "Origin", required = false)] che recupererà l'intestazione HTTP [Origin] della richiesta. Tale intestazione è stata inviata dal mittente della richiesta:
Origin:http://localhost:8081

Si specifica che l’intestazione HTTP [Origin] è facoltativa [required = false]. In questo caso, se l'intestazione è assente, il parametro [String origin] assumerà il valore null. Poiché [required = true] è il valore predefinito, se l'intestazione è assente viene generata un'eccezione. Si è voluto evitare questo caso;

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

Questi due parametri vengono inseriti da Spring;

  • riga 35: si delega l'elaborazione della richiesta al metodo delle righe 19-30;
  • righe 15-16: viene iniettato l’oggetto [ApplicationModel];
  • righe 21-23: se l'applicazione è configurata per accettare le richieste interdominio e se il mittente ha inviato l'intestazione HTTP [Origin] e se tale origine inizia con [http://localhost], allora si accetta la richiesta inter-dominio, altrimenti la si rifiuta;
  • riga 25: se il client si trova nel dominio [http://localhost:port], si invia l'intestazione HTTP:
Access-Control-Allow-Origin:  http://localhost:porta

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

  • riga 25: abbiamo segnalato due intestazioni HTTP particolari nella richiesta HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

All'intestazione HTTP [Access-Control-Request-X], il server risponde con un'intestazione HTTP [Access-Control-Allow-X] in cui indica ciò che è consentito. Le righe 23-26 si limitano a riprendere la richiesta del client per indicare che è stata accettata;

Ora siamo pronti per nuovi test. Lanciamo la nuova versione del servizio web e scopriamo che il problema persiste. Non è cambiato nulla. Se alla riga 35 sopra inseriamo un output in console, questo non viene mai visualizzato, dimostrando così che il metodo [getAllMedecins] della riga 34 non viene mai chiamato.

Dopo alcune ricerche, scopriamo che Spring MVC gestisce autonomamente i comandi HTTP e [OPTIONS] con un'elaborazione predefinita. Pertanto è sempre Spring a rispondere e mai il metodo [getAllMedecins] alla riga 34. Questo comportamento predefinito di Spring MVC può essere modificato. Modifichiamo la classe [WebConfig] esistente:

  

package rdvmedecins.web.config;

...
import org.springframework.web.servlet.DispatcherServlet;

@Configuration
public class WebConfig {

    // configurazione di dispatcherservlet per le intestazioni CORS
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
    
    // mappatura jSON
...
  • righe 10-11: il bean [dispatcherServlet] serve a definire il servlet che gestisce le richieste dei client. In questo caso è di tipo [DispatcherServlet], il servlet del framework Spring MVC;
  • riga 12: si crea un'istanza di tipo [DispatcherServlet];
  • riga 13: si richiede alla servlet di inoltrare all'applicazione i comandi HTTP [OPTIONS];
  • riga 14: si rende la servlet così configurata;

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

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

Esaminiamo ora la seconda richiesta:

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

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

Modifichiamo il controller [RdvMedecinsController] nel modo seguente:


    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;
...
    // elenco dei medici
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins(HttpServletResponse httpServletResponse,
            @RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
        // la risposta
        Response<List<Medecin>> response;
        // intestazioni CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // stato dell'applicazione
...
  • righe 1-2: viene inserito il controller [RdvMedecinsCorsController];
  • righe 7-8: si inietta nei parametri del metodo [getAllMedecins] l’oggetto HttpServletResponse, che incapsula la risposta che verrà inviata al client, e l’intestazione HTTP [Origin];
  • riga 12: si richiama il metodo [sendOptions] del controller [RdvMedecinsCorsController], lo stesso che è stato richiamato per elaborare la richiesta HTTP [OPTIONS]. Invierà quindi le stesse intestazioni HTTP utilizzate per quella richiesta;

Dopo questa modifica, i risultati sono i seguenti:

 

Abbiamo ottenuto correttamente l’elenco dei medici.

8.4.14.3. Gli altri URL [GET]

Mostriamo ora gli altri URL interrogati tramite un GET. Nei controller, il codice delle azioni che li gestiscono segue il modello delle azioni che hanno precedentemente gestito l’URL [/getAllMedecins]. Il lettore può verificare il codice negli esempi forniti con questo documento. Ecco un esempio:

in [RdvMedecinsCorsController]


    // elenco degli appuntamenti di un medico
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
    public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin,    HttpServletResponse response) {
        sendOptions(origin, response);
}

in [RdvMedecinsController]


    // elenco degli appuntamenti di un medico
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour,
            HttpServletResponse httpServletResponse, @RequestHeader(value = "Origin", required = false) String origin)
                    throws JsonProcessingException {
        // la risposta
        Response<List<Rv>> response = null;
        boolean erreur = false;
        // intestazioni CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // stato dell'applicazione
...

Ecco ora alcune schermate di esecuzione:

 
 
 
 
 
 

8.4.14.4. I URL [POST]

Esaminiamo il seguente caso:

  • si effettua un trasferimento da POST [1] verso URL [2];
  • in [3], il valore inviato. Si tratta di una stringa jSON;
  • in totale, si cerca di eliminare l'appuntamento con il codice [id] 100;

Per il momento non modifichiamo alcun codice. Il risultato ottenuto è quindi il seguente:

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

Modifichiamo il metodo [RdvMedecinsCorsController.sendOptions] nel modo seguente:


    public void sendOptions(String origin, HttpServletResponse response) {
        // Cors consentito?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // si imposta l'intestazione CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // si autorizzano alcune intestazioni
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // si autorizza il GET
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
  • riga 9: abbiamo aggiunto l'intestazione HTTP [Content-Type] (le maiuscole e le minuscole non fanno differenza);
  • riga 11: è stato aggiunto il metodo HTTP [POST];

In questo modo i metodi [POST] vengono trattati allo stesso modo delle richieste [GET]. Ecco l'esempio di URL [/supprimerRv]:

in [RdvMedecinsController]


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

in [RdvMedecinsCorsController]


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

Il risultato ottenuto è il seguente:

 

Per URL e [/ajouterRv], si ottiene il seguente risultato:

 

8.4.14.5. Conclusion

La nostra applicazione ora supporta le richieste interdominio. Queste possono essere autorizzate o meno tramite configurazione nella classe [ApplicationModel]:


    // dati di configurazione
    private boolean corsAllowed = false;

8.5. Client programmato del servizio web / jSON

Torniamo all’architettura generale dell’applicazione che vogliamo scrivere:

La parte superiore dello schema è stata già implementata. Si tratta del server web / jSON. Passiamo ora alla parte inferiore, iniziando dal suo livello [DAO]. Lo implementeremo e poi lo testeremo con un client da console. L’architettura di test sarà la seguente:

8.5.1. Il progetto del client da console

Il progetto STS del client da console sarà il seguente:

  

8.5.2. Configurazione Maven

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


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>istia.st.rdvmedecins</groupId>
        <artifactId>rdvmedecins-webjson-client-console</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>rdvmedecins-webjson-client-console</name>
        <description>Client console du serveur web / jSON</description>

        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <java.version>1.8</java.version>
        </properties>

        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
                <relativePath /> <!-- ricerca del genitore dal repository -->
        </parent>

        <dependencies>
                <!-- Spring -->
                <dependency>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-web</artifactId>
                </dependency>
                <!-- libreria jSON utilizzata da Spring -->
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-core</artifactId>
                </dependency>
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-databind</artifactId>
                </dependency>
                <!-- componente utilizzato da Spring RestTemplate -->
                <dependency>
                        <groupId>org.apache.httpcomponents</groupId>
                        <artifactId>httpclient</artifactId>
                </dependency>
        </dependencies>
</project>
  • righe 15-20: il progetto Spring Boot principale;
  • righe 24-27: il client console del server web / jSON si basa su un componente denominato [RestTemplate] fornito dalla dipendenza [spring-web];
  • righe 29-36: la serializzazione/deserializzazione degli oggetti jSON richiede una libreria jSON. Utilizziamo una variante della libreria Jackson impiegata da Spring Web;
  • righe 38-41: al livello più basso, il componente [RestTemplate] comunica con il server tramite i socket TCP/IP. Vogliamo impostare il valore di [timeout], ovvero il tempo massimo di attesa per una risposta dal server. Il componente [RestTemplate] non ci permette di impostarlo. Per farlo, passeremo al costruttore [RestTemplate] un componente di basso livello fornito dalla dipendenza [org.apache.httpcomponents.httpclient]. È questa dipendenza che ci consentirà di impostare il [timeout] della comunicazione;

8.5.3. Il pacchetto [rdvmedecins.client.entities]

  

Il pacchetto [rdvmedecins.client.entities] raggruppa tutte le entità che il servizio web / jSON invia tramite i suoi vari URL. Non le descriveremo nuovamente in dettaglio. Ci limiteremo a dire che le entità JPA e [Client, Creneau, Medecin, Rv, Personne] sono state private di tutte le loro annotazioni JPA e delle loro annotazioni jSON. Ecco, ad esempio, la classe [Rv]:


package rdvmedecins.client.entities;

import java.util.Date;

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

    // giorno dell'appuntamento
    private Date jour;

    // un appuntamento è associato a un cliente
    private Client client;

    // un appuntamento è collegato a una fascia oraria
    private Creneau creneau;

    // chiavi esterne
    private long idClient;
    private long idCreneau;

    // Costruttore predefinito
    public Rv() {
    }

    // con parametri
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }

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

// getter e setter
...
}

8.5.4. Il pacchetto [rdvmedecins.client.requests]

  

Il pacchetto [rdvmedecins.client.requests] raggruppa le due classi il cui valore jSON è associato a URL, [/ajouterRv] e [supprimerRv]. Sono identiche a quelle presenti sul lato server.

8.5.5. Il pacchetto [rdvmedecins.client.responses]

  

[Response] è il tipo di tutte le risposte del servizio web / jSON. Si tratta di un tipo generico:


package rdvmedecins.client.responses;

import java.util.List;

public class Response<T> {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // eventuali messaggi di errore
    private List<String> messages;
    // il corpo della risposta
    private T body;

    // costruttori
    public Response() {

    }

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

    // getter e setter
...
}
  • riga 5: il tipo [T] varia a seconda dell'URL del servizio web / jSON;

8.5.6. Il pacchetto [rdvmedecins.client.dao]

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

8.5.7. Il pacchetto [rdvmedecins.client.config]

  

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


package rdvmedecins.client.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {

    @Bean
    public RestTemplate restTemplate() {
        // creazione del componente RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // risultato
        return restTemplate;
    }
    
    // mappatori jSON
    
    @Bean
    public ObjectMapper jsonMapper(){
        return new ObjectMapper();
    }
    
    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
    }

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

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

}
  • riga 13: la classe [DaoConfig] è una classe di configurazione Spring;
  • riga 14: il pacchetto [rdvmedecins.client.dao] verrà esaminato alla ricerca di componenti Spring. Al suo interno si troverà il componente [Dao];
  • righe 17-24: definiscono un singleton Spring denominato [restTemplate] (il nome del metodo). Questo metodo restituisce un'istanza [RestTemplate], che è lo strumento di base fornito da Spring per comunicare con un servizio web / jSON;
  • riga 21: si potrebbe scrivere [RestTemplate restTemplate = new RestTemplate() ;]. Nella maggior parte dei casi è sufficiente. Ma in questo caso vogliamo impostare i [timeout] del client. A tal fine, si inietta nel componente [RestTemplate] un componente di basso livello di tipo [HttpComponentsClientHttpRequestFactory] (riga 20) che ci consentirà di impostare questi [timeout]. La dipendenza Maven necessaria è stata presentata;
  • righe 28-57: definiscono i mappatori jSON. Si tratta dei mappatori jSON utilizzati sul lato server (cfr. paragrafo 8.4.11.3) per serializzare il tipo T della risposta [Response<T>]. Questi stessi convertitori verranno ora utilizzati sul lato client per deserializzare il tipo T;

8.5.8. L'interfaccia [IDao]

Torniamo all’architettura dell’applicazione:

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


package rdvmedecins.client.dao;

import java.util.List;

import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;

public interface IDao {
    // URL del servizio web
    public void setUrlServiceWebJson(String url);

    // timeout
    public void setTimeout(int timeout);

    // autenticazione
    public void authenticate(User user);

    // elenco dei clienti
    public List<Client> getAllClients(User user);

    // elenco dei medici
    public List<Medecin> getAllMedecins(User user);

    // elenco delle fasce orarie di un medico
    public List<Creneau> getAllCreneaux(User user, long idMedecin);

    // trova un cliente identificato dal suo ID
    public Client getClientById(User user, long id);

    // trovare un cliente identificato dal suo ID
    public Medecin getMedecinById(User user, long id);

    // trovare un appuntamento identificato dal suo ID
    public Rv getRvById(User user, long id);

    // trovare una fascia oraria identificata dal proprio ID
    public Creneau getCreneauById(User user, long id);

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

    // eliminare un RV
    public void supprimerRv(User user, long idRv);

    // elenco degli appuntamenti di un medico in un determinato giorno
    public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);

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

}
  • riga 14: il metodo che consente di impostare la radice del servizio web / jSON, ad esempio [http://localhost:8080];
  • riga 17: il metodo che consente di impostare i [timeout] lato client. Si desidera controllare questo parametro poiché alcuni client HTTP a volte impiegano molto tempo ad attendere una risposta che non arriverà;
  • riga 20: il metodo che consente di identificare un utente [login, passwd]. Genera un'eccezione se l'utente non viene riconosciuto;
  • righe 22-53: a ogni URL esposta dal servizio web / jSON è associato un metodo dell'interfaccia la cui firma deriva dalla firma del metodo lato server che gestisce la URL esposta. Prendiamo ad esempio il seguente URL lato server:

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

Sul lato client, si dichiara il seguente metodo:


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

Questa firma è valida quando il server invia una risposta [int status, List<String> messages, String body] con [status==0]. In questo caso abbiamo [messages==null && body!=null]. Non è valida quando si ha [status!=0]. In questo caso si ottiene [messages!=null && body==null]. È necessario segnalare in qualche modo che si è verificato un errore. A tal fine, verrà generata un'eccezione di tipo [RdvMedecinsException] come segue:


package rdvmedecins.client.dao;

import java.util.List;

public class RdvMedecinsException extends RuntimeException {

    private static final long serialVersionUID = 1L;
    // codice di errore
    private int status;
    // elenco dei messaggi di errore
    private List<String> messages;

    public RdvMedecinsException() {
    }

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

    // getter e setter
...
}
  • righe 9 e 11: l’eccezione riprenderà i valori dei campi [status, messages] dell’oggetto [Response<T>] inviato dal server;
  • riga 5: la classe [RdvMedecinsException] estende la classe [RuntimeException]. Si tratta quindi di un'eccezione non controllata, ovvero non è obbligatorio gestirla con un try/catch né dichiararla nella firma dei metodi dell'interfaccia;

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


package rdvmedecins.client.entities;

public class User {

    // dati
    private String login;
    private String passwd;

    // costruttori
    public User() {
    }

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

    // getter e setter
    ...
}

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

8.5.9. Il pacchetto [rdvmedecins.clients.console]

Ora che conosciamo l’interfaccia del livello [DAO], possiamo presentare l’applicazione da console.

  

La classe [Main] è la seguente:


package rdvmedecins.clients.console;

import java.io.IOException;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {

    // serializzatore jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // timeout delle connessioni in millisecondi
    static private int TIMEOUT = 1000;

    public static void main(String[] args) throws IOException {
        // si recupera un riferimento sul livello [DAO]
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // si imposta l'URL del servizio web / json
        dao.setUrlServiceWebJson("http://localhost:8080");
        // si impostano i timeout in millisecondi
        dao.setTimeout(TIMEOUT);

        // Autenticazione
        String message = "/authenticate [admin,admin]";
        try {
            dao.authenticate(new User("admin", "admin"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        message = "/authenticate [user,user]";
        try {
            dao.authenticate(new User("user", "user"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        message = "/authenticate [user,x]";
        try {
            dao.authenticate(new User("user", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        message = "/authenticate [x,x]";
        try {
            dao.authenticate(new User("x", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        message = "/authenticate [admin,x]";
        try {
            dao.authenticate(new User("admin", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // elenco dei clienti
        message = "/getAllClients";
        try {
            showResponse(message, dao.getAllClients(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // elenco dei medici
        message = "/getAllMedecins";
        try {
            showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // elenco degli orari disponibili del medico 2
        message = "/getAllCreneaux/2";
        try {
            showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

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

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

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

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

        // aggiunta di un appuntamento
        message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
        long idRv = 0;
        try {
            Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
            idRv = response.getId();
            showResponse(message, response);
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // elenco degli appuntamenti del medico 1 dell'8 gennaio 2015
        message = "/getRvMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // agenda del medico 1 dell'8 gennaio 2015
        message = "/getAgendaMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
        // eliminazione dell'appuntamento aggiunto
        message = String.format("/supprimerRv [idRv=%s]", idRv);
        try {
            dao.supprimerRv(new User("admin", "admin"), idRv);
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // elenco degli appuntamenti del medico 1 dell'8 gennaio 2015
        message = "/getRvMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
        // chiusura contesto
        context.close();
    }

    private static void showException(String message, RdvMedecinsException e) {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
        for (String msg : e.getMessages()) {
            System.out.println(msg);
        }
    }

    private static <T> void showResponse(String message, T response) throws JsonProcessingException {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(mapper.writeValueAsString(response));
    }
}
  • riga 19: il serializzatore jSON che ci consentirà di visualizzare la risposta del server, riga 184;
  • riga 25: il componente [AnnotationConfigApplicationContext] è un componente Spring in grado di utilizzare le annotazioni di configurazione di un'applicazione Spring. Passiamo al suo costruttore, la classe [AppConfig] che configura l'applicazione;
  • riga 26: si recupera un riferimento al livello [DAO];
  • righe 27-30: lo si configura;
  • righe 32-169: si testano tutti i metodi dell'interfaccia [IDao];

I risultati ottenuti sono i seguenti:


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

Lasciamo al lettore il compito di associare i risultati al codice. Quest’ultimo mostra come richiamare ciascun metodo del livello [DAO]. Basti notare alcuni punti:

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

I log della console sono controllati dai seguenti file:

 

[application.properties]


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

[logback.xml]


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- agli encoder viene assegnato per impostazione predefinita il tipo ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- controllo del livello dei log -->
        <root level="info"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>

8.5.10. Implementazione del livello [DAO]

Ora non ci resta che presentare il cuore del livello [DAO], ovvero l'implementazione della sua interfaccia [IDao]. Lo faremo in modo graduale.

 

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

La classe padre [AbstractDao] è la seguente:


package rdvmedecins.client.dao;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import rdvmedecins.client.entities.User;

public abstract class AbstractDao implements IDao {

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

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

    public void setTimeout(int timeout) {
        // impostazione del timeout delle richieste del client web
        HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
                .getRequestFactory();
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
    }

    private String getBase64(User user) {
        // si codificano in base 64 l'utente e la sua password - richiede
        // Java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }

    // richiesta generica
    protected String getResponse(User user, String url, String jsonPost) {
...
    }

}
  • riga 20: la classe è astratta, il che ci impedisce di designarla come componente Spring. Sarà la sua classe figlia a essere designata come tale;
  • righe 23-24: iniettiamo il bean [restTemplate] che abbiamo definito nella classe di configurazione [AppConfig];
  • riga 25: URL è la radice del servizio web /jSON;
  • righe 32-38: impostano il timeout del client in attesa di una risposta dal server;
  • riga 34: recuperiamo il componente [HttpComponentsClientHttpRequestFactory] che avevamo iniettato nel bean [restTemplate] al momento della sua creazione (cfr. [AppConfig]);
  • riga 36: impostiamo il tempo massimo di attesa del client quando stabilisce una connessione con il server;
  • riga 37: impostiamo il tempo massimo di attesa del client quando attende una risposta a una delle sue richieste;

L'implementazione dei metodi di comunicazione con il server verrà fattorizzata nel seguente metodo generico:


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

Continuiamo:


// richiesta generica
    protected String getResponse(User user, String url, String jsonPost) {
        // URL: URL da contattare
        // jsonPost: il valore jSON da inviare
        try {
            // esecuzione della richiesta
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }
            // si esegue la richiesta
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e) {
            throw new RdvMedecinsException(20, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(21, getMessagesForException(e));
        }
    }
  • righe 23-24: l’istruzione che invia la richiesta al server e ne riceve la risposta. Il componente [RestTemplate] offre un numero significativo di metodi di scambio con il server. Si sarebbe potuto scegliere un metodo diverso da [exchange]. Il secondo parametro della chiamata definisce il tipo di risposta attesa, in questo caso una stringa jSON. Il primo parametro è la richiesta di tipo [RequestEntity] (riga 7). Il risultato del metodo [exchange] è di tipo [ResponseEntity<String>]. Il tipo [ResponseEntity] incapsula la risposta completa del server, le intestazioni HTTP e il documento inviato da quest’ultimo. Allo stesso modo, il tipo [RequestEntity] incapsula l’intera richiesta del client, comprese le intestazioni HTTP e l’eventuale valore inviato tramite POST;
  • riga 23: è il corpo dell’oggetto [ResponseEntity<String>] che viene restituito al metodo chiamante, ovvero la stringa jSON inviata dal server;
  • righe 9-21: dobbiamo costruire la richiesta di tipo [RequestEntity]. Essa varia a seconda che si utilizzi un GET o un POST per effettuare la richiesta;
  • riga 9: la richiesta per un GET. La classe [RequestEntity] offre metodi statici per creare le richieste GET, POST, HEAD,... Il metodo [RequestEntity.get] consente di creare una richiesta GET concatenando i diversi metodi che la compongono:
    • il metodo [RequestEntity.get] accetta come parametro il URL di destinazione sotto forma di un'istanza URI,
    • il metodo [accept] consente di definire gli elementi dell’intestazione HTTP [Accept]. Qui specifichiamo che accettiamo il tipo [application/json] che il server invierà;
    • il risultato di questa concatenazione di metodi è un tipo [HeadersBuilder];
  • righe 10-12: nel caso in cui il parametro [User user] non sia null, si include l’intestazione HTTP [Authorization] nella richiesta;
  • riga 13: il metodo [HeadersBuilder.build] utilizza queste diverse informazioni per costruire il tipo [RequestEntity] della richiesta;
  • riga 15: la richiesta per un POST. Il metodo [RequestEntity.post] consente di creare una richiesta POST concatenando i diversi metodi che la costruiscono:
    • il metodo [RequestEntity.post] accetta come parametro il URL di destinazione sotto forma di un'istanza URI,
    • il metodo [header] consente di definire le intestazioni HTTP che si desidera utilizzare, in questo caso quella relativa all’autorizzazione,
    • il metodo [header] che segue include nella richiesta l’intestazione [Content-Type: application/json] per indicarle che il valore inviato le arriverà sotto forma di una stringa jSON;
    • il metodo [accept] consente di indicare che accettiamo il tipo [application/json] che il server invierà;
  • righe 17-19: nel caso in cui il parametro [User user] non sia null, si include l'intestazione HTTP [Authorization] nella richiesta;
  • riga 20: il metodo [BodyBuilder.body] imposta il valore registrato. Questo è il secondo parametro del metodo generico [getResponse] (riga 2);
  • righe 25-28: se si verifica un errore qualsiasi, viene generata un'eccezione di tipo [RdvMedecinsException];

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


    // elenco dei messaggi di errore di un'eccezione
    protected static List<String> getMessagesForException(Exception exception) {
        // si recupera l'elenco dei messaggi di errore dell'eccezione
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            // si recupera il messaggio solo se !=null e non è vuoto
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa successiva
            cause = cause.getCause();
        }
        return erreurs;
}

Il metodo privato [getBase64] fornisce il codice Base64 della stringa 'login:passwd' per l'intestazione di autenticazione HTTP:


    private String getBase64(User user) {
        // codifica in base 64 l'utente e la sua password - richiede Java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}

La classe [Dao] estende la classe [AbstractDao] nel modo seguente:


package rdvmedecins.client.dao;

import java.io.IOException;
import java.util.List;

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

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;

@Service
public class Dao extends AbstractDao implements IDao {

    // mappatori jSON
    @Autowired
    ObjectMapper jsonMapper;

    @Autowired
    private ObjectMapper jsonMapperShortCreneau;

    @Autowired
    private ObjectMapper jsonMapperLongRv;

    @Autowired
    private ObjectMapper jsonMapperShortRv;

    public List<Client> getAllClients(User user) {
        ...
    }

    public List<Medecin> getAllMedecins(User user) {
...
    }
...
}
  • riga 22: la classe [Dao] è un componente Spring. Qui è stata utilizzata l'annotazione [@Service]. Si sarebbe potuto continuare a utilizzare l'annotazione [@Component] utilizzata fino a quel momento;
  • righe 26-36: iniezione dei quattro mappatori jSON definiti nella classe di configurazione [DaoConfig];

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

Innanzitutto una richiesta [GET]:


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

La richiesta POST esaminata sarà la seguente:


    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
        // la risposta
        Response<Rv> response;
        try {
            // l'appuntamento
            String jsonResponse = getResponse(user, "/ajouterRv",
                    jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
            // l'appuntamento Appuntamento
            response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
            });
        } catch (RdvMedecinsException e) {
            throw e;
        } catch (IOException e) {
            throw new RdvMedecinsException(381, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(382, getMessagesForException(e));
        }
        // analisi della risposta
        int status = response.getStatus();
        if (status != 0) {
            throw new RdvMedecinsException(status, response.getMessages());
        } else {
            return response.getBody();
        }
}
  • riga 6: il metodo [getResponse] viene chiamato con i seguenti parametri:
    • 1: l'utente;
    • 2: l’URL di destinazione,
    • 3: il valore inviato: si passa il valore jSON di tipo [PostAjouter] costruito con le informazioni ricevute come parametri dal metodo. Si utilizza un mappatore jSON senza filtri;
  • riga 9: lato server, è il mappatore jSON [jsonMapperLongRv] che ha serializzato la risposta del server. Lato client, si utilizza lo stesso mappatore per deserializzarla;
  • riga 6: il URL [/ajouterRv] restituisce il valore jSON di tipo [Response<Rv>];
  • righe 4-11: qui, il metodo [getResponse] è stato inserito in un try/catch perché la serializzazione del valore inviato può generare un'eccezione. Il metodo [getResponse] potrebbe generare un'eccezione [RdvMedecinsException]. In tal caso, ci si limita a rilanciarla (righe 11-12);

Il codice che segue (righe 13-24) è analogo a quello appena esaminato. L’unica differenza rispetto a un’operazione GET è quindi il secondo parametro del metodo [getResponse], che deve essere il valore jSON del valore da inserire.

Gli altri metodi seguono lo stesso schema.

8.5.11. Anomalia

Effettuando vari test si riscontra un'anomalia riassunta nella seguente classe [Anomalie]:


package rdvmedecins.clients.console;

import java.io.IOException;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Anomalie {

    // serializzatore jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // timeout delle connessioni in millisecondi
    static private int TIMEOUT = 1000;

    public static void main(String[] args) throws IOException {
        // si recupera un riferimento sul livello [DAO]
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // si imposta l'URL del servizio web / json
        dao.setUrlServiceWebJson("http://localhost:8080");
        // si impostano i timeout in millisecondi
        dao.setTimeout(TIMEOUT);

        // Autenticazione
        String message = "/authenticate [admin,admin]";
        try {
            dao.authenticate(new User("admin", "admin"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // Autenticazione
        message = "/authenticate [admin,x]";
        try {
            dao.authenticate(new User("admin", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // Autenticazione
        message = "/authenticate [user,user]";
        try {
            dao.authenticate(new User("user", "user"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }

        // Chiusura del contesto
        context.close();
    }

    private static void showException(String message, RdvMedecinsException e) {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
        for (String msg : e.getMessages()) {
            System.out.println(msg);
        }
    }
}
  • righe 31-38: si autentica l’utente [admin, admin];
  • righe 40-47: si autentica l'utente [admin, x], che quindi ha una password errata;
  • righe 49-56: si autentica l'utente [user, user], che è un utente esistente ma non autorizzato;

Ecco i risultati:

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 si commentano le righe 33-38 del codice, si ottiene 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 atteso. È come se, una volta che l’utente [admin, admin] si fosse autenticato con successo per la prima volta, la sua password non fosse più necessaria per le volte successive. È proprio così. Spring Security utilizza per impostazione predefinita una sessione che fa sì che, una volta che un utente si è autenticato, non debba più farlo nelle richieste successive. È possibile modificare la configurazione di [Spring Security] nel server web / jSON affinché ciò non avvenga più:

  

Il file [SecurityConfig] deve essere modificato come segue:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
            // nessuna sessione
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
  • la riga 5 richiede che non vi sia alcuna sessione di sicurezza;

Questo ha risolto il problema dell'anomalia.

8.6. Codice del server Spring / Thymeleaf

8.6.1. Introduzione

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

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

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

  • abbiamo un'architettura client [2] / server [1] in cui il client e il server comunicano tramite jSON;
  • in [1], il livello web Spring MVC / Thymeleaf fornisce viste, frammenti di vista e dati in jSON. Il server è quindi un server web / jSON come il server [Web1]. Anch’esso è stateless;
  • in [2]: il codice JavaScript incorporato nella vista caricata all’avvio dell’applicazione è strutturato a livelli:
    • il livello [présentation] gestisce le interazioni con l’utente,
    • il livello [DAO] gestisce l’accesso ai dati tramite il server [Web2];
  • il client [2] memorizzerà alcune viste nella cache per alleggerire il carico del server;

Costruiremo il server web / jSON [Web1] implementato con Spring MVC / Thymeleaf in diverse fasi:

  • scoperta del framework CSS Bootstrap;
  • scrittura delle viste;
  • scrittura del controller;

Successivamente, e separatamente, realizzeremo il client JS del server [Web1]. Per dimostrare chiaramente che questo client gode di una certa indipendenza rispetto al server [Web1], lo realizzeremo con lo strumento [Webstorm] anziché con STS.

Di seguito, alcuni dettagli saranno tralasciati perché potrebbero distrarci dall’aspetto fondamentale, ovvero l’organizzazione del codice. Il lettore interessato potrà trovare il codice completo sul sito di questo documento.

8.6.2. Il progetto STS

  • in [1], i codici Java;
  • in [2], le viste;

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


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

La configurazione Java è gestita da due file:

 

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


package rdvmedecins.springthymeleaf.server.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;

@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {

    // ----------------- configurazione del livello [web]
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

    // Configurazione di DispatcherServlet per le intestazioni CORS
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }

}

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

La classe [AppConfig] configura l'intera applicazione:


package rdvmedecins.springthymeleaf.server.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import rdvmedecins.client.config.DaoConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {

    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // radice del servizio web / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout in millisecondi
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;

    ...
    
}
  • riga 11: [AppConfig] importa la configurazione del livello [DAO] e del livello [web];
  • righe 15-16: gli identificativi che consentiranno all’applicazione di effettuare un accesso all’avvio dell’applicazione per memorizzare nella cache medici e clienti;
  • riga 18: l'URL del servizio web / jSON [Web1];
  • riga 20: il timeout delle chiamate HTTP dell'applicazione;
  • riga 22: un valore booleano per autorizzare o meno le chiamate tra domini;

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

  

server.port=8081

8.6.3. Le funzionalità dell'applicazione

Sono state descritte nel paragrafo 8.2. Le riportiamo ora. Tramite un browser, si richiede l’URL [http://localhost:8081/boot.html]:

  • [1], la pagina di accesso all’applicazione;
  • in [2] e [3], rispettivamente l’ID e la password di chi desidera utilizzare l’applicazione. Ci sono due utenti: admin/admin (login/password) con un ruolo (ADMIN) e user/user con un ruolo (USER). Solo il ruolo ADMIN ha il diritto di utilizzare l'applicazione. Il ruolo USER serve solo a mostrare la risposta del server in questo caso d'uso;
  • in [4], il pulsante che consente di connettersi al server;
  • in [5], la lingua dell’applicazione. Ce ne sono due: il francese (impostazione predefinita) e l’inglese;
  • in [6], l'URL del server [rdvmedecins-springthymeleaf-server];
  • in [1], si effettua l'accesso;
  • una volta effettuato l'accesso, è possibile scegliere il medico con cui si desidera fissare un appuntamento [2] e il giorno dell'appuntamento [3]. Non appena vengono inseriti il medico e il giorno, viene visualizzato automaticamente il calendario:
  • una volta visualizzato il calendario del medico, è possibile prenotare una fascia oraria [5];
  • in [6], si seleziona il paziente per l’appuntamento e si conferma la scelta in [7];

Una volta confermato l'appuntamento, si torna automaticamente all'agenda dove il nuovo appuntamento è ora registrato. Questo appuntamento potrà essere successivamente cancellato con [8].

Le funzionalità principali sono state descritte. Sono semplici. Concludiamo con la gestione della lingua:

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

8.6.4. Fase 1: introduzione al framework CSS Bootstrap

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

8.6.4.1. Il progetto degli esempi

Il formato degli esempi sarà il seguente:

  • in [1]: il progetto nella sua interezza;
  • in [2]: i codici Java;
  • in [3]: gli script JavaScript;
  • in [4]: le librerie JavaScript;
  • in [5]: le viste Thymeleaf;
  • in [6]: i fogli di stile;

8.6.4.1.1. Configurazione Maven

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


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>istia.st</groupId>
    <artifactId>rdvmedecins-webjson-client-bootstrap</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>rdvmedecins-webjson-client-bootstrap</name>
    <description>Démos Bootstrap</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath /> <!-- ricerca del genitore dal repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

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

</project>

8.6.4.1.2. Configurazione Java
  

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


package istia.st.rdvmedecins;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;

@EnableAutoConfiguration
@ComponentScan({ "istia.st.rdvmedecins" })
public class BootstrapDemo extends WebMvcConfigurerAdapter {

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

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }
}

Abbiamo già incontrato questo tipo di codice.

8.6.4.1.3. Il controller Spring
  

Il controller [BootstrapController] è il seguente:


package istia.st.rdvmedecins;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class BootstrapController {

    @RequestMapping(value = "/bs-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bso1() {
        return "bs-01";
    }

    @RequestMapping(value = "/bs-02", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs02() {
        return "bs-02";
    }

    @RequestMapping(value = "/bs-03", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs03() {
        return "bs-03";
    }

    @RequestMapping(value = "/bs-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs04() {
        return "bs-04";
    }

    @RequestMapping(value = "/bs-05", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs05() {
        return "bs-05";
    }

    @RequestMapping(value = "/bs-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs06() {
        return "bs-06";
    }

    @RequestMapping(value = "/bs-07", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs07() {
        return "bs-07";
    }

    @RequestMapping(value = "/bs-08", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs08() {
        return "bs-08";
    }
}

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

8.6.4.1.4. Il file [application.properties]

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


server.port=8082

8.6.4.2. Esempio n. 1: il jumbotron

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

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


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

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


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

8.6.4.3. Esempio n. 2: la barra di navigazione

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

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

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


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

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


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

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

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

Ecco un esempio:

Image

8.6.4.4. Esempio n. 3: il pulsante con elenco

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

  • La novità è il menu a tendina [1], noto anche come "dropdown";

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


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

La vista [navbar2.xml] è la seguente:


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

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


function initNavBar2() {
    // menu a tendina delle lingue
    $('.dropdown-toggle').dropdown();
}

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

function setLang(lang) {
    var msg;
    switch (lang) {
    case 'fr':
        msg = "Vous avez choisi la langue française...";
        break;
    case 'en':
        msg = "You have selected english language...";
        break;
    }
    showInfo(msg);
}

function showInfo(message) {
    $("#info").text(message);
}
  • righe 1-4: la funzione che inizializza [dropdown]. [$('.dropdown-toggle')] individua l'elemento con la classe [dropdown-toggle]. Si tratta del pulsante con elenco (riga 28 della vista). A esso viene applicata la funzione JS [dropdown()], definita nel file JS [bootstrap.js]. Solo dopo questa operazione il pulsante si comporta come un pulsante con elenco;
  • righe 10-21: la funzione eseguita quando si seleziona una lingua;

Ecco un esempio:

Image

8.6.4.5. Esempio n. 4: un menu

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

È stato aggiunto un menu [1].

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


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

La vista [navbar3.xml] è la seguente:


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

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


...
function initNavBar3() {
    // menu a tendina delle lingue
    $('.dropdown-toggle').dropdown();
    // l'immagine animata
    loading = $("#loading");
    loading.hide();
}

function afficherAgenda() {
    showInfo("option [Agenda] cliquée...");
}

function retourAccueil() {
    showInfo("option [Retour accueil] cliquée...");
}

function retourAgenda() {
    showInfo("option [Retour agenda] cliquée...");
}

function validerRv() {
    showInfo("option [Valider] cliquée...");
}

function setMenu(show) {
    // i link del menu
    var lnkAfficherAgenda = $("#lnkAfficherAgenda");
    var lnkAccueil = $("#lnkAccueil");
    var lnkValiderRv = $("#lnkValiderRv");
    var lnkRetourAgenda = $("#lnkRetourAgenda");
    // li si inserisce in un dizionario
    var options = {
        "lnkAccueil" : lnkAccueil,
        "lnkAfficherAgenda" : lnkAfficherAgenda,
        "lnkValiderRv" : lnkValiderRv,
        "lnkRetourAgenda" : lnkRetourAgenda
    }
    // si nascondono tutti i link
    for ( var key in options) {
        options[key].hide();
    }
    // si visualizzano quelli richiesti
    for (var i = 0; i < show.length; i++) {
        var option = show[i];
        options[option].show();
    }
}
  • righe 2-18: la funzione di inizializzazione della pagina;
  • riga 4: per visualizzare il pulsante con l'elenco delle lingue;
  • righe 6-7: l'immagine animata è nascosta;
  • righe 26-48: una funzione [setMenu] che permette di indicare quali opzioni devono essere visibili;

Andiamo nella console di sviluppo (Ctrl-Maiusc-I) e inseriamo il seguente codice [1]:

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

8.6.4.6. Esempio n. 5: un menu a tendina

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

La novità è in [1]. Qui utilizziamo un componente fornito al di fuori di Bootstrap, [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].

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


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

La vista [choixmedecin.xml] è la seguente:


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

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


...
function afficherAgenda() {
    var idMedecin = $('#idMedecin option:selected').val();
    showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}

function initChoixMedecin() {
    // il menu a tendina dei medici
    $('#idMedecin').selectpicker();
    // il menu
    setMenu([ "lnkAfficherAgenda" ]);
}
  • righe 7-12: la funzione eseguita al caricamento della pagina;
  • riga 9: l'istruzione che trasforma il [select] della pagina in un menu a tendina Bootstrap. [$('#idMedecin')] fa riferimento al [select] (riga 7 della vista [choixmedecin]) e la funzione JS [selectpicker] proviene dal file JS [bootstrap-select.js];
  • riga 11: viene visualizzata solo una delle opzioni del menu;
  • righe 2-5: la funzione JS viene eseguita quando si fa clic sull’opzione di menu [Agenda];
  • riga 3: si recupera il valore dell'opzione selezionata nell'elenco a discesa: [$('#idMedecin option:selected')] individua innanzitutto il componente [id=idMedecin] e poi, all'interno di tale componente, l'opzione selezionata. L'operazione [..].val() recupera quindi il valore dell'elemento trovato, ovvero l'attributo [value] dell'opzione selezionata;

Ecco un esempio di selezione di un medico:

 

8.6.4.7. Esempio n. 6: un calendario

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

Image

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

 

Grazie al pulsante «Elenco delle lingue», è possibile impostare il calendario (e solo il calendario) in inglese:

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>
        <!-- script locale -->
        <script type="text/javascript" src="resources/js/bs-06.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- barra di navigazione -->
            <div th:include="navbar3"></div>
            <!-- Jumbotron Bootstrap -->
            <div th:include="jumbotron"></div>
            <!-- contenuto -->
            <div id="content" th:include="choixmedecinjour">
            </div>
            <!-- informazioni -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • riga 8: il file CSS del componente [bootstrap-datepicker];
  • riga 16: il file JS del componente [bootstrap-datepicker];
  • riga 17: il file JS per la gestione di un calendario francese. Per impostazione predefinita, è in inglese;
  • riga 15: il file JS di una libreria denominata [moment] che consente di accedere a numerose funzioni di calcolo del tempo [http://momentjs.com/];
  • riga 28: la vista del calendario;

La vista [choixmedecinjour.xml] è la seguente:


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

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


...
var calendar_infos = {};

function initChoixMedecinJour() {
    // calendario
    var calendar_container = $("#calendar_container");
    calendar_infos = {
        "container" : calendar_container,
        "html" : calendar_container.html(),
        "today" : moment().format('YYYY-MM-DD'),
        "langue" : "fr"
    }
    // creazione calendario
    updateCalendar();
    // il menu a tendina dei medici
    $('#idMedecin').selectpicker();
    $('#idMedecin').change(function(e) {
        afficherAgenda();
    })
    // il menu
    setMenu([]);
}
  • riga 2: il calendario è gestito da diverse funzioni JS. La variabile [calendar_infos] raccoglierà le informazioni relative al calendario. È una variabile globale, in modo da essere visibile alle diverse funzioni;
  • riga 6: si identifica il contenitore del calendario;
  • righe 7-12: le informazioni memorizzate relative al calendario;
    • riga 8: un riferimento al suo contenitore,
    • riga 9: il codice HTML del calendario. Con queste due informazioni, è possibile eliminare il calendario e rigenerarlo,
    • riga 10: la data odierna nel formato [aaaa-mm-jj],
    • riga 11: la lingua del calendario;
  • riga 14: creazione del calendario;
  • riga 16: il menu a tendina dei medici;
  • righe 17-19: ogni volta che il valore selezionato in questo menu a tendina cambierà, verrà eseguito il metodo [afficherAgenda];
  • riga 21: nessun menu nella barra di navigazione;

La funzione [updateCalendar] è la seguente:


function updateCalendar(renew) {
    if (renew) {
        // aggiornamento del calendario attuale
        calendar_infos.container.html(calendar_infos.html);
    }
    // inizializzazione del calendario
    var calendar = $("#calendar");
    var settings = {
        format : "yyyy-mm-dd",
        startDate : calendar_infos.today,
        language : calendar_infos.langue,
    };
    calendar.datepicker(settings);
    // selezione della data corrente
    if (calendar_infos.date) {
        calendar.datepicker('setDate', calendar_infos.date)
    }
    // eventi
    calendar.datepicker().on('hide', function(e) {
        // visualizzazione del giorno selezionato
        displayJour();
    });
    calendar.datepicker().on('changeDate', function(e) {
        // si annota la nuova data
        calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
        // visualizzazione informazioni agenda
        afficherAgenda();
        // visualizzazione del giorno selezionato
        displayJour();
    });
    // visualizzazione del giorno selezionato
    displayJour();
}
  • riga 1: la funzione [updateCalendar] accetta un parametro che può essere presente o meno. Se è presente, il calendario viene rigenerato (riga 4) in base alle informazioni contenute in [calendar_infos];
  • riga 7: si fa riferimento al calendario;
  • righe 8-12: i suoi parametri di inizializzazione;
    • riga 9: il formato delle date gestite da [aaaa-mm-jj],
    • riga 10: la prima data selezionabile nel calendario. In questo caso, la data odierna. Le date precedenti non potranno essere selezionate,
    • riga 11: la lingua del calendario. Ce ne saranno due: ['en'] e ['fr'];
  • riga 13: il calendario è configurato;
  • righe 15-17: se è stata inizializzata la data [calendar_infos], allora si imposta tale data come data corrente del calendario;
  • righe 19-22: ogni volta che il calendario si chiuderà, verrà visualizzata la data selezionata;
  • righe 23-30: ogni volta che si verifica un cambiamento di data nel calendario:
    • riga 25: si registra la data selezionata in [calendar_infos],
    • riga 27: si visualizzano le informazioni sull'agenda,
    • riga 29: viene visualizzato il giorno selezionato;
  • riga 32: visualizzazione del giorno selezionato, se presente;

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


// visualizza il giorno selezionato
function displayJour() {
    if (calendar_infos.date) {
        var displayjour = $("#displayjour");
        moment.locale(calendar_infos.langue);
        jour = moment(calendar_infos.date).format('LL');
        displayjour.val(jour);
    }
}
  • riga 3: se è già stata selezionata una data (all'inizio il calendario non ha una data selezionata);
  • riga 4: si individua il componente in cui verrà inserita la data;
  • riga 5: questa data può essere scritta in inglese o in francese. Si imposta la lingua della libreria [moment];
  • riga 6: si visualizza la data selezionata nella lingua scelta e nel formato lungo;
  • riga 7: questa data viene visualizzata;

Ecco due esempi:

Quando si cambia medico o data, viene eseguito il metodo [afficherAgenda]:


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

8.6.4.8. Esempio n. 7: una tabella HTML "responsive"

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

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

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

Se si riduce la dimensione della finestra del browser, si ottiene quanto segue:

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

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


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

La vista [agenda.xml] è la seguente:


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

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


@CHARSET "UTF-8";

#intervalli th {
    text-align: center;
}

#intervalli td {
    text-align: center;
    font-weight: bold;
}

.status-metro {
  display: inline-block;
  padding: 2px 5px;
  color:#fff;
}

.status-metro.status-active {
  background: #43c83c;
}

.status-metro.status-suspended {
  background: #fa3031;
}

Gli stili [status-*] provengono da un esempio di utilizzo della tabella [footable] trovato sul sito della biblioteca.

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


function initAgenda() {
    // la tabella degli intervalli orari
    $("#creneaux").footable();
}

Questo è tutto. [$("#creneaux")] fa riferimento alla tabella HTML che si desidera rendere «responsive». Inoltre, si trovano le funzioni JS associate ai due link [Réserver] e [Supprimer]:


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

function supprimer(idRv) {
    showInfo("Suppression du rv n° " + idRv);
}

8.6.4.9. Esempio n. 8: una finestra modale

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

 

Image

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

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>
        <!-- script locale -->
        <script type="text/javascript" src="resources/js/bs-08.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- barra di navigazione -->
            <div th:include="navbar3" />
            <!-- Jumbotron Bootstrap -->
            <div th:include="jumbotron" />
            <!-- contenuto -->
            <div id="content" th:include="choixmedecinjour" />
            <div id="agenda" th:include="agenda-modal" />
            <div th:include="resa" />
            <!-- informazioni -->
            <div class="alert alert-success">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • riga 19: il file JS necessario per le finestre modali;
  • riga 32: la vista [agenda-modal] è identica alla vista [agenda] tranne che per un dettaglio: la funzione JS che gestisce il collegamento [Réserver]:

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

La funzione [showDialogResa] ha il compito di visualizzare la finestra modale di selezione di un cliente;

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

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

Il risultato è la seguente visualizzazione:

 

Da notare che la finestra modale non viene visualizzata per impostazione predefinita. Per questo motivo, non è visibile all’avvio dell’applicazione, sebbene il suo codice HTML sia presente nel documento.

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


var idCreneau;
var idClient;
var resa;

function showDialogResa(idCreneau) {
    // si memorizza l'ID della fascia oraria
    this.idCreneau = idCreneau;
    // viene visualizzata la finestra di dialogo di prenotazione
    var resa = $("#resa");
    resa.modal('show');
    // log
    showInfo("Réservation du créneau n° " + idCreneau);
}

function cancelDialogResa() {
    // si nasconde la finestra di dialogo
    resa.modal('hide');
}

// conferma della prenotazione
function validateResa() {
    // si recuperano le informazioni
    var idClient = $('#idClient option:selected').val();
    // si nasconde la finestra di dialogo
    resa.modal('hide');
    // informazioni
    showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}

function initResa() {
    // il menu a tendina dei clienti
    $('#idClient').selectpicker();
    // finestra modale
    resa = $("#resa");
    resa.modal({});    
}
  • righe 30-36: la funzione di inizializzazione della finestra modale;
  • riga 32: la finestra modale contiene un menu a tendina che deve essere inizializzato;
  • righe 34-35: inizializzazione della finestra modale stessa;
  • righe 5-13: la funzione JS associata al link [Réserver];
  • riga 7: si memorizza il parametro della funzione nella variabile globale della riga 1;
  • righe 9-10: la finestra modale viene resa visibile;
  • riga 12: si registra un'informazione nella finestra di informazioni;
  • righe 15-18: gestione del pulsante [Annuler]. Ci si limita a nascondere la finestra modale (riga 17);
  • righe 21-31: la funzione JS associata al pulsante [Valider];
  • riga 23: si recupera l'attributo [value] del cliente selezionato;
  • riga 25: si nasconde la finestra di dialogo;
  • riga 27: si registrano le due informazioni: il numero della fascia oraria prenotata e il nome del cliente;

8.6.5. Fase 2: scrittura delle viste

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

  

8.6.5.1. La vista [navbar-start]

Visualizza la barra di navigazione della pagina iniziale:

Image

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


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

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

evt
gestore
clic sul pulsante di connessione
connecter() - ligne 27
clic sul link [Français]
setLang('fr') - ligne 37
clic sul link [English]
setLang('en') - ligne 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">
    <!-- Jumbotron di Bootstrap -->
    <div class="jumbotron">
        <div class="row">
            <div class="col-md-2">
                <img src="resources/images/caduceus.jpg" alt="RvMedecins" />
            </div>
            <div class="col-md-10">
                <h1 th:utext="#{application.header}" />
            </div>
        </div>
    </div>
</section>

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

8.6.5.3. La vista [login]

È la vista visualizzata sotto il jumbotron nella pagina di avvio:

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é modelli né eventi.

8.6.5.4. La vista [navbar-run]

Questa è la barra di navigazione visualizzata quando l'accesso è andato a buon fine:

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" />
                <!-- pulsanti a destra -->
                <form class="navbar-form navbar-right" role="form">
                    <!-- disconnessione -->
                    <button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
                    <!-- lingue -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger" th:text="#{langues}">Langue</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
                            </li>
                            <li>
                                <a href="javascript:setLang('en')" th:text="#{langues.en}" />
                            </li>
                        </ul>
                    </div>
                </form>
            </div>
        </div>
    </div>
    <!-- inizializzazione pagina -->
    <script th:inline="javascript">
        /*<![CDATA[*/
             // si sta inizializzando la pagina
            initNavBarRun();
        /*]]>*/
    </script>
</section>

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

evt
gestore
clic sul pulsante di disconnessione
deconnecter() - ligne 19
clic sul link [Français]
setLang('fr') - ligne 29
clic sul link [English]
setLang('en') - ligne 32

8.6.5.5. La vista [accueil]

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

Image

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


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

Il suo modello è il seguente:

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

Nella sua forma attuale, la vista non sembra disporre di un gestore di eventi. In realtà questi sono definiti nella funzione [initChoixMedecinJour]. Questa funzione è stata presentata al paragrafo 8.6.4.7, a pagina 466 e in particolare a pagina 469. Vi si trovano i seguenti gestori di eventi:

evt
gestore
scelta di un medico
getAgenda
scelta di una data
getAgenda

8.6.5.6. La vista [agenda]

La vista [agenda] mostra una giornata dell'agenda di un medico:

Image

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


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

Il modello di questa vista contiene un solo elemento:

  • [agenda] (riga 4): un modello piuttosto complesso creato appositamente per la visualizzazione dell'agenda;

Dispone dei seguenti gestori di eventi:

evt
gestore
clic sul pulsante [Supprimer]
supprimerRv(idRv) - ligne 37
clic sul link [Réserver]
reserverCreneau(idCreneau) - ligne 34

La vista [resa] della riga 47 è quella visualizzata quando l'utente clicca su un link [Réserver]:

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">Titolo modale</h4> -->
                    </div>
                    <div class="modal-body">
                        <div class="alert alert-info">
                            <h3>
                                <span th:text="#{resa.titre}">Prise de rendez-vous</span>
                            </h3>
                        </div>
                        <div class="row">
                            <div class="col-md-3">
                                <h2 th:text="#{resa.client}">Client</h2>
                                <select name="idClient" id="idClient" class="combobox" data-style="btn-primary">
                                    <option th:each="clientItem : ${clientItems}" th:text="${clientItem.texte}" th:value="${clientItem.id}" />
                                </select>
                            </div>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()" th:text="#{resa.annuler}">Annuler</button>
                        <button type="button" class="btn btn-primary" onclick="javascript:validerRv()" th:text="#{resa.valider}">Valider</button>
                    </div>
                </div><!-- /.modal-content -->
            </div><!-- /.modal-dialog -->
        </div><!-- /.modal -->
        <!-- inizializza la pagina -->
        <script th:inline="javascript">
            /*<![CDATA[*/
             // si inizializza la pagina
            initResa();
        /*]]>*/
        </script>
    </body>
</html>

Il suo modello contiene un solo elemento:

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

Dispone dei seguenti gestori di eventi:

evt
gestore
clic sul pulsante [Annuler]
cancelDialogResa() - ligne 30
clic sul pulsante [Valider]
validerRv() - ligne 31

8.6.5.7. La vista [erreurs]

Questa è la vista che viene visualizzata se l'azione richiesta dall'utente non è andata a buon fine:

Image

Il codice [erreurs.xml] è il seguente:


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

Il suo modello contiene un solo elemento:

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

La vista non dispone di un gestore di eventi.

8.6.5.8. Résumé

La tabella seguente riporta le viste e i relativi modelli:

vista
modello
gestori di eventi
navbar-start

connecter, setLang
jumbotron


login


navbar-run

deconnecter, setLang
home
rdvmedecins.medecinItems (liste des médecins)
getAgenda
agenda
agenda (une journée de l'agenda)
supprimerRv, reserverCreneau
Prenotazione
clientItems (liste des clients)
cancelDialogResa, validerRv
errori
erreurs (liste d'erreurs)

8.6.6. Fase 3: scrittura delle azioni

Torniamo all'architettura del servizio web [Web1]:

Vedremo ora quali URL sono esposte da [Web1] e la loro implementazione:

8.6.6.1. Le URL esposte dal servizio [Web1]

Sono le seguenti:

  • una URL per ciascuna delle viste precedenti o una loro combinazione;
  • una URL per aggiungere una RV;
  • una URL per eliminare un RV;

Tutte restituiscono una risposta del tipo [Reponse] come segue:


public class Reponse {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // la barra di navigazione
    private String navbar;
    // il jumbotron
    private String jumbotron;
    // il corpo della pagina
    private String content;
    // l'agenda
    private String agenda;
...
}
  • riga 5: uno stato della risposta: 1 (OK), 2 (errore);
  • riga 7: il flusso HTML delle viste [navbar-start] o [navbar-run] a seconda dei casi;
  • riga 9: il flusso HTML della vista [jumbotron];
  • riga 13: il flusso HTML della vista [agenda];
  • riga 9: il flusso HTML delle viste [accueil], [erreurs], [login] a seconda dei casi;

Le URL riportate sono le seguenti

/getNavbarStart
inserisce la vista [navbar-start] in [Reponse.navbar]
/getNavbarRun
inserisce la vista [navbar-run] in [Reponse.navbar]
/getAccueil
inserisce la vista [accueil] in [Reponse.content]
/getJumbotron
inserisce la vista [jumbotron] in [Reponse.jumbotron]
/getAgenda
inserisce la vista [agenda] in [Reponse.agenda]
/getLogin
inserisce la vista [login] in [Reponse.content]
/getNavbarRunJumbotronAccueil
  • se la connessione va a buon fine, inserisce la vista [navbar-run] in [Reponse.navbar], la vista [jumbotron] nella vista [Reponse.jumbotron], la vista [accueil] nella vista [Reponse.content]
  • in caso di connessione non riuscita, inserisce la vista [erreurs] in [Reponse.content] e imposta [Reponse.status] a 2
/getNavbarRunJumbotronAccueilAgenda
inserisce la vista [navbar-run] in [Reponse.navbar], la vista [jumbotron] in [Reponse.jumbotron], la vista [accueil] nella vista [Reponse.content], la vista [agenda] nella vista [Reponse.agenda]
/ajouterRv
aggiunge l'appuntamento selezionato e inserisce la nuova agenda in [Reponse.agenda]
/supprimerRv
elimina l'appuntamento selezionato e inserisce il nuovo calendario in [Reponse.agenda]

8.6.6.2. Il singleton [ApplicationModel]

 

La classe [ApplicationModel] viene istanziata in un unico esemplare e iniettata nel controller dell'applicazione. Il suo codice è il seguente:


package rdvmedecins.springthymeleaf.server.models;

import java.util.ArrayList;
...

@Component
public class ApplicationModel implements IDao {

....
}
  • riga 6: [ApplicationModel] è un componente Spring;
  • riga 7: che implementa l’interfaccia del livello [DAO]. Lo facciamo affinché le azioni non debbano conoscere il livello [DAO], ma solo il singleton [ApplicationModel]. L’architettura di [Web1] diventa quindi la seguente:

Torniamo al codice della classe [ApplicationModel]:


package rdvmedecins.springthymeleaf.server.models;

import java.util.ArrayList;
...

@Component
public class ApplicationModel implements IDao {

    // il livello [DAO]
    @Autowired
    private IDao dao;
    // la configurazione
    @Autowired
    private AppConfig appConfig;

    // dati provenienti dal livello [DAO]
    private List<ClientItem> clientItems;
    private List<MedecinItem> medecinItems;
    // dati di configurazione
    private String userInit;
    private String mdpUserInit;
    private boolean corsAllowed;
    // eccezione
    private RdvMedecinsException rdvMedecinsException;

    // produttore
    public ApplicationModel() {
    }

    @PostConstruct
    public void init() {
        // configurazione
        userInit = appConfig.getUSER_INIT();
        mdpUserInit = appConfig.getMDP_USER_INIT();
        dao.setTimeout(appConfig.getTIMEOUT());
        dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
        corsAllowed = appConfig.isCORS_ALLOWED();
        // si memorizzano nella cache gli elenchi a discesa dei medici e dei clienti
        List<Medecin> medecins = null;
        List<Client> clients = null;
        try {
            medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
            clients = dao.getAllClients(new User(userInit, mdpUserInit));
        } catch (RdvMedecinsException ex) {
            rdvMedecinsException = ex;
        }
        if (rdvMedecinsException == null) {
            // si creano gli elementi degli elenchi a discesa
            medecinItems = new ArrayList<MedecinItem>();
            for (Medecin médecin : medecins) {
                medecinItems.add(new MedecinItem(médecin));
            }
            clientItems = new ArrayList<ClientItem>();
            for (Client client : clients) {
                clientItems.add(new ClientItem(client));
            }
        }
    }

    // getter e setter
    ...

    // implementazione dell'interfaccia [IDao]
    @Override
    public void setUrlServiceWebJson(String url) {
        dao.setUrlServiceWebJson(url);
    }

    @Override
    public void setTimeout(int timeout) {
        dao.setTimeout(timeout);
    }

    @Override
    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
        return dao.ajouterRv(user, jour, idCreneau, idClient);
    }

    ...
}
  • riga 11: iniezione del riferimento all’implementazione del livello [DAO]. È poi questo riferimento che viene utilizzato per implementare l’interfaccia [IDao] (righe 64-80);
  • riga 14: inserimento della configurazione dell'applicazione;
  • righe 33-37: utilizzo di questa configurazione per configurare vari elementi dell’architettura dell’applicazione;
  • righe 38-46: si memorizzano nella cache le informazioni che alimenteranno gli elenchi a discesa dei medici e dei clienti. Si parte quindi dal presupposto che, se un medico o un cliente cambia, l’applicazione debba essere riavviata. L’idea qui è quella di mostrare che un singleton Spring può fungere da cache per l’applicazione web;

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


package rdvmedecins.springthymeleaf.server.models;

import rdvmedecins.client.entities.Personne;

public class PersonneItem {

    // elemento di un elenco
    private Long id;
    private String texte;

    // costruttore
    public PersonneItem() {

    }

    public PersonneItem(Personne personne) {
        id = personne.getId();
        texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
    }

    // getter e setter
...
}
  • riga 8: il campo [id] sarà il valore dell'attributo [value] di un'opzione dell'elenco a discesa;
  • riga 9: il campo [texte] sarà il testo visualizzato da un'opzione dell'elenco a discesa;

8.6.6.3. La classe [BaseController]

 

La classe [BaseController] è la classe padre dei controller [RdvMedecinsController] e [RdvMedecinsCorsController]. Non era obbligatorio creare questa classe padre. In essa sono stati raggruppati i metodi di utilità della classe [RdvMedecinsController], che non sono fondamentali tranne uno. È possibile classificarli in tre gruppi:

  1. i metodi di utilità;
  2. i metodi che rendono le viste fuse con i relativi modelli;
  3. il metodo di inizializzazione di un'azione

protected List<String>
getErreursForException(Exception exception)

protected List<String>
getErreursForModel(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 getPartialViewAccueil(WebContext
thymeleafContext)
genera la vista [accueil] senza modello

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

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

protected Reponse getViewErreurs(WebContext thymeleafContext, List<String> erreurs)
restituisce la risposta al cliente quando l'azione richiesta si è conclusa con un errore

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

Esaminiamo due di questi metodi.

Il metodo [getPartialViewAgenda] genera la vista più complessa, quella dell’agenda. Il suo codice è il seguente:


    // flusso [agenda]
    protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
        // contesti
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        // si costruisce il modello della pagina [agenda]
        ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
        // l'agenda con il relativo modello
        thymeleafContext.setVariable("agenda", modelAgenda);
        thymeleafContext.setVariable("clientItems", application.getClientItems());
        return engine.process("agenda", thymeleafContext);
}
  • righe 9-10: i due elementi del modello dell'agenda:
    • riga 9: l'agenda visualizzata.
    • riga 10: l’elenco dei clienti visualizzato quando l’utente fissa un appuntamento;

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


// modello della pagina [Agenda]
    private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
        // il titolo della pagina
        String dateFormat = springContext.getMessage("date.format", null, locale);
        Medecin médecin = agenda.getMedecin();
        String titre = springContext.getMessage("agenda.titre", new String[] { médecin.getTitre(), médecin.getPrenom(),
                médecin.getNom(), new SimpleDateFormat(dateFormat).format(agenda.getJour()) }, locale);
        // gli orari disponibili per la prenotazione
        ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
        int i = 0;
        for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
            // fascia oraria del medico
            Creneau créneau = creneauMedecinJour.getCreneau();
            ViewModelCreneau modelCréneau = new ViewModelCreneau();
            modelCréneaux[i] = modelCréneau;
            // ID
            modelCréneau.setId(créneau.getId());
            // fascia oraria
            modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
                    créneau.getHfin(), créneau.getMfin()));
            Rv rv = creneauMedecinJour.getRv();
            // cliente e ordine
            String commande;
            if (rv == null) {
                modelCréneau.setClient("");
                commande = springContext.getMessage("agenda.reserver", null, locale);
                modelCréneau.setCommande(commande);
                modelCréneau.setAction(ViewModelCreneau.ACTION_RESERVER);

            } else {
                Client client = rv.getClient();
                modelCréneau.setClient(String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom()));
                commande = springContext.getMessage("agenda.supprimer", null, locale);
                modelCréneau.setCommande(commande);
                modelCréneau.setIdRv(rv.getId());
                modelCréneau.setAction(ViewModelCreneau.ACTION_SUPPRIMER);
            }
            // fascia oraria successiva
            i++;
        }
        // si restituisce il modello dell'agenda
        ViewModelAgenda modelAgenda = new ViewModelAgenda();
        modelAgenda.setTitre(titre);
        modelAgenda.setCreneaux(modelCréneaux);
        return modelAgenda;
    }
  • riga 6: l'agenda ha un titolo:

Image

oppure:

Image

Si nota che il formato della data dipende dalla lingua. Questo formato viene recuperato dai file dei messaggi (riga 4).

  • righe 11-40: per ogni fascia oraria, occorre visualizzare la vista:

Image

oppure la vista:

Image

  • righe 19-20: visualizzano la fascia oraria;
  • righe 25-28: nel caso in cui la fascia oraria sia libera. In tal caso, occorre visualizzare il pulsante [Réserver];
  • righe 31-36: il caso in cui la fascia oraria sia occupata. In questo caso occorre visualizzare sia il cliente che il pulsante [Supprimer];

L’altro metodo su cui forniamo ulteriori spiegazioni è il metodo [getActionContext]. Viene chiamato all’inizio di ciascuna delle azioni di [RdvMedecinsController]. La sua firma è la seguente:


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

Restituisce il seguente tipo [ActionContext]:


public class ActionContext {

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

I suoi parametri sono i seguenti:

  • [lang]: la lingua richiesta per l'azione è "en" o "fr";
  • [origin]: l'intestazione HTTP [origin] nel caso di una chiamata tra domini;
  • [request]: la richiesta HTTP in fase di elaborazione, ciò che da tempo viene definita un'azione;
  • [response]: la risposta che verrà fornita a tale richiesta;
  • [result]: ogni azione di [RdvMedecinsController] riceve un valore inviato, di cui si verifica la validità. [result] è il risultato di tale verifica;
  • [rdvMedecinsController]: il controller contenitore delle azioni;

Il metodo [getActionContext] è implementato come segue:


    // contesto di un'azione
    protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
        // lingua?
        if (lang == null) {
            lang = "fr";
        }
        // locale
        Locale locale = null;
        if (lang.trim().toLowerCase().equals("fr")) {
            // francese
            locale = new Locale("fr", "FR");
        } else {
            // tutto il resto in inglese
            locale = new Locale("en", "US");
        }
        // intestazioni CORS
        rdvMedecinsCorsController.sendOptions(origin, response);
        // ActionContext
        ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
        // errori di inizializzazione
        RdvMedecinsException e = application.getRdvMedecinsException();
        if (e != null) {
            actionContext.setErreurs(e.getMessages());
            return actionContext;
        }
        // errori di POST?
        if (result != null && result.hasErrors()) {
            actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
            return actionContext;
        }
        // nessun errore
        return actionContext;
}
  • righe 3-15: a partire dal parametro [lang], si imposta la locale dell’azione;
  • riga 17: si inviano le intestazioni HTTP necessarie per le richieste interdominio. Non entriamo nei dettagli. La tecnica utilizzata è quella descritta nel paragrafo 8.4.14;
  • riga 19: creazione di un oggetto [ActionContext] senza errori;
  • riga 21: abbiamo visto al paragrafo 8.6.6.2 che il singleton [ApplicationModel] accedeva al database per recuperare sia i clienti che i medici. Tale accesso può fallire. Si memorizza quindi l’eccezione che si verifica. Alla riga 21, recuperiamo questa eccezione;
  • righe 22-25: se si è verificata un’eccezione all’avvio dell’applicazione, non è possibile eseguire alcuna azione. Si restituisce quindi, per qualsiasi azione, un oggetto [ActionContext] contenente i messaggi di errore dell’eccezione;
  • righe 27-20: si analizza il parametro [result] per verificare se il valore inviato fosse valido o meno. Se non era valido, si restituisce un oggetto [ActionContext] con i messaggi di errore appropriati;
  • riga 32: caso senza errori;

Esaminiamo ora le azioni del controller [RdvMedecinsController]

8.6.6.4. L'azione [/getNavBarStart]

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


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

Restituisce il tipo [Reponse] seguente:


public class Reponse {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // la barra di navigazione
    private String navbar;
    // il jumbotron
    private String jumbotron;
    // il corpo della pagina
    private String content;
    // l'agenda
    private String agenda;
...
}

e presenta i seguenti parametri:

  • [PostLang postlang]: il valore inviato è il seguente:

public class PostLang {

    // data
    @NotNull
    private String lang;
...
}

La classe [PostLang] è la classe padre di tutti i valori inviati. Infatti, il cliente deve sempre specificare la lingua in cui deve essere eseguita l'azione.

Il metodo [getNavbarStart] è implementato come segue:


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

Di seguito descriviamo solo le novità.

8.6.6.5. L'azione [/getNavbarRun]

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


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

L'azione può restituire due tipi di risposta:

  • la risposta con errore (righe 10-13):
 {"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) {
        // contesti dell'azione
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // errori?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // si restituisce la vista [jumbotron]
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        return reponse;
}

L'azione può restituire due tipi di risposta:

  • la risposta con errore (righe 10-13):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • la 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] restituisce la vista [login]:


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

L'azione può restituire due tipi di risposta:

  • la risposta con errore (righe 9-11):
 {"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 [/getAccueil]

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


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

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

Il codice di implementazione è il seguente:


    @RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // contesti dell'azione
        ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // errori?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // la vista [accueil] è protetta
        try{
            // utente
            User user = postUser.getUser();
            // si stanno verificando le credenziali [userName, password]
            application.authenticate(user);
        }catch(RdvMedecinsException e){
            // viene restituito un errore
            return getViewErreurs(thymeleafContext, e.getMessages());
        }
        // viene restituita la vista [accueil]
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        return reponse;
}
  • righe 15-22: si noti che la pagina [accueil] è protetta e che quindi l'utente deve essere autenticato;

L'azione può restituire due tipi di risposta:

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

8.6.6.9. L'azione [/getNavbarRunJumbotronAccueil]

L'azione [/getNavbarRunJumbotronAccueil] genera le viste [navbar-run, jumbotron, accueil]. Ha la seguente firma:


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

L'implementazione dell'azione è la seguente:


// navbar + jumbotron + home
    @RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // contesti dell'azione
        ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
                rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // errori?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // la vista [accueil] è protetta
        try {
            // utente
            User user = postUser.getUser();
            // si stanno verificando le credenziali [userName, password]
            application.authenticate(user);
        } catch (RdvMedecinsException e) {
            // viene restituito un errore
            return getViewErreurs(thymeleafContext, e.getMessages());
        }
        // si invia la risposta
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        return reponse;
    }

L'azione può restituire due tipi di risposta:

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

8.6.6.10. L'azione [/getAgenda]

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


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

public class PostGetAgenda extends PostUser {

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

L'implementazione è la seguente:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // contesti dell'azione
        ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result,    rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        Locale locale = actionContext.getLocale();
        // errori?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // si verifica la validità del POST
        if (result != null) {
            new PostGetAgendaValidator().validate(postGetAgenda, result);
            if (result.hasErrors()) {
                // si restituisce la vista [erreurs]
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        ...
}
  • fino alla riga 14, il codice è ormai classico;
  • righe 16-21: si esegue un controllo aggiuntivo sul valore inviato. La data deve essere successiva o uguale a quella odierna. Per verificarlo si utilizza un validatore:

package rdvmedecins.web.validators;

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

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import rdvmedecins.springthymeleaf.server.requests.PostGetAgenda;
import rdvmedecins.springthymeleaf.server.requests.PostValiderRv;

public class PostGetAgendaValidator implements Validator {

    public PostGetAgendaValidator() {
    }

    @Override
    public boolean supports(Class<?> classe) {
        return PostGetAgenda.class.equals(classe) || PostValiderRv.class.equals(classe);
    }

    @Override
    public void validate(Object post, Errors errors) {
        // il giorno scelto per l'appuntamento
        Date jour = null;
        if (post instanceof PostGetAgenda) {
            jour = ((PostGetAgenda) post).getJour();
        } else {
            if (post instanceof PostValiderRv) {
                jour = ((PostValiderRv) post).getJour();
            }
        }
        // si convertono le date nel formato yyyy-MM-dd
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String strJour = sdf.format(jour);
        String strToday = sdf.format(new Date());
        // il giorno scelto non deve essere precedente alla data odierna
        if (strJour.compareTo(strToday) < 0) {
            errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
        }
    }

}
  • riga 19: il validatore opera su due classi: [PostGetAgenda] e [PostValiderRv];

Torniamo al codice dell’azione [/getAgenda]:


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

8.6.6.11. L'azione [/getNavbarRunJumbotronAccueilAgenda]

L'azione [/getNavbarRunJumbotronAccueilAgenda] restituisce le viste [navbar-run, jumbotron, accueil, agenda]. La sua implementazione è la seguente:


    @RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
            HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // contesti dell'azione
        ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // errori?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // agenda
        Reponse agenda = getAgenda(post, result, request, response, null);
        if (agenda.getStatus() != 1) {
            return agenda;
        }
        // si invia la risposta
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        reponse.setAgenda(agenda.getAgenda());
        return reponse;
}
  • righe 15-18: si sfrutta l'esistenza dell'azione [/getAgenda] per richiamarla. Successivamente si esamina il status della risposta (riga 16). Se si rileva un errore, non si procede oltre e si restituisce la risposta;
  • riga 20: si inviano le viste richieste:
 {"status":1,"navbar": navbar-run, "jumbotron": jumbotron, "agenda":agenda, "content":accueil}

8.6.6.12. L'azione [/supprimerRv]

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


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

public class PostSupprimerRv extends PostUser {

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

L'implementazione dell'azione è la seguente:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // contesti dell'azione
        ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
                rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // errori?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // valori inviati
        User user = postSupprimerRv.getUser();
        long idRv = postSupprimerRv.getIdRv();
        // si elimina l'appuntamento
        AgendaMedecinJour agenda = null;
        try {
            // lo si recupera
            Rv rv = application.getRvById(user, idRv);
            Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
            long idMedecin = creneau.getIdMedecin();
            Date jour = rv.getJour();
            // si elimina l'appuntamento associato
            application.supprimerRv(user, idRv);
            // si rigenera l'agenda del medico
            agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // si restituisce la nuova agenda
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // si restituisce la vista [erreurs]
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // si restituisce la vista [erreurs]
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
}
  • riga 22: si recupera l’appuntamento da eliminare. Se non esiste, si genera un’eccezione;
  • righe 23-25: a partire da questo appuntamento, si individuano il medico e il giorno in questione. Queste informazioni sono necessarie per rigenerare l’agenda del medico;
  • riga 27: l’appuntamento viene eliminato;
  • riga 29: si richiede il nuovo calendario del medico. Questo è importante. Oltre alla fascia oraria appena liberata, altri utenti dell’applicazione potrebbero aver apportato modifiche al calendario. È importante restituire all’utente la versione più recente dello stesso;
  • righe 31-34: si restituisce l'agenda:
 {"status":1,"navbar": null, "jumbotron": null, "agenda":agenda, "content":null}

8.6.6.13. L'azione [/validerRv]

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


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

public class PostValiderRv extends PostUser {

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

L'implementazione dell'azione è la seguente:


// conferma di un appuntamento
    @RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // contesti dell'azione
        ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebApplicationContext springContext = actionContext.getSpringContext();
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // errori?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // si verifica la validità della data dell'appuntamento
        if (result != null) {
            new PostGetAgendaValidator().validate(postValiderRv, result);
            if (result.hasErrors()) {
                // si restituisce la vista [erreurs]
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        // valori inviati
        User user = postValiderRv.getUser();
        long idClient = postValiderRv.getIdClient();
        long idCreneau = postValiderRv.getIdCreneau();
        Date jour = postValiderRv.getJour();
        // azione
        try {
            // si recuperano le informazioni sulla fascia oraria
            Creneau créneau = application.getCreneauById(user, idCreneau);
            long idMedecin = créneau.getIdMedecin();
            // si aggiunge l'appuntamento
            application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
            // si aggiorna l'agenda
            AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
                    new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // si restituisce la nuova agenda
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // si restituisce la vista [erreurs]
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // si restituisce la vista [erreurs]
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
    }
}

Il codice è analogo a quello dell'azione [/supprimerRv].

8.6.7. Fase 4: test del server Spring/Thymeleaf

Ora testeremo le diverse azioni precedenti con il plugin Chrome [Advanced Rest Client] (cfr. paragrafo 9.6).

8.6.7.1. Configurazione dei test

Tutte le azioni richiedono un valore inviato tramite POST. Invieremo alcune varianti della stringa jSON seguente:

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

Questo valore inviato contiene informazioni superflue per la maggior parte delle azioni. Tuttavia, tali informazioni vengono ignorate dalle azioni che le ricevono e non causano errori. Questo valore inviato ha il vantaggio di coprire i diversi valori da inviare.

8.6.7.2. L'azione [/getNavbarStart]

  • in [1], l'azione testata;
  • in [2], il valore inviato;
  • in [3], il valore inserito è una stringa jSON;
  • in [4], la vista [navbar-start] viene richiesta in inglese;

Il risultato ottenuto è il seguente:

 

Abbiamo ricevuto la vista [navbar-start] in inglese (campi evidenziati).

Ora commettiamo un errore. Modifichiamo il valore dell’attributo [lang] da quello inviato a null. Riceviamo il seguente risultato:

 

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

8.6.7.3. L'azione [/getNavbarRun]

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


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

Il risultato ottenuto è il seguente:

 

8.6.7.4. L'azione [/getJumbotron]

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


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

Il risultato ottenuto è il seguente:

 

8.6.7.5. L'azione [/getLogin]

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


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

Il risultato ottenuto è il seguente:

 

8.6.7.6. L'azione [/getAccueil]

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


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

Il risultato ottenuto è il seguente:

 

Riproviamo con un utente sconosciuto:


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

Il risultato ottenuto è il seguente:

 

Riproviamo con un utente esistente ma non autorizzato a utilizzare l'applicazione:


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

Il risultato ottenuto è il seguente:

 

8.6.7.7. L'azione [/getAgenda]

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


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

Il risultato ottenuto è il seguente:

 

Riproviamo con una data precedente a oggi:

 

Riproviamo con un medico inesistente:


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

Il risultato ottenuto è il seguente:

 

8.6.7.8. L'azione [/getNavbarRunJumbotronAccueil]

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


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

Il risultato ottenuto è il seguente:

 

Lo stesso vale per un utente sconosciuto:

 

8.6.7.9. L'azione [/getNavbarRunJumbotronAccueilAgenda]

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


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

Il risultato ottenuto è il seguente:

 

Inseriamo un medico che non esiste:

 

8.6.7.10. L'azione [/supprimerRv]

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


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

Il Rv n. 93 non esiste. Il risultato ottenuto è il seguente:

 

Con un appuntamento esistente:

 

È possibile verificare nel database che l’appuntamento sia stato effettivamente eliminato. Viene restituita la nuova agenda.

8.6.7.11. L'azione [/validerRv]

Chiediamo l'azione [validerRv] con il seguente valore registrato:


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

Il risultato ottenuto è il seguente:

 

È possibile verificare nel database che l'appuntamento sia stato effettivamente creato. Il nuovo calendario è stato inviato.

Si esegue la stessa operazione con un numero di fascia oraria inesistente:

 

Si ripete la stessa operazione con un numero di cliente inesistente:

 

8.6.8. Fase 5: Scrittura del client JavaScript

Torniamo all’architettura del server [Web1]:

Il client [2] del server [Web1] è un client JavaScript di tipo APU (applicazione a pagina singola):

  • il client richiede la pagina di avvio a un server web (non necessariamente [Web1]);
  • richiede le pagine successive al server [Web1] tramite chiamate Ajax;

Per realizzare questo client, utilizzeremo lo strumento [Webstorm] (cfr. paragrafo 9.8). Questo strumento mi è sembrato più pratico rispetto a STS. Il suo principale vantaggio è che offre il completamento automatico durante la digitazione del codice, oltre ad alcune opzioni di refactoring. Ciò consente di evitare numerosi errori.

8.6.8.1. Il progetto JS

Il progetto JS presenta la seguente struttura:

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

8.6.8.2. L'architettura del codice

Il codice è stato suddiviso in tre livelli:

  • il livello [présentation] raggruppa le funzioni di inizializzazione della pagina [boot.xml] e quelle dei vari componenti Bootstrap. È implementato dal file [ui.js];
  • il livello [événements] raggruppa tutti i gestori di eventi del livello [présentation]. È implementato dal file [evts.js];
  • il livello [DAO] invia le richieste HTTP al server [Web1]. È implementato dal file [dao.js];

8.6.8.3. Il livello [présentation]

  

Il livello [présentation] è implementato dal seguente file [ui.js]:


//il livello [présentation]
var ui = {
// variabili globali;
  "agenda": "",
  "resa": "",
  "langue": "",
  "urlService": "http://localhost:8081",
  "page": "login",
  "jourAgenda": "",
  "idMedecin": "",
  "user": {},
  "login": {},
  "exceptionTitle": {},
  "calendar_infos": {},
  "erreur": "",
  "idCreneau": "",
  "done": "",
// componenti della vista
  "body": "",
  "navbar": "",
  "jumbotron": "",
  "content": "",
  "exception": "",
  "exception_text": "",
  "exception_title": "",
  "loading": ""
};
// il livello degli eventi
var evts = {};
// il livello [dao]
var dao = {};

// ------------ documento pronto
$(document).ready(function () {
  // inizializzazione del documento
  console.log("document.ready");
  // componenti della pagina
  ui.navbar = $("#navbar");
  ui.jumbotron = $("#jumbotron");
  ui.content = $("#content");
  ui.erreur = $("#erreur");
  ui.exception = $("#exception");
  ui.exception_text = $("#exception-text");
  ui.exception_title = $("#exception-title");
  // si memorizza la pagina di accesso per poterla riprodurre
  ui.login.lang = ui.langue;
  ui.login.navbar = ui.navbar.html();
  ui.login.jumbotron = ui.jumbotron.html();
  ui.login.content = ui.content.html();
  // URL del servizio
  $("#urlService").val(ui.urlService);
});

// ------------------------ funzioni di inizializzazione dei componenti Bootstrap
ui.initNavBarStart = function () {
...
};

ui.initNavBarRun = function () {
...
};

ui.initChoixMedecinJour = function () {
...
};

ui.updateCalendar = function (renew) {
...
};

// visualizza il giorno selezionato
ui.displayJour = function () {
...
};

ui.initAgenda = function () {
...
};

ui.initResa = function () {
 ...
};

  • Per isolare i livelli l'uno dall'altro, si è deciso di inserirli in tre oggetti:
    • [ui] per il livello [présentation] (righe 2-27),
    • [evts] per il livello di gestione degli eventi (riga 29),
    • [dao] per il livello [DAO] (riga 31);

Questa suddivisione dei livelli in tre oggetti consente di evitare una serie di conflitti tra i nomi delle variabili e delle funzioni. Ogni livello utilizza variabili e funzioni precedute dal prefisso dell’oggetto che incapsula il livello stesso.

  • righe 38-44: si memorizzano i campi che saranno sempre presenti indipendentemente dalle viste visualizzate. Ciò evita di effettuare ricerche jQuery ripetitive e inutili;
  • righe 46-49: si memorizza localmente la pagina di avvio per poterla ripristinare quando l’utente si disconnette e non ha cambiato lingua;
  • righe 54-83: funzioni di inizializzazione dei componenti Bootstrap. Sono state tutte illustrate nell’analisi dedicata a tali componenti al paragrafo 8.6.4;

8.6.8.4. Le funzioni di utilità del livello [événements]

  

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


// inizio dell'attesa
evts.beginWaiting = function () {
  // inizio attesa
  ui.loading = $("#loading");
  ui.loading.show();
  ui.exception.hide();
  ui.erreur.hide();
  evts.travailEnCours = true;
};

// fine attesa
evts.stopWaiting = function () {
  // fine attesa
  evts.travailEnCours = false;
  ui.loading = $("#loading");
  ui.loading.hide();
};

// visualizzazione del risultato
evts.showResult = function (result) {
  // visualizzazione dei dati ricevuti
  var data = result.data;
  // si analizza lo stato
  switch (result.status) {
    case 1:
      // errore?
      if (data.status == 2) {
        ui.erreur.html(data.content);
        ui.erreur.show();
      } else {
        if (data.navbar) {
          ui.navbar.html(data.navbar);
        }
        if (data.jumbotron) {
          ui.jumbotron.html(data.jumbotron);
        }
        if (data.content) {
          ui.content.html(data.content)
        }
        if (data.agenda) {
          ui.agenda = $("#agenda");
          ui.resa = $("#resa");
        }
      }
      break;
    case 2:
      // visualizzazione errore
      evts.showException(data);
      break;
  }
};

// ------------ funzioni varie
evts.showException = function (data) {
  // visualizzazione errore
  ui.exception.show();
  ui.exception_text.html(data);
  ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
  • riga 2: la funzione [evts.beginwaiting] viene chiamata prima di qualsiasi azione asincrona [DAO];
  • righe 4-5: viene visualizzata l’immagine animata di attesa;
  • righe 6-7: si nasconde l'area di visualizzazione degli errori e delle eccezioni (non sono la stessa cosa);
  • riga 8: si segnala che è in corso un'operazione asincrona;
  • riga 12: la funzione [evts.stopwaiting] viene chiamata dopo che un'azione asincrona [DAO] ha restituito il proprio risultato;
  • riga 14: si nota che l'operazione asincrona è terminata;
  • riga 15: si nasconde l’immagine animata di attesa;
  • riga 20: la funzione [evts.showResult] visualizza il risultato [result] di un'azione asincrona [DAO]. Il risultato è un oggetto JS della forma seguente {'status':status,'data':data,'sendMeBack':sendMeBack}.
  • righe 47-50: utilizzate se [result.status==2]. Ciò accade quando il server [Web1] invia una risposta con un'intestazione di errore HTTP (ad esempio 403 forbidden). In questo caso, [data] è la stringa jSON inviata dal server per segnalare l'errore;
  • riga 25: caso in cui sia stata ricevuta una risposta valida dal server [Web1]. Il campo [data] contiene quindi la risposta del server: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
  • riga 27: caso in cui il server [Web1] abbia inviato una risposta di errore {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':errori};
  • righe 28-29: viene visualizzata la vista [erreurs];
  • righe 31-33: eventuale visualizzazione della barra di navigazione;
  • righe 34-36: eventuale visualizzazione del jumbotron;
  • righe 37-39: eventuale visualizzazione del campo [data.content]. Rappresenta, a seconda dei casi, una delle viste [accueil, agenda];
  • righe 40-43: se l'agenda è stata rigenerata, si recuperano alcuni riferimenti ai suoi componenti in modo da non doverli ricercare ogni volta che se ne avrà bisogno;
  • riga 54: la funzione [evts.showException] ha lo scopo di visualizzare il testo dell'eccezione contenuto nel suo parametro [data];
  • righe 57-58: viene visualizzato il testo dell'eccezione;
  • riga 58: il titolo dell’eccezione dipende dalla lingua attualmente impostata;

Il file [evts.js] contiene più di 300 righe di codice che non intendo commentare tutte. Mi limiterò a citare alcuni esempi per illustrare lo spirito di questo livello.

8.6.8.5. Accesso di un utente

Image

La connessione di un utente è gestita dalla seguente funzione:


// ------------------------ connessione
evts.connecter = function () {
  // si recuperano i valori da inviare
  var login = $("#login").val().trim();
  var passwd = $("#passwd").val().trim();
  // si imposta l'URL del server
  ui.urlService = $("#urlService").val().trim();
  dao.setUrlService(ui.urlService);
  // parametri della richiesta
  var post = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "lang": ui.langue
  };
  var sendMeBack = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "caller": evts.connecterDone
  };
  // si esegue la richiesta
  evts.execute([{
    "name": "accueil-sans-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • righe 4-5: si recuperano il nome utente e la password dell’utente;
  • righe 7-8: si recupera l'URL dal servizio [Web1]. Viene memorizzato sia nel livello [ui] che nel livello [dao];
  • righe 10-16: il valore da inviare: la lingua corrente e l'utente che sta tentando di effettuare l'accesso;
  • righe 17-23: l'oggetto [sendMeBack] è un oggetto che viene passato alla funzione [DAO] che verrà chiamata e che quest'ultima dovrà restituire alla funzione della riga 22. Qui l’oggetto [sendMeBack] incapsula l’utente che sta tentando di connettersi;
  • righe 25-29: la funzione [evts.execute] è in grado di eseguire una sequenza di azioni asincrone. In questo caso, viene passato un elenco costituito da una sola azione. I campi di quest’ultima sono i seguenti:
    • [name]: il nome dell'azione asincrona da eseguire,
    • [post]: il valore da inviare al server [Web1],
    • [sendMeBack]: il valore che l’azione asincrona deve restituire insieme al proprio risultato;

Prima di approfondire la funzione [evts.execute], esaminiamo la funzione [evts.connecterDone] alla riga 22. È la funzione alla quale la funzione asincrona [DAO], una volta chiamata, deve restituire il proprio risultato:


evts.connecterDone = function (result) {
  // visualizzazione del risultato
  evts.showResult(result);
  // Connessione riuscita?
  if (result.status == 1 && result.data.status == 1) {
    // pagina
    ui.page = "accueil-sans-agenda";
    // registrazione dell'utente
    ui.user = result.sendMeBack.user;
  }
};
  • riga 3: viene visualizzato il risultato restituito dal server [Web1];
  • riga 5: se questo risultato non contiene errori, si memorizzano il tipo della nuova pagina (riga 7) e l'utente autenticato (riga 9);

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


// esecuzione di una sequenza di azioni
evts.execute = function (actions) {
  // lavoro in corso?
  if (evts.travailEnCours) {
    // non si sta facendo nulla
    return;
  }
  // in attesa
  evts.beginWaiting();
  // esecuzione delle azioni
  dao.doActions(actions, evts.stopWaiting);
};
  • riga 2: il parametro [actions] è un elenco di azioni asincrone da eseguire;
  • righe 4-7: l'esecuzione è consentita solo se non ce n'è già un'altra in corso;
  • riga 9: si avvia l'attesa;
  • riga 11: si richiede al livello [DAO] di eseguire la sequenza di azioni. Il secondo parametro è il nome della funzione da eseguire quando tutte le azioni della sequenza avranno restituito il proprio risultato;

Non entreremo ora nei dettagli della funzione [dao.doActions]. Esamineremo un altro evento.

8.6.8.6. Cambio di lingua

Image

Il cambio di lingua è gestito dalla seguente funzione:


// ------------------------ cambio di lingua
evts.setLang = function (lang) {
  // cambio di lingua?
  if (lang == ui.langue) {
    // non si esegue alcuna operazione
    return;
  }
  // nuova lingua
  ui.langue = lang;
  // quale pagina va tradotta?
  switch (ui.page) {
    case "login":
      evts.getLogin();
      break;
    case "accueil-sans-agenda":
      evts.getAccueilSansAgenda();
      break;
    case "accueil-avec-agenda":
      evts.getAccueilAvecAgenda(ui);
      break;
  }
};
  • riga 2: il parametro [lang] è la nuova lingua: 'fr' o 'en';
  • righe 4-7: se la nuova lingua è quella corrente, non si esegue alcuna operazione;
  • riga 9: si memorizza la nuova lingua;
  • righe 12-20: in caso di cambio di lingua, è necessario rigenerare la pagina attualmente visualizzata dal browser. Sono possibili tre pagine:
    • quella denominata [login], in cui la pagina visualizzata è quella di autenticazione,
    • quella denominata [accueil-sans-agenda], che è la pagina visualizzata subito dopo un'autenticazione riuscita,
    • quella denominata [accueil-avec-agenda], che è la pagina visualizzata non appena viene visualizzata la prima agenda. Successivamente, rimane sempre in primo piano fino alla disconnessione dell’utente;

Esamineremo il caso della pagina [accueil-avec-agenda]. Esistono tre versioni di questa funzione:

  
  • la versione [ getAccueilAvecAgenda-one] esegue una singola azione asincrona;
  • la versione [ getAccueilAvecAgenda-parallel] esegue quattro azioni asincrone in parallelo;
  • la versione [ getAccueilAvecAgenda-sequence] esegue quattro azioni asincrone una dopo l'altra;

8.6.8.7. La funzione [ getAccueilAvecAgenda-one]

È la seguente funzione:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // parametri della richiesta
  var post = {
    "user": ui.user,
    "lang": ui.langue,
    "idMedecin": ui.idMedecin,
    "jour": ui.jourAgenda
  };
  var sendMeBack = {
    "caller": evts.getAccueilAvecAgendaDone
  };
  // richiesta
  evts.execute([{
    "name": "accueil-avec-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • righe 4-9: il valore da inviare incapsula l’utente connesso, la lingua desiderata, il numero del medico di cui si desidera l’agenda, il giorno dell’agenda desiderata;
  • righe 10-12: l'oggetto [sendMeBack] è l'oggetto che verrà restituito alla funzione della riga 11. In questo caso, non contiene alcuna informazione;
  • righe 14-18: esecuzione di una sequenza di azioni asincrone, denominata [accueil-avec-agenda] (riga 15);
  • riga 11: la funzione eseguita quando l’azione asincrona [accueil-avec-agenda] avrà restituito il proprio risultato;

La funzione [evts.getAccueilAvecAgendaDone] alla riga 11 visualizza il risultato della funzione asincrona denominata [accueil-avec-agenda]:


evts.getAccueilAvecAgendaDone = function (result) {
  // visualizzazione del risultato
  evts.showResult(result);
  // nuova pagina?
  if (result.status == 1 && result.data.status == 1) {
    ui.page = "accueil-avec-agenda";
  }
};
  • riga 1: [result] è il risultato della funzione asincrona denominata [accueil-avec-agenda];
  • riga 3: viene visualizzato questo risultato;
  • riga 5: se il risultato è corretto, si registra la nuova pagina (riga 6);

8.6.8.8. La funzione [ getAccueilAvecAgenda-parallel]

Si tratta della seguente funzione:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // azioni [navbar-run, jumbotron, accueil, agenda] in //
  // navbar-run
  var navbarRun = {
    "name": "navbar-run"
  };
  navbarRun.post = {
    "lang": ui.langue
  };
  navbarRun.sendMeBack = {
    "caller": evts.showResult
  };
  // jumbotron
  var jumbotron = {
    "name": "jumbotron"
  };
  jumbotron.post = {
    "lang": ui.langue
  };
  jumbotron.sendMeBack = {
    "caller": evts.showResult
  };
  // home
  var accueil = {
    "name": "accueil"
  };
  accueil.post = {
    "lang": ui.langue,
    "user": ui.user
  };
  accueil.sendMeBack = {
    "caller": evts.showResult
  };
  // agenda
  var agenda = {
    "name": "agenda"
  };
  agenda.post = {
    "user": ui.user,
    "lang": ui.langue,
    "idMedecin": ui.idMedecin,
    "jour": ui.jourAgenda
  };
  agenda.sendMeBack = {
    'idMedecin': ui.idMedecin,
    'giorno': ui.jourAgenda,
    "caller": evts.getAgendaDone
  };
  // esecuzione azioni in //
  evts.execute([navbarRun, jumbotron, accueil, agenda])
};
  • riga 51: questa volta si eseguono quattro azioni asincrone. Verranno eseguite in parallelo;
  • righe 5-13: definizione dell'azione [navbarRun] che recupera la barra di navigazione [navbar-run];
  • riga 12: la funzione da eseguire quando l’azione asincrona [navbarRun] avrà restituito il risultato;
  • righe 15-23: definizione dell'azione [jumbotron] che recupera la vista [jumbotron];
  • riga 22: la funzione da eseguire quando l'azione asincrona [jumbotron] avrà restituito il risultato;
  • righe 25-34: definizione dell'azione [accueil] che recupera la vista [accueil];
  • riga 33: la funzione da eseguire quando l'azione asincrona [accueil] avrà restituito il risultato;
  • righe 36-49: definizione dell'azione [agenda] che recupera la vista [jumbotron];
  • riga 48: la funzione da eseguire quando l'azione asincrona [agenda] avrà restituito il risultato;

8.6.8.9. La funzione [ getAccueilAvecAgenda-sequence]

Si tratta della seguente funzione:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // azioni [navbar-run, jumbotron, accueil, agenda] in ordine
  // agenda
  var agenda = {
    "name" : "agenda"
  };
  agenda.post = {
    "user" : ui.user,
    "lang" : ui.langue,
    "idMedecin" : ui.idMedecin,
    "jour" : ui.jourAgenda
  };
  agenda.sendMeBack = {
    'idMedecin': ui.idMedecin,
    'giorno: ui.jourAgenda,
    "caller" : evts.getAgendaDone
  };
  // home
  var accueil = {
    "name" : "accueil"
  };
  accueil.post = {
    "lang" : ui.langue,
    "user" : ui.user
  };
  accueil.sendMeBack = {
    "caller" : evts.showResult,
    "next" : agenda
  };
  // jumbotron
  var jumbotron = {
    "name" : "jumbotron"
  };
  jumbotron.post = {
    "lang" : ui.langue
  };
  jumbotron.sendMeBack = {
    "caller" : evts.showResult,
    "next" : accueil
  };
  // navbar-run
  var navbarRun = {
    "name" : "navbar-run"
  };
  navbarRun.post = {
    "lang" : ui.langue
  };
  navbarRun.sendMeBack = {
    "caller" : evts.showResult,
    "next" : jumbotron
  };
  // esecuzione sequenziale delle azioni
  evts.execute([ navbarRun ])
};
  • riga 54: si esegue l'azione [navbarRun]. Una volta terminata, si passa alla successiva: [jumbotron], riga 51. Anche questa azione viene quindi eseguita a sua volta. Una volta terminata, si passa alla successiva: [accueil], riga 40. Anche questa viene eseguita a sua volta. Una volta completata, si passa a quella successiva: [agenda], riga 29. Anche questa viene eseguita a sua volta. Una volta completata, ci si ferma poiché l’azione [agenda] non ha un’azione successiva.

8.6.8.10. Il livello [DAO]

  

Il file [dao.js] raggruppa tutte le funzioni del livello [DAO]. Le presenteremo una alla volta:


// URL esposte dal server
dao.urls = {
  "login": "/getLogin",
  "accueil": "/getAccueil",
  "jumbotron": "/getJumbotron",
  "agenda": "/getAgenda",
  "supprimerRv": "/supprimerRv",
  "validerRv": "/validerRv",
  "navbar-start": "/getNavbarStart",
  "navbar-run": "/getNavbarRun",
  "accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
  "accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- interfaccia
// URL del server
dao.setUrlService = function (urlService) {
  dao.urlService = urlService;
};
  • righe 16-18: la funzione che consente di impostare l'URL del servizio [Web1];
  • righe 2-13: il dizionario che associa il nome di un'azione asincrona all'URL del server [Web1] da interrogare;

// ------------------ gestione generica delle azioni
// esecuzione di una sequenza di azioni asincrone
dao.doActions = function (actions, done) {
  // elaborazione delle azioni
  dao.actionsCount = actions.length;
  dao.actionIndex = 0;
  for (var i = 0; i < dao.actionsCount; i++) {
    // richiesta asincrona DAO
    var deferred = $.Deferred();
    deferred.done(dao.actionDone);
    dao.doAction(deferred, actions[i], done);
  }
};
  • riga 3: la funzione [dao.doActions] esegue una sequenza di azioni asincrone [actions]. Il parametro [done] è la funzione da eseguire quando tutte le azioni hanno restituito il proprio risultato;
  • righe 7-12: le azioni asincrone vengono eseguite in parallelo. Tuttavia, nel caso in cui una di esse abbia un'azione successiva, quest'ultima viene eseguita al termine dell'azione che la precede;
  • riga 9: si crea l’oggetto [Deferred] nello stato [pending];
  • riga 10: quando questo oggetto passerà allo stato [resolved], verrà eseguita la funzione [dao.actionDone];
  • riga 11: l'azione n. i dell'elenco viene eseguita in modo asincrono. Il parametro [done] della riga 3 viene passato come parametro;

La funzione [dao.actionDone], che viene eseguita al termine di ogni azione asincrona, è la seguente:


// è stato ricevuto un risultato
dao.actionDone = function (result) {
  // chiama?
  var sendMeBack = result.sendMeBack;
  if (sendMeBack && sendMeBack.caller) {
    sendMeBack.caller(result);
  }
  // successivo?
  if (sendMeBack && sendMeBack.next) {
    // richiesta asincrona DAO
    var deferred = $.Deferred();
    deferred.done(dao.actionDone);
    dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
  }
  // finito?
  dao.actionIndex++;
  if (dao.actionIndex == dao.actionsCount) {
    // fatto?
    if (sendMeBack && sendMeBack.done) {
      sendMeBack.done(result);
    }
  }
};
  • riga 2: la funzione [dao.actionDone] riceve il risultato [result] da una delle azioni asincrone dell'elenco delle azioni da eseguire;
  • righe 4-7: se l'azione asincrona completata aveva specificato una funzione a cui restituire il risultato, tale funzione viene chiamata;
  • righe 9-14: se l'azione asincrona completata ha un'azione successiva, allora tale azione viene a sua volta eseguita;
  • riga 16: un'azione è terminata. Si incrementa il contatore delle azioni completate. Un'azione che ha un numero indeterminato di azioni successive conta come una sola azione;
  • righe 19-21: se inizialmente era stata specificata una funzione [done] da eseguire una volta che tutte le azioni della sequenza avessero restituito il proprio risultato, allora tale funzione viene ora eseguita;

Il metodo [dao.doAction] esegue un'azione asincrona:


// esecuzione di un'azione
dao.doAction = function (deferred, action, done) {
  // funzione "done" da incorporare nell'azione
  if (action.sendMeBack) {
    action.sendMeBack.done = done;
  } else {
    action.sendMeBack = {
      "done": done
    };
  }
  // esecuzione dell'azione
  dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
  • righe 4-10: come appena visto, la funzione che elaborerà il risultato dell'azione asincrona da eseguire deve avere accesso alla funzione [done]. A tal fine, si inserisce quest’ultima nell’oggetto [sendMeBack], oggetto che farà parte del risultato dell’operazione asincrona;
  • riga 12: si esegue la funzione [dao.executePost] che effettua una chiamata HTTP al server [Web1]. L'URL di destinazione è l'URL associata al nome dell'azione da eseguire;

La funzione [dao.executePost] esegue una chiamata a HTTP:


// richiesta HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
  // si effettua manualmente una chiamata Ajax
  $.ajax({
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    url: dao.urlService + url,
    type: 'POST',
    data: JSON3.stringify(post),
    dataType: 'json',
    success: function (data) {
      // si restituisce il risultato
      deferred.resolve({
        "status": 1,
        "data": data,
        "sendMeBack": sendMeBack
      });
    },
    error: function (jqXHR, textStatus, errorThrown) {
      var data;
      if (jqXHR.responseText) {
        data = jqXHR.responseText;
      } else {
        data = textStatus;
      }
      // si restituisce l'errore
      deferred.resolve({
        "status": 2,
        "data": data,
        "sendMeBack": sendMeBack
      });
    }
  });
};

Abbiamo già incontrato e commentato questa funzione. Si noti semplicemente, alla riga 9, che la funzione URL ha come obiettivo la concatenazione della funzione URL del server [Web1] con la funzione URL associata al nome dell'azione.

8.6.8.11. La pagina di avvio

  

Image

La pagina di avvio [boot.html] mostra la schermata sopra riportata. È l'unica pagina caricata direttamente dal browser. Le altre vengono recuperate tramite richieste Ajax. Il suo codice è il seguente:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
  <meta name="viewport" content="width=device-width"/>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title>RdvMedecins</title>
  <!-- Bootstrap core CSS -->
  <link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
  <link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
  <link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
  <link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
  <!-- Stili personalizzati per questo modello -->
  <link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
  <!-- Bootstrap core JavaScript ================================================== -->
  <script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
  <script type="text/javascript" src="vendor/bootstrap.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-select.js"></script>
  <script type="text/javascript" src="vendor/moment-with-locales.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
  <script type="text/javascript" src="vendor/footable.js"></script>
  <!-- script utente -->
  <script type="text/javascript" src="js/json3.js"></script>
  <script type="text/javascript" src="js/ui.js"></script>
  <script type="text/javascript" src="js/evts.js"></script>
  <script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
  <script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
      <div class="navbar-collapse collapse">
        <img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
        <!-- Modulo di identificazione -->
        <div class="navbar-form navbar-right" role="form" id="formulaire">
          <div class="form-group">
            <input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
          </div>
          <div class="form-group">
            <input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
          </div>
          <div class="form-group">
            <input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
          </div>
          <button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
          <!-- lingue -->
          <div class="btn-group">
            <button type="button" class="btn btn-danger">Langue</button>
            <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
              <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
            </button>
            <ul class="dropdown-menu" role="menu">
              <li><a href="javascript:evts.setLang('fr')">Français</a></li>
              <li><a href="javascript:evts.setLang('en')">English</a></li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
<div class="container">
  <!-- Jumbotron Bootstrap -->
  <div id="jumbotron">
    <div class="jumbotron">
      <div class="row">
        <div class="col-md-2">
          <img src="images/caduceus.jpg" alt="RvMedecins"/>
        </div>
        <div class="col-md-10">
          <h1>
            Cabinet médical<br/>Les Médecins associés
          </h1>
        </div>
      </div>
    </div>
  </div>
  <!-- messaggi di errore -->
  <div id="erreur"></div>
  <div id="exception" class="alert alert-danger" style="display: none">
    <h3 id="exception-title"></h3>
    <span id="exception-text"></span>
  </div>
  <!-- contenuto -->
  <div id="content">
    <div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
  </div>
</div>
<!-- pagina iniziale -->
<script>
  // inizializzazione della pagina
  ui.langue = 'fr';
  ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
  ui.exceptionTitle['en'] = "The following server error was met:";
  ui.initNavBarStart();
</script>
</body>
</html>
  • abbiamo già incontrato questo tipo di pagina nel capitolo dedicato a Bootstrap (paragrafo 8.6.4);
  • righe 99-105: inizializzazione di alcuni elementi del livello [présentation];
  • riga 27: viene utilizzato lo script [getAccueilAvecAgenda-sequence.js]. Modificando lo script in questa riga si ottengono tre comportamenti diversi per visualizzare la pagina [accueil-avec-agenda]:
    • [getAccueilAvecAgenda-one.js] ottiene la pagina con una sola chiamata a HTTP,
    • [getAccueilAvecAgenda-parallel.js] recupera la pagina con quattro chiamate simultanee a HTTP,
    • [getAccueilAvecAgenda-sequence.js] recupera la pagina con quattro chiamate successive a HTTP;

8.6.8.12. Tests

Esistono diversi modi per eseguire i test. In questo caso utilizzeremo lo strumento [Webstorm]:

  • in [1] si apre un progetto. È sufficiente selezionare la cartella [2] contenente la struttura statica (HTML, CSS, JS) del sito da testare;
  • in [3], il sito statico;
  • in [4-5], si carica la pagina [boot.html];
  • in [5], si vede che un server incorporato in [Webstorm] ha fornito la pagina [boot.html] dalla porta [63342]. È un aspetto importante da comprendere, poiché significa che gli script della pagina [boot.html] effettueranno chiamate interdominio al server [Web1], che a sua volta opera su [localhost:8081]. Il browser che ha caricato [boot.html] sa di averla caricata da [localhost:63342]. Non accetterà quindi che questa pagina effettui chiamate al sito [localhost:8081] perché non si tratta della stessa porta. Attuerà quindi le chiamate interdominio descritte al paragrafo 8.4.14. Per questo motivo, l’applicazione [Web1] deve essere configurata per accettare tali chiamate interdominio. La configurazione avviene nel file [AppConfig] del server Spring / Thymeleaf:
 

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {

    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // radice servizio web / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout in millisecondi
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;
...

Lasciamo al lettore il compito di eseguire i test sul client JS. Esso deve essere in grado di riprodurre le funzionalità descritte al paragrafo 8.6.3.

Una volta che il client JS è stato dichiarato corretto, è possibile distribuirlo nella cartella del server [Web1] per evitare di dover autorizzare le richieste interdominio:

  

Come illustrato sopra, abbiamo copiato il sito testato nella cartella [src / main / resources / static]. Successivamente è possibile richiedere URL e [http://localhost:8081/boot.html]:

Image

Ora non abbiamo più bisogno delle richieste interdominio e possiamo scrivere nel file di configurazione [AppConfig] del server [Web1]:


    // CORS
    private final boolean CORS_ALLOWED=false;

L'applicazione sopra indicata continuerà a funzionare. Se torniamo all'applicazione [Webstorm], questa non funziona più:

Image

Image

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

Image

Si tratta di un errore di richiesta interdominio non autorizzata.

8.6.8.13. Conclusion

Abbiamo realizzato la seguente architettura JS:

  • i livelli sono ben distinti;
  • abbiamo un'applicazione di tipo APU (applicazione a pagina singola). È proprio questa caratteristica che ora ci consentirà di generare un'applicazione nativa per diversi dispositivi mobili (Android, IoS, Windows Phone);
  • abbiamo creato un modello in grado di eseguire azioni asincrone in parallelo, in sequenza o con una combinazione delle due;

8.6.9. Fase 6: generazione di un’applicazione nativa per Android

Lo strumento [Phonegap] [http://phonegap.com/] consente di produrre un eseguibile per dispositivi mobili (Android, IoS, Windows 8, ...) a partire da un'applicazione HTML / JS / CSS. Esistono diversi modi per raggiungere questo obiettivo. Noi utilizziamo il metodo più semplice: uno strumento disponibile online sul sito di Phonegap [http://build.phonegap.com/apps]. Questo strumento provvederà a caricare il file zip del sito statico da convertire. La pagina di avvio deve chiamarsi [index.html]. Rinominiamo quindi la pagina [boot.html] in [index.html]:

 

quindi comprimiamo la cartella, in questo caso [rdvmedecins-client-js-03]. Successivamente accediamo al sito di Phonegap [http://build.phonegap.com/apps]:

  • prima di [1], potrebbe essere necessario creare un account;
  • in [1], si avvia;
  • in [2], si sceglie un piano gratuito che consente solo un’applicazione Phonegap;
  • in [3], si scarica l'applicazione compressa [4];
  • in [5], si assegna un nome all'applicazione;
  • in [6], si procede alla compilazione. Questa operazione può richiedere 1 minuto. Attendere fino a quando le icone delle diverse piattaforme mobili indicano che la compilazione è terminata;
  • sono stati generati solo i file binari per Android ([7]) e Windows ([8]);
  • fare clic su [7] per scaricare il file binario Android;
  • in [9] il file binario [apk] scaricato;

Avviare un emulatore [GenyMotion] per un tablet Android (vedere paragrafo 9.9):

 

Nell'immagine sopra, si avvia un emulatore di tablet con Android API 19. Una volta avviato l'emulatore,

  • sbloccarlo trascinando il cursore di sblocco (se presente) sul lato e poi rilasciandolo;
  • con il mouse, trascinate il file [PGBuildApp-debug.apk] che avete scaricato e rilasciatelo sull'emulatore. Verrà quindi installato ed eseguito;

È necessario sostituire URL con [1]. A tal fine, in una finestra di comando, digitate il comando [ipconfig] (riga 1 qui sotto) che visualizzerà i diversi indirizzi IP del vostro computer:


C:\Users\Serge Tahé>ipconfig

Configuration IP de Windows


Carte réseau sans fil Connexion au réseau local* 15 :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :

Carte Ethernet Connexion au réseau local :

   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
   Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
   Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
   Masque de sous-réseau. . . . . . . . . : 255.255.0.0
   Passerelle par défaut. . . . . . . . . : 172.19.0.254

Carte réseau sans fil Wi-Fi :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :

...

Annotate l’indirizzo Wi-Fi IP (righe 6-9) oppure l’indirizzo sulla rete locale IP (righe 11-17). Quindi utilizzare questo indirizzo IP nel campo URL del server web:

Fatto ciò, accedete al servizio web:

Provate l'applicazione sull'emulatore. Dovrebbe funzionare. A livello di server, è possibile autorizzare o meno le intestazioni CORS nella classe [ApplicationModel]:


    // CORS
    private final boolean CORS_ALLOWED=false;

Questo non ha alcuna importanza per l'applicazione Android, poiché non viene eseguita in un browser. L'esigenza delle intestazioni CORS deriva infatti dal browser e non dal server.

8.6.10. Conclusioni del caso di studio

Abbiamo sviluppato la seguente architettura:

Si tratta di un'architettura a tre livelli complessa. L'obiettivo era riutilizzare il livello [Web2], che era il livello server dell'applicazione [AngularJS-Spring MVC] del documento [Tutoriel AngularJS / Spring 4], per passare daURL e [http://tahe.developpez.com/angularjs-spring4/]. È solo per questo motivo che si è optato per un’architettura a tre livelli. Laddove nell’applicazione [AngularJS-Spring MVC], il client di [Web2] era un client [AngularJS], qui il client di [Web2] è un'architettura a due livelli [jQuery] / [Spring MVC / Thymeleaf]. Abbiamo aumentato il numero di livelli, quindi ci sarà una perdita di prestazioni.

L’applicazione qui analizzata è stata sviluppata nel corso del tempo in tre documenti diversi:

  1. da [Introduction aux frameworks JSF2, Primefaces et Primefaces mobile] a URL e [http://tahe.developpez.com/java/primefaces/]. Il caso di studio era stato quindi sviluppato con i framework JSF2 / Primefaces. Primefaces è una libreria di componenti AJAX che evita di scrivere codice JavaScript. L’applicazione sviluppata all’epoca era meno complessa di quella qui analizzata. Presentava una versione web classica per computer e una versione mobile per telefoni;
  2. da [Tutoriel AngularJS / Spring 4] a URL e [http://tahe.developpez.com/angularjs-spring4/]. L’applicazione sviluppata all’epoca presentava le stesse caratteristiche di quella analizzata nel presente documento. L’applicazione era stata inoltre portata su Android;
  3. il presente documento;

Da questo lavoro emergono, a mio avviso, i seguenti punti:

  • l’applicazione [Primefaces] è stata di gran lunga la più semplice da scrivere e la sua versione web mobile si è rivelata performante. Non richiede conoscenze di JavaScript. Non è possibile portarla in modo nativo sui dispositivi mobili, ma è davvero necessario? Sembra difficile modificare lo stile dell’applicazione. Si lavora infatti con i fogli di stile di Primefaces. Questo potrebbe rappresentare uno svantaggio;
  • l’applicazione [AngularJS-Spring MVC] è stata complessa da scrivere. Il framework [AngularJS] mi è sembrato piuttosto difficile da comprendere quando si vuole padroneggiarlo. L’architettura [client Angular] / [service web / jSON implémenté par Spring MVC] è particolarmente pulita ed efficiente. Questa architettura è replicabile per qualsiasi applicazione web. È l’architettura che mi sembra più promettente perché mette in gioco competenze diverse sia sul lato client che sul lato server (JS+HTML+CSS sul lato client, Java o altro sul lato server), il che consente di sviluppare il client e il server in parallelo;
  • per l’applicazione sviluppata in questo documento con un’architettura a tre livelli [client jQuery] / [serveur Web1 / Spring MVC / Thymeleaf] / [serveur Web2 / Spring MVC], è possibile che alcuni trovino la tecnologia [jQuery+Spring MVC+Thymelaf] più semplice da comprendere rispetto a quella di [AngularJS]. Il livello [DAO] del client JavaScript che abbiamo scritto è riutilizzabile in altre applicazioni;