Skip to content

14. Applicazione web MVC in un’architettura a 3 livelli – Esempio 1

14.1. Présentation

Finora ci siamo limitati a esempi a scopo didattico. Per questo motivo, dovevano essere semplici. Presentiamo ora un’applicazione di base, ma comunque più ricca di tutte quelle presentate finora. Avrà la particolarità di utilizzare i tre livelli di un’architettura a tre livelli:

Image

Il lettore è invitato a rileggere i principi di un’applicazione web MVC in un’architettura a tre livelli, se li ha dimenticati, al paragrafo 4.

L’applicazione web che scriveremo consentirà di gestire un gruppo di persone con quattro operazioni:

  • elenco delle persone del gruppo
  • aggiunta di una persona al gruppo
  • modifica di una persona del gruppo
  • eliminazione di una persona dal gruppo

Queste quattro operazioni corrispondono alle operazioni di base su una tabella di un database. Scriveremo due versioni di questa applicazione:

  • nella versione 1, il livello [dao] non utilizzerà alcun database. Le persone del gruppo saranno memorizzate in un semplice oggetto [ArrayList] gestito internamente dal livello [dao]. Ciò consentirà al lettore di testare l’applicazione senza vincoli legati al database.
  • Nella versione 2, inseriremo il gruppo di persone in una tabella del database. Dimostreremo che ciò avverrà senza alcun impatto sul livello web della versione 1, che rimarrà invariato.

e schermate che seguono mostrano le pagine che l’applicazione scambia con l’utente.

Image

Image

Image

 

14.2. Il progetto Eclipse

Il progetto dell'applicazione si chiama [personnes-01]:

Image

Questo progetto comprende i tre livelli dell’architettura a tre livelli dell’applicazione:

  • il livello [dao] è contenuto nel pacchetto [istia.st.mvc.personnes.dao]
  • il livello [metier] o [service] è contenuto nel pacchetto [istia.st.mvc.personnes.service]
  • il livello [web] o [ui] è contenuto nel pacchetto [istia.st.mvc.personnes.web]
  • il pacchetto [istia.st.mvc.personnes.entites] contiene gli oggetti condivisi tra diversi livelli
  • il pacchetto [istia.st.mvc.personnes.tests] contiene i test JUnit dei livelli [dao] e [service]

Esploreremo in successione i tre livelli [dao], [service] e [web]. Poiché sarebbe troppo lungo da scrivere e forse troppo noioso da leggere, a volte potremmo essere un po’ concisi nelle spiegazioni, tranne quando ciò che viene presentato è nuovo.

14.3. La rappresentazione di una persona

L’applicazione gestisce un gruppo di persone. Le schermate del paragrafo 14.1 hanno mostrato alcune delle caratteristiche di una persona. Formalmente, queste sono rappresentate da una classe [Personne]:

Image

La classe [Personne] è la seguente:

package istia.st.springmvc.personnes.entites;

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

public class Personne {

     // ID univoco della persona
    private int id;
     // la versione attuale
    private long version;
     // il cognome
    private String nom;
     // il nome
    private String prenom;
     // la data di nascita
    private Date dateNaissance;
     // lo stato civile
    private boolean marie = false;
     // il numero di figli
    private int nbEnfants;

     // getter - setter
...

     // costruttore predefinito
    public Personne() {

    }

     // costruttore con inizializzazione dei campi della persona
    public Personne(int id, String prenom, String nom, Date dateNaissance,
            boolean marie, int nbEnfants) {
        setId(id);
        setNom(nom);
        setPrenom(prenom);
        setDateNaissance(dateNaissance);
        setMarie(marie);
        setNbEnfants(nbEnfants);
    }

     // costruttore di una persona tramite copia di un'altra persona
    public Personne(Personne p) {
        setId(p.getId());
        setVersion(p.getVersion());
        setNom(p.getNom());
        setPrenom(p.getPrenom());
        setDateNaissance(p.getDateNaissance());
        setMarie(p.getMarie());
        setNbEnfants(p.getNbEnfants());
    }


     // toString
    public String toString() {
        return "[" + id + "," + version + "," + prenom + "," + nom + ","
                + new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance)
                + "," + marie + "," + nbEnfants + "]";
    }
}
  • una persona è identificata dalle seguenti informazioni:
    • id: un numero che identifica in modo univoco una persona
    • cognome: il cognome della persona
    • nome: il suo nome
    • dateNaissance: la sua data di nascita
    • stato civile: se è sposata o meno
    • nbEnfants: il numero dei suoi figli
  • L'attributo [version] è un attributo aggiunto artificialmente per le esigenze dell'applicazione. Da un punto di vista orientato agli oggetti, sarebbe stato senza dubbio preferibile aggiungere questo attributo in una classe derivata da [Personne]. La sua necessità emerge quando si analizzano i casi d’uso dell’applicazione web. Uno di questi è il seguente:

Al momento T1, un utente U1 accede alla modifica di una persona P. In quel momento, il numero di figli è 0. L'utente modifica tale numero portandolo a 1, ma prima che possa confermare la modifica, un altro utente U2 accede alla modifica della stessa persona P. Poiché U1 non ha ancora confermato la propria modifica, U2 vede il numero di figli pari a 0. U2 mette il nome della persona P in maiuscolo. Successivamente, U1 e U2 confermano le loro modifiche in quest’ordine. Sarà la modifica di U2 a prevalere: il nome verrà scritto in maiuscolo e il numero di figli rimarrà pari a zero, anche se U1 ritiene di averlo modificato in 1.

Il concetto di versione della persona ci aiuta a risolvere questo problema. Riprendiamo lo stesso caso d’uso:

All’ora T1, un utente U1 accede alla modifica di una persona P. In quel momento, il numero di figli è 0 e la versione è V1. Modifica il numero di figli portandolo a 1, ma prima che possa confermare la modifica, un utente U2 accede per modificare la stessa persona P. Poiché U1 non ha ancora confermato la modifica, U2 vede il numero di figli pari a 0 e la versione V1. U2 scrive il nome della persona P in maiuscolo. Successivamente, U1 e U2 convalidano le proprie modifiche in questo ordine. Prima di convalidare una modifica, si verifica che chi modifica una persona P possieda la stessa versione della persona P attualmente registrata. Questo sarà il caso dell’utente U1. La sua modifica viene quindi accettata e si cambia la versione della persona modificata da V1 a V2 per segnalare che la persona ha subito una modifica. Al momento della convalida della modifica di U2, si noterà che possiede una versione V1 della persona P, mentre attualmente la versione di quest’ultima è V2. A quel punto sarà possibile comunicare all’utente U2 che qualcuno lo ha preceduto e che deve ripartire dalla nuova versione della persona P. Egli lo farà, recupererà una persona P con versione V2 che ora ha un figlio, scriverà il nome in maiuscolo e confermerà. La sua modifica sarà accettata se la persona P registrata ha ancora la versione V2. Alla fine, le modifiche apportate da U1 e U2 saranno prese in considerazione, mentre nel caso d’uso senza versione, una delle modifiche andava persa.

  • righe 32-40: un costruttore in grado di inizializzare i campi di una persona. Si omette il campo [version].
  • righe 43-51: un costruttore che crea una copia della persona che gli viene passata come parametro. Si ottengono così due oggetti con contenuto identico ma referenziati da due puntatori diversi.
  • riga 55: il metodo [toString] viene ridefinito per restituire una stringa di caratteri che rappresenta lo stato della persona

14.4. Il livello [dao]

Il livello [dao] è costituito dalle seguenti classi e interfacce:

Image

  • [IDao] è l’interfaccia presentata dal livello [dao]
  • [DaoImpl] è un'implementazione di quest'ultima in cui il gruppo di persone è incapsulato in un oggetto [ArrayList]
  • [DaoException] è un tipo di eccezioni non controllate (unchecked), generate dal livello [dao]

L’interfaccia [IDao] è la seguente:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // elenco di tutte le persone
    Collection getAll();
     // recuperare una persona specifica
    Personne getOne(int id);
     // aggiungere/modificare una persona
    void saveOne(Personne personne);
     // eliminare una persona
    void deleteOne(int id);
}
  • L’interfaccia dispone di quattro metodi per le quattro operazioni che si desidera eseguire sul gruppo di persone:
    • getAll: per ottenere una raccolta di persone
    • getOne: per ottenere una persona con un id specifico
    • saveOne: per aggiungere una persona (id=-1) o modificare una persona esistente (id <> -1)
    • deleteOne: per eliminare una persona con un id specifico

Il livello [dao] potrebbe generare delle eccezioni. Queste saranno di tipo [DaoException] :

package istia.st.springmvc.personnes.dao;

public class DaoException extends RuntimeException {

     // codice di errore
    private int code;

    public int getCode() {
        return code;
    }

// costruttore
    public DaoException(String message,int code) {
        super(message);
        this.code=code;
    }
}
  • riga 3: la classe [DaoException], derivata da [RuntimeException], è un tipo di eccezione non controllata: il compilatore non ci obbliga a:
    • gestire questo tipo di eccezioni con un try/catch quando si chiama un metodo che potrebbe generarla
    • inserire l’indicatore "throws DaoException" nella firma di un metodo che potrebbe generare l’eccezione

Questa tecnica ci evita di dover firmare i metodi dell’interfaccia [IDao] con eccezioni di un tipo specifico. Qualsiasi implementazione che generi eccezioni non controllate sarà quindi accettabile, garantendo così flessibilità nell’architettura.

  • riga 6: un codice di errore. Il livello [dao] genererà diverse eccezioni che saranno identificate da codici di errore distinti. Ciò consentirà al livello che deciderà di gestire l’eccezione di conoscere l’origine esatta dell’errore e di adottare così le misure appropriate. Esistono altri modi per ottenere lo stesso risultato. Uno di questi consiste nel creare un tipo di eccezione per ogni possibile tipo di errore, ad esempio NomManquantException, PrenomManquantException, AgeIncorrectException, ...
  • righe 13-16: il costruttore che consentirà di creare un'eccezione identificata da un codice di errore e da un messaggio di errore.
  • righe 8-10: il metodo che consentirà al codice di gestione di un'eccezione di recuperarne il codice di errore.

La classe [DaoImpl] implementa l’interfaccia [IDao]:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;

public class DaoImpl implements IDao {

     // un elenco di persone
    private ArrayList personnes = new ArrayList();

     // numero della persona successiva
    private int id = 0;

     // inizializzazioni
    public void init() {
        try {
            Personne p1 = new Personne(-1, "Joachim", "Major",
                    new SimpleDateFormat("dd/MM/yyyy").parse("13/11/1984"),
                    true, 2);
            saveOne(p1);
            Personne p2 = new Personne(-1, "Mélanie", "Humbort",
                    new SimpleDateFormat("dd/MM/yyyy").parse("12/02/1985"),
                    false, 1);
            saveOne(p2);
            Personne p3 = new Personne(-1, "Charles", "Lemarchand",
                    new SimpleDateFormat("dd/MM/yyyy").parse("01/03/1986"),
                    false, 0);
            saveOne(p3);
        } catch (ParseException ex) {
            throw new DaoException(
                    "Erreur d'initialisation de la couche [dao] : "
                            + ex.toString(), 1);
        }
    }

     // elenco delle persone
    public Collection getAll() {
        return personnes;
    }

     // recupera una persona specifica
    public Personne getOne(int id) {
         // si sta cercando la persona
        int i = getPosition(id);
         // È stata trovata?
        if (i != -1) {
            return new Personne(((Personne) personnes.get(i)));
        } else {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }

     // aggiungere o modificare una persona
    public void saveOne(Personne personne) {
         // il parametro "persona" è valido?
        check(personne);
         // Aggiunta o modifica?
        if (personne.getId() == -1) {
             // aggiunta
            personne.setId(getNextId());
            personne.setVersion(1);
            personnes.add(personne);
            return;
        }
         // modifica - si sta cercando la persona
        int i = getPosition(personne.getId());
         // È stata trovata?
        if (i == -1) {
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] qu'on veut modifier n'existe pas", 2);
        }
         // Si dispone della versione corretta dell'originale?
        Personne original = (Personne) personnes.get(i);
        if (original.getVersion() != personne.getVersion()) {
            throw new DaoException("L'original de la personne [" + personne
                    + "] a changé depuis sa lecture initiale", 3);
        }
         // si attende 10 ms
         //wait(10);
         // Va bene - si effettua la modifica
        original.setVersion(original.getVersion()+1);
        original.setNom(personne.getNom());
        original.setPrenom(personne.getPrenom());
        original.setDateNaissance((personne.getDateNaissance()));
        original.setMarie(personne.getMarie());
        original.setNbEnfants(personne.getNbEnfants());
    }

     // eliminazione di una persona
    public void deleteOne(int id) {
         // si cerca la persona
        int i = getPosition(id);
         // L'abbiamo trovata?
        if (i == -1) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        } else {
             // si elimina la persona
            personnes.remove(i);
        }
    }

     // generatore di ID
    private int getNextId() {
        id++;
        return id;
    }

     // cerca una persona
    private int getPosition(int id) {
        int i = 0;
        boolean trouvé = false;
         // si scorre l'elenco delle persone
        while (i < personnes.size() && !trouvé) {
            if (id == ((Personne) personnes.get(i)).getId()) {
                trouvé = true;
            } else {
                i++;
            }
        }
         // risultato?
        return trouvé ? i : -1;
    }

     // verifica di una persona
    private void check(Personne p) {
         // persona p
        if (p == null) {
            throw new DaoException("Personne null", 10);
        }
         // ID
        if (p.getId() != -1 && p.getId() < 0) {
            throw new DaoException("Id [" + p.getId() + "] invalide", 11);
        }
         // data di nascita
        if (p.getDateNaissance() == null) {
            throw new DaoException("Date de naissance manquante", 12);
        }
         // numero di figli
        if (p.getNbEnfants() < 0) {
            throw new DaoException("Nombre d'enfants [" + p.getNbEnfants()
                    + "] invalide", 13);
        }
         // cognome
        if (p.getNom() == null || p.getNom().trim().length() == 0) {
            throw new DaoException("Nom manquant", 14);
        }
         // nome
        if (p.getPrenom() == null || p.getPrenom().trim().length() == 0) {
            throw new DaoException("Prénom manquant", 15);
        }
    }

     // in attesa
    private void wait(int N) {
         // si attende N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // si visualizza la traccia dell'eccezione
            e.printStackTrace();
            return;
        }
    }
}

Ci limiteremo a fornire solo le linee generali di questo codice. Ci soffermeremo tuttavia un po’ sulle parti più delicate.

  • riga 13: l’oggetto [ArrayList] che conterrà il gruppo di persone
  • riga 16: l’identificativo dell’ultima persona aggiunta. Ad ogni nuova aggiunta, questo identificativo verrà incrementato di 1.

La classe [DaoImpl] verrà istanziata in un unico esemplare. Si tratta di un cosiddetto singleton. Un’applicazione web serve i propri utenti in modo simultaneo. In un dato momento, il server web esegue diversi thread. Questi condividono i singleton:

  • quello del livello [dao]
  • quello del livello [service]
  • quelli dei vari controller, validatori di dati, ... del livello web

Se un singleton presenta campi privati, occorre chiedersi immediatamente perché ne abbia. Sono giustificati? Infatti, saranno condivisi tra diversi thread. Se sono di sola lettura, ciò non pone alcun problema purché possano essere inizializzati in un momento in cui si è certi che vi sia un solo thread attivo. In genere si riesce a individuare quel momento. È quello dell’avvio dell’applicazione web, quando questa non ha ancora iniziato a servire i clienti. Se sono in lettura/scrittura, allora è necessario implementare una sincronizzazione dell’accesso ai campi, altrimenti si va incontro a un disastro. Illustreremo questo problema quando testeremo il livello [dao].

  • La classe [DaoImpl] non ha un costruttore. Verrà quindi utilizzato il suo costruttore predefinito.
  • righe 19-38: il metodo [init] verrà chiamato al momento dell’istanziazione del singleton del livello [dao]. Esso crea un elenco di tre persone.
  • righe 41-43: implementa il metodo [getAll] dell’interfaccia [IDao]. Restituisce un riferimento all’elenco delle persone.
  • righe 46-55: implementa il metodo [getOne] dell’interfaccia [IDao]. Il suo parametro è l’ID della persona ricercata.

Per recuperarla, si ricorre a un metodo privato [getPosition] presente nelle righe 113-126. Questo metodo restituisce la posizione nell’elenco della persona ricercata oppure -1 se la persona non è stata trovata.

Se la persona è stata trovata, il metodo [getOne] restituisce un riferimento (riga 51) a una copia di tale persona e non alla persona stessa. Infatti, quando un utente desidera modificare una persona, le informazioni relative a quest’ultima vengono richieste al livello [dao] e inoltrate al livello [web] per la modifica, sotto forma di un riferimento a un oggetto [Personne]. Questo riferimento fungerà da contenitore per i dati inseriti nel modulo di modifica. Quando, nel livello web, l’utente invierà le proprie modifiche, il contenuto del contenitore verrà modificato. Se il contenitore è un riferimento alla persona reale del [ArrayList] del livello [dao], allora quest’ultima viene modificata anche se le modifiche non sono state applicate ai livelli [service] e [dao]. Quest’ultimo è l’unico autorizzato a gestire l’elenco delle persone. Pertanto, il livello web deve operare su una copia della persona da modificare. In questo caso, il livello [dao] fornisce tale copia.

Se la persona ricercata non viene trovata, viene generata un’eccezione di tipo [DaoException] con il codice di errore 2 (riga 53).

  • righe 94-104: implementa il metodo [deleteOne] dell’interfaccia [IDao]. Il suo parametro è l’ID della persona da eliminare. Se la persona da eliminare non esiste, viene generata un’eccezione di tipo [DaoException] con codice di errore 2.
  • righe 58-91: implementa il metodo [saveOne] dell'interfaccia [IDao]. Il suo parametro è un oggetto [Personne]. Se questo oggetto ha un id=-1, si tratta dell’aggiunta di una persona. In caso contrario, si tratta di modificare la persona dell’elenco con tale id utilizzando i valori del parametro.
    • riga 60: la validità del parametro [Personne] viene verificata da un metodo privato [check] definito alle righe 129-155. Questo metodo esegue verifiche di base sul valore dei diversi campi di [Personne]. Ogni volta che viene rilevata un’anomalia, viene generato un [DaoException] con un codice di errore specifico. Poiché il metodo [saveOne] non gestisce questa eccezione, la segnalerà al metodo chiamante.
    • riga 62: se il parametro [Personne] ha un ID pari a -1, si tratta di un’aggiunta. L’oggetto [Personne] viene aggiunto all’elenco interno delle persone (riga 66), con il primo ID disponibile (riga 64) e un numero di versione pari a 1 (riga 65).
    • Se il parametro [Personne] ha un [id] diverso da -1, si tratta di modificare la persona dell’elenco interno con quel [id]. Innanzitutto, si verifica (righe 70-75) che la persona da modificare esista. In caso contrario, viene generata un’eccezione di tipo [DaoException] con il codice di errore 2.
    • Se la persona è effettivamente presente, si verifica che la sua versione attuale corrisponda a quella del parametro [Personne], che contiene le modifiche da apportare all’originale. In caso contrario, significa che chi intende modificare la persona non ne possiede l’ultima versione. Glielo si comunica generando un'eccezione di tipo [DaoException] con codice di errore 3 (righe 79-80).
    • Se tutto va bene, le modifiche vengono apportate all’originale della persona (righe 85-90)

È evidente che questo metodo deve essere sincronizzato. Ad esempio, tra il momento in cui si verifica che la persona da modificare sia effettivamente presente e quello in cui la modifica verrà effettivamente apportata, la persona potrebbe essere stata eliminata dall’elenco da qualcun altro. Il metodo dovrebbe quindi essere dichiarato [synchronized] per garantire che venga eseguito da un solo thread alla volta. Lo stesso vale per gli altri metodi dell’interfaccia [IDao]. Non lo facciamo, preferendo spostare questa sincronizzazione nel livello [service]. Per evidenziare i problemi di sincronizzazione, durante i test del livello [dao] interromperemo l’esecuzione di [saveOne] per 10 ms (riga 83) tra il momento in cui sappiamo di poter apportare la modifica e quello in cui la apportiamo effettivamente. Il thread che esegue [saveOne] perderà quindi il controllo del processore a favore di un altro. In questo modo aumentiamo le probabilità che si verifichino conflitti di accesso all’elenco delle persone.

14.5. Test del livello [dao]

Viene scritto un test JUnit per il livello [dao]:

[TestDao] è il test JUnit. Per evidenziare i problemi di accesso concorrente all’elenco delle persone, vengono creati thread di tipo [ThreadDaoMajEnfants]. Il loro compito è aumentare di 1 il numero di figli di una determinata persona.

[TestDao] comprende cinque test, da [test1] a [test5]. Ne presentiamo solo due; il lettore è invitato a scoprire gli altri nel codice sorgente associato a questo articolo.

package istia.st.springmvc.personnes.tests;

import java.text.ParseException;
...

public class TestDao extends TestCase {

     // livello [dao]
    private DaoImpl dao;

     // costruttore
    public TestDao() {
        dao = new DaoImpl();
        dao.init();
    }

     // elenco delle persone
    private void doListe(Collection personnes) {
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // modifica-eliminazione di un elemento inesistente
    public void test2() throws ParseException {
...
    }

     // gestione delle versioni delle persone
    public void test3() throws ParseException, InterruptedException {
...
    }

     // blocco ottimistico - accesso multithread
    public void test4() throws Exception {
...
    }

     // test di validità di saveOne
    public void test5() throws ParseException {
    ...
}
  • riga 9: riferimento all’implementazione del livello [dao] sottoposto a test
  • righe 12-15: il costruttore del test JUnit. Crea un'istanza di tipo [DaoImpl] del livello [dao] da testare e la inizializza.

Il metodo [test1] verifica i quattro metodi dell’interfaccia [IDao] nel modo seguente:

    public void test1() throws ParseException {
         // elenco attuale
        Collection personnes = dao.getAll();
        int nbPersonnes = personnes.size();
         // visualizzazione
        doListe(personnes);
         // aggiunta di una persona
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // verifica - si verificherà un errore se la persona non viene trovata
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getNom());
         // modifica
        p1.setNom("Y");
        dao.saveOne(p1);
         // verifica - si verificherà un crash se la persona non viene trovata
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getNom());
         // eliminazione
        dao.deleteOne(id1);
         // verifica
        int codeErreur = 0;
        boolean erreur = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // si dovrebbe verificare un errore con codice 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // elenco delle persone
        personnes = dao.getAll();
        assertEquals(nbPersonnes, personnes.size());
    }
  • riga 3: si richiede l’elenco delle persone
  • riga 6: si visualizza l'elenco
[1,1,Joachim,Major,13/01/1984,true,2]
[2,1,Mélanie,Humbort,12/01/1985,false,1]
[3,1,Charles,Lemarchand,01/01/1986,false,0]

Il test aggiunge quindi una persona, la modifica e la elimina. In questo modo vengono utilizzati i quattro metodi dell'interfaccia [IDao].

  • righe 8-10: si aggiunge una nuova persona (id=-1).
  • riga 11: si recupera l’ID della persona aggiunta, poiché l’aggiunta le ha assegnato un ID. Prima non ne aveva uno.
  • righe 13-14: si richiede al livello [dao] una copia della persona appena aggiunta. È importante ricordare che se la persona richiesta non viene trovata, il livello [dao] genera un'eccezione. Si verificherà quindi un arresto anomalo alla riga 13. Si sarebbe potuto gestire questo caso in modo più corretto. Alla riga 14, si verifica il nome della persona trovata.
  • righe 16-17: si modifica tale nome e si richiede al livello [dao] di salvare le modifiche.
  • righe 19-20: si richiede al livello [dao] una copia della persona appena aggiunta e si verifica il suo nuovo nome.
  • riga 22: si elimina la persona aggiunta all’inizio del test.
  • righe 23-34: si richiede al livello [dao] una copia della persona appena eliminata. Si dovrebbe ottenere un [DaoException] con codice 2.
  • righe 36-37: si richiede nuovamente l’elenco delle persone. Si dovrebbe ottenere lo stesso elenco presente all’inizio del test.

Il metodo [test4] mira a evidenziare i problemi di accesso concorrente ai metodi del livello [dao]. Ricordiamo che questi ultimi non sono stati sincronizzati. Il codice del test è il seguente:

    public void test4() throws Exception {
         // aggiunta di una persona
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // creazione di N thread per l'aggiornamento del numero di figli
        final int N = 10;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
         // si attende il completamento dei thread
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // si recupera la persona
        p1 = dao.getOne(id1);
         // deve avere N figli
        assertEquals(N, p1.getNbEnfants());
         // eliminazione della persona p1
        dao.deleteOne(p1.getId());
         // verifica
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // deve esserci un errore con codice 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }
  • righe 3-6: si aggiunge all’elenco una persona P senza figli. Si annota il suo [id] (riga 6).
  • righe 7-13: si avviano N thread. Ciascuno di essi incrementerà di 1 unità il numero di figli della persona P. Alla fine, la persona P dovrà avere N figli.
  • righe 15-17: il metodo [test4] che ha avviato i N thread attende che questi abbiano completato il loro lavoro prima di verificare il nuovo numero di figli della persona P.
  • righe 18-21: si recupera la persona P e si verifica che il numero dei suoi figli sia pari a N.
  • righe 22-35: la persona P viene rimossa, quindi si verifica che non sia più presente nell’elenco.

Alla riga 11 si vede che i thread sono di tipo [ThreadDaoMajEnfants]. Il costruttore di questo tipo ha tre parametri:

  1. il nome assegnato al thread, per poterlo monitorare tramite i log
  2. un riferimento al livello [dao] affinché il thread possa accedervi
  3. l'ID della persona su cui il thread deve lavorare

Il tipo [ThreadDaoMajEnfants] è il seguente:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
     // nome del thread
    private String name;
     // riferimento al livello [dao]
    private IDao dao;
     // ID della persona su cui si lavorerà
    private int idPersonne;

     // costruttore
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

     // nucleo del thread
    public void run() {
         // monitoraggio
        suivi("lancé");
         // si ripete il ciclo finché non si riesce ad incrementare di 1
         // il numero di figli della persona idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // si recupera una copia della persona da idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // monitoraggio
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version "+personne.getVersion());
             // attesa di 10 ms per liberare il processore
            try {
                 // seguito
                suivi("début attente");
                 // si interrompe per lasciare spazio al processore
                Thread.sleep(10);
                 // monitoraggio
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // attesa terminata - si tenta di convalidare la copia
             // nel frattempo altri thread potrebbero aver modificato l'originale
            int codeErreur = 0;
            try {
                 // si incrementa di 1 il numero di figli di questa copia
                personne.setNbEnfants(nbEnfants + 1);
                 // si sta tentando di modificare l'originale
                dao.saveOne(personne);
                 // operazione riuscita - l'originale è stato modificato
                fini = true;
            } catch (DaoException ex) {
                 // viene recuperato il codice di errore
                codeErreur = ex.getCode();
                 // deve trattarsi di un errore della versione 3 - altrimenti si riprova
                 // l'eccezione
                if (codeErreur != 3) {
                    throw ex;
                } else {
                     // monitoraggio
                    suivi(ex.getMessage());
                }
                 // l'originale è cambiato - si ricomincia da capo
            }
        }
         // seguito
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

     // follow-up
    private void suivi(String message) {
        System.out
                .println(name + " [" + new Date().getTime()+ "] : " + message);
    }
}
  • riga 9: [ThreadDaoMajEnfants] è effettivamente un thread
  • righe 18-22: il costruttore che inizializza il thread con tre informazioni
    1. il nome [name] assegnato al thread
    2. un riferimento [dao] al livello [dao]. Si noti che, ancora una volta, stiamo lavorando con il tipo dell’interfaccia [IDao] e non con quello dell’implementazione [DaoImpl].
    3. l’identificativo [id] della persona su cui il thread deve operare

Quando [test4] avvia un thread [ThreadDaoMajEnfants] (riga 12 di test4), viene eseguito il metodo [run] (riga 25) di quest’ultimo:

  • righe 78-81: il metodo privato [suivi] consente di generare log a schermo. Il metodo [run] lo utilizza per consentire il monitoraggio dell’esecuzione del thread.
  • il thread cercherà di incrementare di 1 il numero di figli della persona P con identificativo [id]. Questo aggiornamento potrebbe richiedere diversi tentativi. Prendiamo due thread [TH1] e [TH2]. [TH1] richiede una copia della persona P al livello [dao]. La ottiene e constata che ha la versione V1. [TH1] viene interrotto. [TH2], che lo seguiva, fa la stessa cosa e ottiene la stessa versione V1 della persona P. [TH2] viene interrotto. [TH2] riprende il controllo, incrementa il numero di figli di P e salva le sue modifiche. Sappiamo che a questo punto queste sono state salvate e che la versione di P passerà a V2. [TH1] ha terminato il proprio lavoro. [TH2] riprende il controllo e fa lo stesso. Il suo aggiornamento di P verrà rifiutato poiché possiede una copia di P della versione V1, mentre l’originale P ha ora la versione V2. [TH2] deve quindi ripetere l’intero ciclo di [lecture -> mise à jour -> sauvegarde]. Ecco perché troviamo il ciclo nelle righe 32-72. In esso, il thread:
  • richiede una copia della persona P da modificare (riga 34)
  • attende 10 ms (riga 43). Si tratta di un'operazione artificiale volta a interrompere il thread tra la lettura della persona P e il suo effettivo aggiornamento nell'elenco delle persone, al fine di aumentare la probabilità di conflitti.
  • incrementa il numero di figli di P (riga 54) e salva P (riga 56). Se il thread non dispone della versione corretta di P, verrà generata un’eccezione dal livello [dao]. Si recupera quindi il codice dell’eccezione (riga 61) per verificare che sia effettivamente il codice 3 (versione errata di P). Se così non fosse, si rilancia l’eccezione al metodo chiamante, ovvero il metodo di test [test4]. Se si verifica l’eccezione con codice 3, si ricomincia il ciclo [lecture -> mise à jour -> sauvegarde]. Se non si verifica alcuna eccezione, l’aggiornamento è stato effettuato e il lavoro del thread è terminato.

Quali sono i risultati dei test?

Nella prima configurazione testata:

  • si commenta l’istruzione di attesa nel metodo [saveOne] di [DaoImpl] (riga 83, paragrafo 14.4).
         // si attende 10 ms
         //wait(10);
  • il metodo [test4] crea 100 thread (riga 8, paragrafo 14.5).
         // creazione di N thread per l'aggiornamento del numero di figli
        final int N = 100;

Si ottengono i seguenti risultati:

Image

I cinque test sono stati superati con successo.

Nella seconda configurazione testata:

  • si rimuove il commento dall'istruzione di attesa nel metodo [saveOne] di [DaoImpl] (riga 83, paragrafo 14.4).
         // si attende 10 ms
        wait(10);
  • il metodo [test4] crea 2 thread (riga 8, paragrafo 14.5).
         // creazione di N thread per l'aggiornamento del numero di figli
        final int N = 2;

Si ottengono i seguenti risultati:

Il test [test4] non è andato a buon fine. Sono stati creati due thread, ciascuno incaricato di incrementare di 1 il numero di figli di una persona P che inizialmente ne aveva 0. Ci si aspettava quindi 2 figli dopo l'esecuzione dei due thread, ma ne abbiamo solo uno.

Esaminiamo i log di schermo di [test4] per capire cosa è successo:

thread n° 0 [1145536368171] : lancé
thread n° 0 [1145536368171] : 0 -> 1 pour la version 1
thread n° 0 [1145536368171] : début attente
thread n° 1 [1145536368171] : lancé
thread n° 1 [1145536368171] : 0 -> 1 pour la version 1
thread n° 1 [1145536368171] : début attente
thread n° 0 [1145536368187] : fin attente
thread n° 1 [1145536368187] : fin attente
thread n° 0 [1145536368187] : a terminé et passé le nombre d'enfants à 1
thread n° 1 [1145536368187] : a terminé et passé le nombre d'enfants à 1
  • riga 1: il thread n. 0 inizia il suo lavoro
  • riga 2: ha recuperato una copia della persona P e rileva che il numero dei suoi figli è pari a 0
  • riga 3: incontra il [Thread.sleep(10)] del proprio metodo [run] e si ferma quindi al tempo [1145536368171] (ms)
  • riga 4: il thread n. 1 riprende quindi il controllo del processore e inizia il proprio lavoro
  • riga 5: ha recuperato una copia della persona P e rileva che il numero dei suoi figli è pari a 0
  • riga 6: incontra il [Thread.sleep(10)] del proprio metodo [run] e quindi si interrompe
  • riga 7: il thread n. 0 riprende il controllo del processore al tempo [1145536368187] (ms), c.a.d, 16 ms dopo averlo perso.
  • riga 8: lo stesso vale per il thread n. 1
  • riga 9: il thread n. 0 ha effettuato l’aggiornamento e portato il numero di figli a 1
  • riga 10: lo stesso vale per il thread n. 1

La domanda è: perché il thread n. 1 è riuscito ad aggiornare i propri dati quando, normalmente, non possedeva più la versione corretta della persona P, che era appena stata aggiornata dal thread n. 0?

Innanzitutto, si nota un’anomalia tra le righe 7 e 8: sembrerebbe che il thread n. 0 abbia perso il controllo del processore tra queste due righe a favore del thread n. 1. Cosa stava facendo in quel momento? Stava eseguendo il metodo [saveOne] del livello [dao]. Quest’ultimo ha la seguente struttura (cfr. paragrafo 14.4):

    public void saveOne(Personne personne) {
...
         // modifica - si cerca la persona
....
         // si dispone della versione corretta dell'originale?
...
         // si attende 10 ms
        wait(10);
         // Va bene - si esegue la modifica
    ...
}
  • il thread n. 0 ha eseguito [saveOne] ed è arrivato fino alla riga 8, dove è stato costretto a cedere il processore. Nel frattempo, ha letto la versione della persona P, che era 1 perché la persona P non era ancora stata aggiornata.
  • Poiché il processore si era liberato, è stato il thread n. 1 a rilevarlo. A sua volta, ha eseguito [saveOne] ed è arrivato alla riga 8, dove è stato costretto a rilasciare il processore. Nel frattempo, ha letto la versione della persona P, che era 1 poiché la persona P non era ancora stata aggiornata.
  • Poiché il processore era diventato libero, è stato il thread n. 0 a ereditarlo. A partire dalla riga 9, ha effettuato l’aggiornamento e ha portato il numero di figli a 1. Successivamente, il metodo [run] del thread n. 0 si è concluso e il thread ha visualizzato il log che indicava di aver portato il numero di figli a 1 (riga 9).
  • Poiché il processore era tornato libero, il thread n. 1 ne ha preso il controllo. A partire dalla riga 9, ha effettuato l’aggiornamento e ha impostato il numero di figli a 1. Perché 1? Perché possiede una copia di P con un numero di figli pari a 0. È quanto indica il log (riga 5). Successivamente, il metodo [run] del thread n. 1 si è concluso e il thread ha visualizzato il log che indicava di aver impostato il numero di figli a 1 (riga 10).

Da dove deriva il problema? Deriva dal fatto che il thread n. 0 non ha avuto il tempo di confermare la propria modifica e quindi di aggiornare la versione della persona P prima che il thread n. 1 tentasse di leggere tale versione per verificare se la persona P fosse cambiata. Questo scenario è improbabile ma non impossibile. È stato necessario forzare il thread n. 0 a cedere il processore per farlo emergere con soli due thread. Senza questo espediente, la configurazione precedente non era riuscita a far emergere lo stesso caso con 100 thread. Il test [test4] era stato superato.

Qual è la soluzione? Ce ne sono senza dubbio diverse. Una di queste, semplice da implementare, consiste nel sincronizzare il metodo [saveOne]:


    public synchronized void saveOne(Personne personne)

La parola chiave [synchronized] garantisce che un solo thread alla volta possa eseguire il metodo. In questo modo, il thread n. 1 sarà autorizzato a eseguire [saveOne] solo quando il thread n. 0 ne sarà uscito. Si ha quindi la certezza che la versione della persona P sarà stata modificata quando il thread n. 1 entrerà in [saveOne]. Il suo aggiornamento verrà quindi rifiutato poiché non disporrà della versione corretta di P.

Sono i quattro metodi del livello [dao] che andrebbero sincronizzati. Decidiamo tuttavia di mantenere questo livello così come è stato descritto e di spostare la sincronizzazione sul livello [service]. Ciò per diversi motivi:

  • partiamo dal presupposto che l’accesso al livello [dao] avvenga sempre tramite un livello [service]. È quanto avviene nella nostra applicazione web.
  • potrebbe essere necessario sincronizzare anche l’accesso ai metodi del livello [service] per motivi diversi da quelli che ci porterebbero a sincronizzare quelli del livello [dao]. In questo caso, non è necessario sincronizzare i metodi del livello [dao]. Se si è certi che:
  • ogni accesso al livello [dao] passa attraverso il livello [service]
  • che il livello [service] venga utilizzato da un solo thread alla volta

allora si ha la certezza che i metodi del livello [dao] non saranno eseguiti da due thread contemporaneamente.

Ora esaminiamo il livello [service].

14.6. Il livello [service]

Il livello [service] è costituito dalle seguenti classi e interfacce:

Image

  • [IService] è l’interfaccia presentata dal livello [dao]
  • [ServiceImpl] ne costituisce un’implementazione

L’interfaccia [IService] è la seguente:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // elenco di tutte le persone
    Collection getAll();
     // recuperare una persona specifica
    Personne getOne(int id);
     // aggiungere/modificare una persona
    void saveOne(Personne personne);
     // eliminare una persona
    void deleteOne(int id);
}

È identica all'interfaccia [IDao].

L'implementazione [ServiceImpl] dell'interfaccia [IService] è la seguente:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.dao.IDao;
import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public class ServiceImpl implements IService {

     // il livello [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

     // elenco delle persone
    public synchronized Collection getAll() {
        return dao.getAll();
    }

     // recuperare una persona specifica
    public synchronized Personne getOne(int id) {
        return dao.getOne(id);
    }

     // aggiungere o modificare una persona
    public synchronized void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // eliminazione di una persona
    public synchronized void deleteOne(int id) {
        dao.deleteOne(id);
    }
}
  • righe 10-19: l’attributo [IDao dao] è un riferimento al livello [dao]. Verrà inizializzato da Spring IoC.
  • righe 22-24: implementazione del metodo [getAll] dell’interfaccia [IService]. Il metodo si limita a delegare la richiesta al livello [dao].
  • righe 27-29: implementazione del metodo [getOne] dell’interfaccia [IService]. Il metodo si limita a delegare la richiesta al livello [dao].
  • righe 32-34: implementazione del metodo [saveOne] dell’interfaccia [IService]. Il metodo si limita a delegare la richiesta al livello [dao].
  • righe 37-39: implementazione del metodo [deleteOne] dell’interfaccia [IService]. Il metodo si limita a delegare la richiesta al livello [dao].
  • Tutti i metodi sono sincronizzati (parola chiave `synchronized`), garantendo che un solo thread alla volta possa utilizzare il livello [service] e, di conseguenza, il livello [dao].

14.7. Test del livello [service]

È stato scritto un test JUnit per il livello [service]:

[TestService] è il test JUnit. I test effettuati sono rigorosamente identici a quelli effettuati per il livello [dao]. Lo scheletro di [TestService] è il seguente:

package istia.st.springmvc.personnes.tests;

...

public class TestService extends TestCase {

     // livello [service]
    private ServiceImpl service;

     // costruttore
    public TestService() {
        service = new ServiceImpl();
        DaoImpl dao=new DaoImpl();
        service.setDao(dao);
    }

     // elenco delle persone
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
         // elenco attuale
        Collection personnes = service.getAll();
        int nbPersonnes = personnes.size();
         // visualizzazione
        doListe(personnes);
         // aggiunta di una persona
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        service.saveOne(p1);
        int id1 = p1.getId();
         // verifica - si verificherà un errore se la persona non viene trovata
        p1 = service.getOne(id1);
        assertEquals("X", p1.getNom());
...
    }

     // modifica-eliminazione di un elemento inesistente
    public void test2() throws ParseException {
...
    }

     // gestione delle versioni delle persone
    public void test3() throws ParseException, InterruptedException {
...
    }

     // blocco ottimistico - accesso multithread
    public void test4() throws Exception {
         // aggiunta di una persona
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p1);
        int id1 = p1.getId();
         // creazione di N thread per l'aggiornamento del numero di figli
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadServiceMajEnfants("thread n° " + i, service,
                    id1);
            taches[i].start();
        }
...
    }

     // test di validità di saveOne
    public void test5() throws ParseException {
    ...
    }
}
  • riga 9: il livello [service] testato è di tipo [ServiceImpl].
  • righe 11-15: il generatore di test JUnit crea un'istanza del livello [service] da testare (riga 12), crea un'istanza del livello [dao] (riga 13) e indica al livello [service] di utilizzare tale livello [dao] (riga 14).

Il metodo [test1] verifica i quattro metodi dell’interfaccia [IService] in modo identico al metodo di verifica del livello [dao] con lo stesso nome. Semplicemente, si accede al livello [service] (righe 25, 32, 35) anziché al livello [dao].

Il metodo [test4] mira a evidenziare i problemi di accesso concorrente ai metodi del livello [service]. Anche in questo caso è identico al metodo di test [test4] del livello [dao]. Vi sono tuttavia alcuni dettagli che cambiano:

  • ci si rivolge al livello [service] anziché al livello [dao] (riga 55)
  • ai thread viene passato un riferimento al livello [service] anziché al livello [dao] (riga 61)

Anche il tipo [ThreadServiceMajEnfants] è pressoché identico al tipo [ThreadDaoMajEnfants], con la sola differenza che opera con il livello [service] anziché con il livello [dao]:

package istia.st.springmvc.personnes.tests;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.service.IService;

public class ThreadServiceMajEnfants extends Thread {

     // nome del thread
    private String name;
     // riferimento al livello [service]
    private IService service;
     // ID della persona su cui si lavorerà
    private int idPersonne;

    public ThreadServiceMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    public void run() {
...
    }

     // monitoraggio
    private void suivi(String message) {
        System.out.println(name + " : " + message);
    }

}
  • riga 12: il thread opera con il livello [service]

Eseguiamo i test con la configurazione che ha causato il problema al livello [dao]:

  • si rimuove il commento dall’istruzione di attesa nel metodo [saveOne] di [DaoImpl] (riga 83, paragrafo 14.4).
         // si attende 10 ms
        wait(10);
  • il metodo [test4] crea 100 thread (riga 65, paragrafo 14.7).
         // creazione di N thread per l'aggiornamento del numero di figli
        final int N = 100;

I risultati ottenuti sono i seguenti:

È stata la sincronizzazione dei metodi del livello [service] a consentire il successo del test [test4].

14.8. Il livello [web]

Ricordiamo l'architettura a tre livelli della nostra applicazione:

Il livello [web] fornirà all’utente delle schermate che gli consentiranno di gestire il gruppo di persone:

  • elenco delle persone del gruppo
  • aggiunta di una persona al gruppo
  • modifica di una persona del gruppo
  • eliminazione di una persona dal gruppo

A tal fine, si avvarrà del livello [service], che a sua volta farà ricorso al livello [dao]. Abbiamo già presentato le schermate gestite dal livello [web] (paragrafo 14.1). Per descrivere il livello web, presenteremo in successione:

  • la sua configurazione
  • le sue viste
  • il suo controller
  • alcuni test

14.8.1. Configurazione dell’applicazione web

Il progetto Eclipse dell'applicazione è il seguente:

Image

  • nel pacchetto [istia.st.mvc.personnes.web] si trova il controller [Application].
  • Le pagine JSP / JSTL si trovano in [WEB-INF/vues].
  • La cartella [lib] contiene gli archivi di terze parti necessari all’applicazione. Sono visibili nella cartella [Web App Libraries].

[web.xml]


Il file [web.xml] è il file utilizzato dal server web per caricare l’applicazione. Il suo contenuto è il seguente:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>mvc-personnes-01</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>
            istia.st.mvc.personnes.web.Application
        </servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  Mappatura ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  file di benvenuto -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Pagina di errore imprevisto -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • righe 27-30: gli URL [/do/*] saranno gestiti dal servlet [personnes]
  • righe 9-12: il servlet [personnes] è un'istanza della classe [Application], una classe che creeremo.
  • righe 13-24: definiscono tre parametri [urlList, urlEdit, urlErreurs] che identificano gli URL delle pagine JSP delle viste [list, edit, erreurs].
  • righe 32-34: l’applicazione dispone di una pagina iniziale predefinita [index.jsp] che si trova nella directory principale dell’applicazione web.
  • righe 36-39: l’applicazione dispone di una pagina di errore predefinita che viene visualizzata quando il server web rileva un’eccezione non gestita dall’applicazione.
    • riga 37: il tag <exception-type> indica il tipo di eccezione gestita dalla direttiva <error-page>, in questo caso il tipo [java.lang.Exception] e i suoi derivati, ovvero tutte le eccezioni.
    • riga 38: il tag <location> indica la pagina JSP da visualizzare quando si verifica un'eccezione del tipo definito da <exception-type>. L'eccezione verificatasi è disponibile in questa pagina in un oggetto denominato exception se la pagina contiene la direttiva:

<%@ page isErrorPage="true" %>
  • (continua)
    • se <exception-type> specifica un tipo T1 e un'eccezione di tipo T2 non derivata da T1 viene segnalata al server web, quest'ultimo invia al client una pagina di eccezione proprietaria, solitamente poco intuitiva. Da qui l’utilità del tag <error-page> nel file [web.xml].

[index.jsp]


Questa pagina viene visualizzata se un utente richiede direttamente il contesto dell’applicazione senza specificare un URL, c.a.d. qui [/personnes-01]. Il suo contenuto è il seguente:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/do/list"/>

[index.jsp] reindirizza il client all’URL [/do/list]. Questo URL mostra l’elenco delle persone del gruppo.

14.8.2. Le pagine JSP / JSTL dell’applicazione


La vista [list.jsp]


Serve a visualizzare l’elenco delle persone:

Image

Il suo codice è il seguente:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.dateNaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbEnfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

  • questa vista riceve un elemento nel proprio modello:
  • l'elemento [personnes] associato a un oggetto di tipo [ArrayList] contenente oggetti di tipo [Personne]
  • righe 22-34: si scorre l'elenco ${personnes} per visualizzare una tabella HTML contenente le persone del gruppo.
  • riga 31: l'URL a cui punta il link [Modifier] viene impostato dal campo [id] della persona corrente, in modo che il controller associato all'URL [/do/edit] sappia quale persona deve essere modificata.
  • riga 32: lo stesso vale per il link [Supprimer].
  • riga 28: per visualizzare la data di nascita della persona nel formato JJ/MM/AAAA, si utilizza il tag <dt> della libreria di tag [DateTime] del progetto Apache [Jakarta Taglibs]:

Image

Il file di descrizione di questa libreria di tag è definito alla riga 3.

  • riga 37: il link [Ajout] per l'aggiunta di una nuova persona ha come destinazione l'URL [/do/edit], proprio come il link [Modifier] della riga 31. È il valore -1 del parametro [id] che indica che si tratta di un'aggiunta piuttosto che di una modifica.

La vista [edit.jsp]


Serve a visualizzare il modulo per l’aggiunta di una nuova persona o la modifica di una persona esistente:

Il codice della vista [edit.jsp] è il seguente:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit != ''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${dateNaissance}" name="dateNaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbEnfants}" name="nbEnfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

Questa vista presenta un modulo per l’aggiunta di una nuova persona o l’aggiornamento di una persona esistente. D’ora in poi, per semplificare la scrittura, useremo il termine unico [mise à jour]. Il pulsante [Valider] (riga 73) attiva il POST del modulo all’URL [/do/validate] (riga 16). Se il POST fallisce, viene nuovamente visualizzata la vista [edit.jsp] con l’errore o gli errori verificatisi; in caso contrario, viene visualizzata la vista [list.jsp].

  • La vista [edit.jsp], visualizzata sia su un GET che su un POST che non supera il controllo, riceve i seguenti elementi nel proprio modello:
attributo
GET
POST
id
ID della persona aggiornata
idem
version
la sua versione
idem
prenom
il suo nome
nome inserito
nom
il suo cognome
cognome inserito
dateNaissance
Data di nascita
data di nascita inserita
marie
stato civile
stato civile inserito
nbEnfants
numero di figli
numero di figli inserito
erreurEdit
vuoto
un messaggio di errore che segnala un errore nell'aggiunta o nella modifica al momento dell'esecuzione di POST, causata dal pulsante [Envoyer]. Vuoto se non ci sono errori.
erreurPrenom
vuoto
segnala un nome errato – vuoto in caso contrario
erreurNom
vuoto
segnala un cognome errato – vuoto in caso contrario
erreurDateNaissance
vuoto
segnala una data di nascita errata – vuoto in caso contrario
erreurNbEnfants
vuoto
segnala un numero di figli errato – vuoto in caso contrario
  • righe 11-15: se il codice POST del modulo non viene elaborato correttamente, si otterrà [erreurEdit!=''] e verrà visualizzato un messaggio di errore.
  • riga 16: il modulo verrà inviato all’URL [/do/validate]
  • riga 20: viene visualizzato l'elemento [id] del modello
  • riga 24: viene visualizzato l'elemento [version] del modello
  • righe 26-32: inserimento del nome della persona:
    • alla visualizzazione iniziale del modulo (GET), ${prenom} visualizza il valore attuale del campo [prenom] dell’oggetto [Personne] aggiornato e ${erreurPrenom} è vuoto.
    • in caso di errore dopo il campo POST, viene visualizzato nuovamente il valore inserito ${prenom} insieme all’eventuale messaggio di errore ${erreurPrenom}
  • righe 33-39: inserimento del cognome della persona
  • righe 40-46: inserimento della data di nascita della persona
  • righe 47-61: inserimento dello stato civile della persona tramite un pulsante di opzione. Si utilizza il valore del campo [marie] dell’oggetto [Personne] per determinare quale dei due pulsanti di opzione debba essere selezionato.
  • righe 62-68: inserimento del numero di figli della persona
  • riga 71: un campo nascosto HTML denominato [id], il cui valore corrisponde al campo [id] della persona oggetto dell’aggiornamento; -1 in caso di aggiunta, un altro valore in caso di modifica.
  • riga 72: un campo nascosto HTML denominato [version], il cui valore corrisponde al campo [id] della persona oggetto dell’aggiornamento.
  • riga 73: il pulsante [Valider] di tipo [Submit] del modulo
  • riga 74: un link che consente di tornare all’elenco delle persone. È stato denominato [Annuler] perché permette di uscire dal modulo senza convalidarlo.

La vista [exception.jsp]


Serve a visualizzare una pagina che segnala che si è verificata un'eccezione non gestita dall'applicazione e che è stata segnalata al server web.

Ad esempio, eliminiamo una persona che non esiste nel gruppo:

Il codice della vista [exception.jsp] è il seguente:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>

<%
  response.setStatus(200);
%>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>

  • questa vista riceve nel proprio modello l’elemento [exception], che rappresenta l’eccezione intercettata dal server web. Affinché questo elemento venga incluso nel modello della pagina JSP dal server web, è necessario che la pagina abbia definito il tag alla riga 3.
  • riga 6: si imposta a 200 il codice di stato HTTP della risposta. Si tratta della prima intestazione HTTP della risposta. Il codice 200 indica al client che la sua richiesta è stata soddisfatta. Generalmente, nella risposta del server è stato incluso un documento HTML. È proprio questo il caso. Se il codice di stato HTTP della risposta non viene impostato a 200, in questo caso assumerà il valore 500, il che significa che si è verificato un errore. Infatti, il server web, avendo intercettato un'eccezione non gestita, rileva questa situazione anomala e la segnala con il codice 500. La reazione al codice HTTP 500 varia a seconda dei browser: Firefox visualizza il documento HTML che può accompagnare questa risposta, mentre IE ignora tale documento e visualizza la propria pagina. Per questo motivo abbiamo sostituito il codice 500 con il codice 200.
  • riga 16: viene visualizzato il testo dell’eccezione
  • riga 18: all’utente viene proposto un link per tornare all’elenco delle persone

La vista [erreurs.jsp]


Serve a visualizzare una pagina che segnala gli errori di inizializzazione dell’applicazione, c.a.d, e gli errori rilevati durante l’esecuzione del metodo [init] del servlet del controller. Può trattarsi, ad esempio, della mancanza di un parametro nel file [web.xml], come illustrato nell’esempio riportato di seguito:

Image

Il codice della pagina [erreurs.jsp] è il seguente:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

La pagina riceve nel proprio modello un elemento [erreurs], che è un oggetto di tipo [ArrayList] contenente oggetti [String], i quali a loro volta sono messaggi di errore. Questi vengono visualizzati dal ciclo delle righe 13-15.

14.8.3. Il controller dell’applicazione

Il controller [Application] è definito nel pacchetto [istia.st.mvc.personnes.web]:

Image


Struc tura e inizializzazione del controller


La struttura del controller [Application] è la seguente:

package istia.st.mvc.personnes.web;

import istia.st.mvc.personnes.dao.DaoException;
...

@SuppressWarnings("serial")
public class Application extends HttpServlet {
     // parametri dell'istanza
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

     // servizio
    ServiceImpl service=null;

     // inizializzazione
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
         // si recuperano i parametri di inizializzazione del servlet
        ServletConfig config = getServletConfig();
         // si elaborano gli altri parametri di inizializzazione
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
             // valore del parametro
            valeur = config.getInitParameter(paramètres[i]);
             // il parametro è presente?
            if (valeur == null) {
                 // si registra l'errore
                erreursInitialisation.add("Le paramètre [" + paramètres[i]
                        + "] n'a pas été initialisé");
            } else {
                 // si memorizza il valore del parametro
                params.put(paramètres[i], valeur);
            }
        }
         // l'URL della vista [erreurs] richiede un trattamento particolare
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException(
                    "Le paramètre [urlErreurs] n'a pas été initialisé");
         // istanziazione del livello [dao]
        DaoImpl dao = new DaoImpl();
        dao.init();
         // istanziazione del livello [service]
        service = new ServiceImpl();
        service.setDao(dao);
    }

     // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
....
    }

     // visualizzazione dell'elenco delle persone
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // modifica / aggiunta di una persona
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // conferma della modifica / aggiunta di una persona
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // conferma modifica / aggiunta di una persona
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // visualizzazione del modulo precompilato
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit) throws ServletException, IOException{
...
    }

     // invio
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
         // passaggio di controllo a GET
        doGet(request, response);    }
}
  • righe 20-36: si recuperano i parametri previsti nel file [web.xml].
  • righe 39-41: il parametro [urlErreurs] deve essere obbligatoriamente presente poiché indica l'URL della vista [erreurs] in grado di visualizzare eventuali errori di inizializzazione. Se non esiste, l’applicazione viene interrotta lanciando una [ServletException] (riga 40). Questa eccezione verrà segnalata al server web e gestita dal tag <error-page> del file [web.xml]. Viene quindi visualizzata la vista [exception.jsp]:

Image

Il link [Retour à la liste] sopra riportato non è funzionante. Utilizzandolo si ottiene la stessa risposta finché l’applicazione non viene modificata e ricaricata. È utile per altri tipi di eccezioni, come abbiamo già visto.

  • riga 43: crea un'istanza [DaoImpl] che implementa il livello [dao]
  • riga 44: inizializza questa istanza (creazione di un elenco iniziale di tre persone)
  • riga 46: crea un'istanza [ServiceImpl] che implementa il livello [service]
  • riga 47: inizializza il livello [service] fornendogli un riferimento al livello [dao]

Dopo l’inizializzazione del controller, i suoi metodi dispongono di un riferimento [service] al livello [service] (riga 15) che utilizzeranno per eseguire le azioni richieste dall’utente. Queste azioni verranno intercettate dal metodo [doGet], che le farà elaborare da un metodo specifico del controller:

Url
Metodo HTTP
metodo del controller
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

Il metodo [doGet]


Questo metodo ha lo scopo di indirizzare l'elaborazione delle azioni richieste dall'utente verso il metodo corretto. Il suo codice è il seguente:

// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

         // si verifica l'esito dell'inizializzazione del servlet
        if (erreursInitialisation.size() != 0) {
             // si passa alla pagina degli errori
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
             // fine
            return;
        }
         // si recupera il metodo di invio della richiesta
        String méthode = request.getMethod().toLowerCase();
         // si recupera l'azione da eseguire
        String action = request.getPathInfo();
         // azione?
        if (action == null) {
            action = "/list";
        }
         // esecuzione dell'azione
        if (méthode.equals("get") && action.equals("/list")) {
             // elenco delle persone
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
             // eliminazione di una persona
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
             // visualizzazione del modulo di aggiunta/modifica di una persona
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
             // conferma del modulo di aggiunta/modifica di una persona
            doValidatePersonne(request, response);
            return;
        }
         // altri casi
        doListPersonnes(request, response);
    }
  • righe 7-13: si verifica che l'elenco degli errori di inizializzazione sia vuoto. In caso contrario, viene visualizzata la vista [erreurs(erreurs)] che segnalerà l'errore o gli errori.
  • riga 15: si recupera il metodo [get] o [post] che il cliente ha utilizzato per effettuare la richiesta.
  • riga 17: si recupera il valore del parametro [action] della richiesta.
  • righe 23-27: elaborazione della richiesta [GET /do/list] che richiede l'elenco delle persone.
  • righe 28-32: elaborazione della richiesta [GET /do/delete] che richiede la cancellazione di una persona.
  • righe 33-37: elaborazione della richiesta [GET /do/edit] che richiede il modulo di aggiornamento di una persona.
  • righe 38-42: elaborazione della richiesta [POST /do/validate] che richiede la convalida della persona aggiornata.
  • riga 44: se l'azione richiesta non è una delle cinque precedenti, allora si procede come se fosse [GET /do/list].

Il metodo [doListPersonnes]


Questo metodo elabora la richiesta [GET /do/list] che richiede l'elenco delle persone:

Image

Il suo codice è il seguente:

1
2
3
4
5
6
7
8
9
     // visualizzazione dell'elenco delle persone
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // il modello della vista [list]
        request.setAttribute("personnes", service.getAll());
         // Visualizzazione della vista [list]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • riga 5: si richiede al livello [service] l'elenco delle persone del gruppo e lo si inserisce nel modello sotto la chiave "persone".
  • riga 7: si visualizza la vista [list.jsp] descritta al paragrafo 14.8.2.

Il metodo [doDeletePersonne]


Questo metodo gestisce la richiesta [GET /do/delete?id=XX] che richiede l’eliminazione della persona con id=XX. L'URL [/do/delete?id=XX] è quello dei link [Supprimer] della vista [list.jsp]:

Image

il cui codice è il seguente:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

Alla riga 12 si vede l'URL [/do/delete?id=XX] del link [Supprimer]. Il metodo [doDeletePersonne], che deve elaborare questo URL, deve rimuovere la persona con id=XX e quindi visualizzare il nuovo elenco delle persone del gruppo. Il suo codice è il seguente:

     // conferma della modifica/aggiunta di una persona
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // si recupera l'ID della persona
        int id = Integer.parseInt(request.getParameter("id"));
         // si elimina la persona
        service.deleteOne(id);
         // reindirizzamento all'elenco delle persone
        response.sendRedirect("list");
    }
  • riga 5: l’URL elaborato è della forma [/do/delete?id=XX]. Si recupera il valore [XX] dal parametro [id].
  • riga 7: si richiede al livello [service] l’eliminazione della persona con l’ID ottenuto. Non effettuiamo alcuna verifica. Se la persona che si intende eliminare non esiste, il livello [dao] genera un’eccezione che viene propagata dal livello [service]. Non la gestiamo nemmeno qui, nel controller. Verrà quindi inoltrata al server web che, in base alla configurazione, visualizzerà la pagina [exception.jsp], descritta al paragrafo 14.8.2:

Image

  • riga 9: se l’eliminazione è avvenuta (nessuna eccezione), si richiede al client di reindirizzarsi all’URL relativo [list]. Poiché quella appena elaborata è [/do/delete], l’URL di reindirizzamento sarà [/do/list]. Il browser verrà quindi indirizzato a [GET /do/list], che provocherà la visualizzazione dell’elenco delle persone.

Il metodo [doEditPersonne]


Questo metodo gestisce la richiesta [GET /do/edit?id=XX] che richiede il modulo di aggiornamento della persona con id=XX. L'URL [/do/edit?id=XX] è quello dei link [Modifier] e del link [Ajout] della vista [list.jsp]:

Image

il cui codice è il seguente:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

Alla riga 11 si vede l'URL [/do/edit?id=XX] del link [Modifier] e alla riga 17 l'URL [/do/edit?id=-1] del link [Ajout]. Il metodo [doEditPersonne] deve visualizzare il modulo di modifica della persona con id=XX oppure, se si tratta di un'aggiunta, presentare un modulo vuoto.

Il codice del metodo [doEditPersonne] è il seguente:

     // modifica / aggiunta di una persona
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // si recupera l'ID della persona
        int id = Integer.parseInt(request.getParameter("id"));
         // Aggiunta o modifica?
        Personne personne = null;
        if (id != -1) {
             // modifica - si recupera la persona da modificare
            personne = service.getOne(id);
        } else {
             // aggiunta - si crea una persona vuota
            personne = new Personne();
            personne.setId(-1);
        }
         // si inserisce l'oggetto [Personne] nel modello della vista [edit]
        request.setAttribute("erreurEdit", "");
        request.setAttribute("id", personne.getId());
        request.setAttribute("version", personne.getVersion());
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDateNaissance();
        if (dateNaissance != null) {
            request.setAttribute("dateNaissance", new SimpleDateFormat(
                    "dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("dateNaissance", "");
        }
        request.setAttribute("marie", personne.getMarie());
        request.setAttribute("nbEnfants", personne.getNbEnfants());
         // visualizzazione della vista [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • il GET ha come destinazione un URL del tipo [/do/edit?id=XX]. Alla riga 5, recuperiamo il valore di [id]. Successivamente si presentano due casi:
  1. se id è diverso da -1, si tratta di una modifica e occorre visualizzare un modulo precompilato con le informazioni della persona da modificare. Alla riga 10, questa persona viene richiesta al livello [service].
  2. id è uguale a -1. In questo caso si tratta di un’aggiunta ed è necessario visualizzare un modulo vuoto. A tal fine, alle righe 13-14 viene creata una persona vuota.
  • L’oggetto [Personne] ottenuto viene inserito nel modello della pagina [edit.jsp] descritto al paragrafo 14.8.2. Questo modello comprende i seguenti elementi [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants]. Questi elementi vengono inizializzati alle righe 17-30, ad eccezione di quelli il cui valore è la stringa vuota [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. È noto che, in caso di loro assenza nel modello, la libreria JSTL visualizzerà una stringa vuota come loro valore. Sebbene anche l’elemento [erreurEdit] abbia come valore una stringa vuota, viene comunque inizializzato poiché nella pagina [edit.jsp] viene eseguito un test sul suo valore.
  • Una volta pronto il modello, il controllo passa alla pagina [edit.jsp], righe 32-33, che genererà la vista [edit].

Il metodo [doValidatePersonne]


Questo metodo gestisce la richiesta [POST /do/validate] che convalida il modulo di aggiornamento. Questa richiesta POST viene attivata dal pulsante [Valider]:

Image

Ricordiamo gli elementi di immissione dati del modulo HTML della vista sopra riportata:

<form method="post" action="<c:url value="/do/validate"/>">
....
        <input type="text" value="${prenom}" name="prenom" size="20">
....
        <input type="text" value="${nom}" name="nom" size="20">
....
        <input type="text" value="${dateNaissance}" name="dateNaissance">
...
        <input type="radio" name="marie" value="true" checked>Oui
....
        <input type="text" value="${nbEnfants}" name="nbEnfants">
....
            <input type="hidden" value="${id}" name="id">
     <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
</form>

La richiesta POST contiene i parametri [prenom, nom, dateNaissance, marie, nbEnfants, id, version] e viene inviata all'URL [/do/validate] (riga 1). Viene elaborata dal seguente metodo [doValidatePersonne]:

// conferma della modifica/aggiunta di una persona
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // recupero degli elementi inviati
        boolean formulaireErroné = false;
        boolean erreur;
         // il nome
        String prenom = request.getParameter("prenom").trim();
         // nome valido?
        if (prenom.length() == 0) {
             // si registra l'errore
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
         // il cognome
        String nom = request.getParameter("nom").trim();
         // nome valido?
        if (nom.length() == 0) {
             // si rileva l'errore
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
         // data di nascita
        Date dateNaissance = null;
        try {
            dateNaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateNaissance").trim());
        } catch (ParseException e) {
             // si rileva l'errore
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
         // stato civile
        boolean marie = Boolean.parseBoolean(request.getParameter("marie"));
         // numero di figli
        int nbEnfants = 0;
        erreur = false;
        try {
            nbEnfants = Integer.parseInt(request.getParameter("nbEnfants")
                    .trim());
            if (nbEnfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
             // si rileva l'errore
            erreur = true;
        }
         // numero di figli errato?
        if (erreur) {
             // viene segnalato l'errore
            request.setAttribute("erreurNbEnfants",
                    "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
         // ID della persona
        int id = Integer.parseInt(request.getParameter("id"));
         // versione
        long version = Long.parseLong(request.getParameter("version"));
         // Il modulo è errato?
        if (formulaireErroné) {
             // si visualizza nuovamente il modulo con i messaggi di errore
            showFormulaire(request, response, "");
             // fine
            return;
        }
         // il modulo è corretto - si registra la persona
        Personne personne = new Personne(id, prenom, nom, dateNaissance, marie,
                nbEnfants);
        personne.setVersion(version);
        try {
             // registrazione
            service.saveOne(personne);
        } catch (DaoException ex) {
             // si visualizza nuovamente il modulo con il messaggio relativo all'errore verificatosi
            showFormulaire(request, response, ex.getMessage());
             // fine
            return;
        }
         // si reindirizza all'elenco delle persone
        response.sendRedirect("list");
    }

     // visualizzazione del modulo precompilato
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit)
            throws ServletException, IOException {
         // si prepara il modello della vista [edit]
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("dateNaissance", request.getParameter(
                "dateNaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbEnfants", request.getParameter("nbEnfants")
                .trim());
         // visualizzazione della vista [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • righe 8-14: viene recuperato il parametro [prenom] della richiesta POST e ne viene verificata la validità. Se risulta errato, l’elemento [erreurPrenom] viene inizializzato con un messaggio di errore e inserito negli attributi della richiesta.
  • righe 16-22: si procede in modo analogo per il parametro [nom]
  • righe 24-32: si procede in modo analogo per il parametro [dateNaissance]
  • riga 34: si recupera il parametro [marie]. Non si verifica la sua validità perché, a priori, deriva dal valore di un pulsante di opzione. Detto questo, nulla impedisce a un programma di generare un [POST /personnes-01/do/validate] accompagnato da un parametro [marie] inventato. Dovremmo quindi verificare la validità di questo parametro. In questo caso, ci affidiamo alla nostra gestione delle eccezioni, che provoca la visualizzazione della pagina [exception.jsp] se il controller non le gestisce autonomamente. Se quindi la conversione del parametro [marie] in valore booleano fallisce alla riga 34, verrà generata un’eccezione che porterà all’invio della pagina [exception.jsp] al cliente. Questo funzionamento ci soddisfa.
  • righe 34-54: si recupera il parametro [nbEnfants] e se ne verifica il valore.
  • riga 56: si recupera il parametro [id] senza verificarne il valore
  • riga 58: si procede allo stesso modo per il parametro [version]
  • righe 60-65: se il modulo contiene errori, viene visualizzato nuovamente con i messaggi di errore generati in precedenza
  • righe 67-69: se è valido, si crea un nuovo oggetto [Personne] con gli elementi del modulo
  • righe 70-78: il profilo viene salvato. Il salvataggio potrebbe non andare a buon fine. In un ambiente multiutente, il profilo da modificare potrebbe essere stato eliminato o già modificato da qualcun altro. In questo caso, il livello [dao] genererà un'eccezione che viene gestita qui.
  • riga 80: se non si è verificata alcuna eccezione, si reindirizza il cliente all’URL [/do/list] per mostrargli il nuovo stato del gruppo.
  • riga 75: se si è verificata un’eccezione durante il salvataggio, si richiede nuovamente la visualizzazione del modulo iniziale passando il messaggio di errore dell’eccezione (terzo parametro).

Il metodo [showFormulaire] (righe 84-101) costruisce il modello necessario per la pagina [edit.jsp] con i valori inseriti (request.getParameter(" ... ")). Si ricorda che i messaggi di errore sono già stati inseriti nel modello dal metodo [doValidatePersonne]. La pagina [edit.jsp] viene visualizzata alle righe 99-100.

14.9. I test dell’applicazione web

Nel paragrafo 14.1 sono stati presentati alcuni test. Invitiamo il lettore a ripeterli. Mostriamo qui altre schermate che illustrano i casi di conflitti di accesso ai dati in un contesto multiutente:

[Firefox] sarà il browser dell’utente U1. Quest’ultimo richiede l’URL [http://localhost:8080/personnes-01]:

Image

[IE] sarà il browser dell’utente U2. Quest’ultimo richiede lo stesso URL:

Image

L’utente U1 accede alla modifica della persona [Lemarchand]:

Image

L’utente U2 fa lo stesso:

Image

L'utente U1 apporta delle modifiche e conferma:

Anche l'utente U2 fa lo stesso:

L'utente U2 torna all'elenco delle persone tramite il link [Annuler] del modulo:

Image

Trova la persona [Lemarchand] così come è stata modificata da U1. Ora U2 elimina [Lemarchand]:

U1 ha ancora il proprio elenco e vuole modificare nuovamente [Lemarchand]:

U1 utilizza il link [Retour à la liste] per vedere di cosa si tratta:

Image

Scopre che, effettivamente, [Lemarchand] non fa più parte dell’elenco...

14.10. Conclusion

Abbiamo implementato l’architettura MVC in un’architettura a tre livelli [web, metier, dao] su un esempio di base di gestione di un elenco di persone. Questo ci ha permesso di utilizzare i concetti presentati nelle sezioni precedenti. Nella versione esaminata, l’elenco delle persone era mantenuto in memoria. Presto esamineremo versioni in cui tale elenco sarà conservato in una tabella del database.

Ma prima presenteremo uno strumento chiamato Spring IoC, che facilita l'integrazione dei diversi livelli di un'applicazione ntier.