Skip to content

3. Articolo 2 - Esempi di architetture web a tre livelli

Obiettivi dell’articolo:

  • architetture a 3 livelli
  • architettura web di base MVC
  • architettura Struts MVC
  • Architettura Spring MVC

Strumenti utilizzati:

  • Spring: http://www.springframework.org/
  • Ibatis SqlMap: http://www.ibatis.com/
  • JUnit: http://www.junit.org/index.htm
  • Eclipse: http://www.eclipse.org/
  • Struts: http://struts.apache.org/
  • Firebird: http://firebird.sourceforge.net/: SGBD, driver JDBC. In realtà, qualsiasi versione di JDBC va bene.
  • IBExpert, edizione personale: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (marzo 2005). IBExpert consente di gestire graficamente SGBD Firebird.
  • Tomcat: http://jakarta.apache.org/tomcat/
  • Plugin Tomcat per Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. Vedi anche il documento https://tahe.developpez.com/java/eclipse/

La comprensione di questo documento richiede diversi prerequisiti. Alcuni di essi possono essere acquisiti nei documenti che ho scritto. In tal caso, li cito. È ovvio che si tratta solo di un suggerimento e che il lettore può utilizzare i propri documenti preferiti.

  • Linguaggio Java: [https://tahe.developpez.com/java/cours]
  • programmazione web in Java: [https://tahe.developpez.com/java/web/]
  • programmazione web con Java, Eclipse, Tomcat: [https://tahe.developpez.com/java/eclipse/]
  • programmazione web con Struts: [https://tahe.developpez.com/java/struts/]
  • utilizzo dell'aspetto IoC di Spring: [https://tahe.developpez.com/java/springioc]
  • Libreria di tag JSTL: [https://tahe.developpez.com/java/eclipse/] (in parte)
  • Documentazione Ibatis SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (marzo 2005).

Le idee contenute in questo documento traggono origine da un libro letto nell'estate del 2004, un magnifico lavoro di Rod Johnson: J2EE Development without EJB, edito da Wrox.


3.1. L'applicazione webarticles

Desideriamo presentare qui alcuni elementi di un’applicazione web di e-commerce. Questa consentirà agli utenti del web

  • di consultare un elenco di articoli provenienti da un database
  • di aggiungerne alcuni a un carrello elettronico
  • e di confermarlo. Tale conferma avrà come unico effetto l’aggiornamento, nel database, delle scorte degli articoli acquistati.

Le diverse viste presentate all’utente saranno le seguenti:

  • la vista [LISTE] che presenta un elenco degli articoli in vendita

Image

  • la vista [INFOS] che fornisce ulteriori informazioni su un prodotto:

Image

  1. la vista [PANIER] che mostra il contenuto del carrello del cliente

Image

  1. la vista [PANIERVIDE] nel caso in cui il carrello del cliente sia vuoto

Image

  1. la vista [ERREURS] che segnala eventuali errori dell'applicazione

Image

3.2. Architettura generale dell'applicazione

Si desidera realizzare un'applicazione con la seguente struttura a tre livelli:

  • i tre livelli sono resi indipendenti grazie all'utilizzo di interfacce Java
  • l'integrazione dei diversi livelli è realizzata tramite Spring
  • ogni livello è costituito da pacchetti separati: web (livello interfaccia utente), domain (livello business) e dao (livello di accesso ai dati).

In questa sede supporremo che i livelli [domain] e [dao] siano già realizzati. Ci concentreremo esclusivamente sul livello [web], che intendiamo realizzare in diversi modi:

  • utilizzando una tecnologia classica basata su servlet controller - pagine JSP
  • utilizzando la tecnologia Struts MVC
  • utilizzando la tecnologia Spring MVC

In ogni caso, l’applicazione rispetterà un’architettura MVC (Modello - Vista - Controller). Se riprendiamo lo schema a livelli riportato sopra, l’architettura MVC si integra in esso nel modo seguente:

L'elaborazione di una richiesta da parte di un cliente si svolge secondo le seguenti fasi:

  1. il cliente invia una richiesta al controller. Questo controller è un servlet che gestisce tutte le richieste dei clienti. Rappresenta il punto di ingresso dell’applicazione. È la «C» di MVC.
  2. il controller elabora questa richiesta. A tal fine, potrebbe aver bisogno dell’aiuto del livello di business, ovvero il modello M nella struttura MVC.
  3. Il controller riceve una risposta dal livello di business. La richiesta del cliente è stata elaborata. Ciò può comportare diverse possibili risposte. Un esempio classico è
    • una pagina di errore se la richiesta non è stata elaborata correttamente
    • una pagina di conferma in caso contrario
  4. il controller sceglie la risposta (= vista) da inviare al cliente. Si tratta, nella maggior parte dei casi, di una pagina contenente elementi dinamici. Il controller fornisce tali elementi alla vista.
  5. La vista viene inviata al cliente. È la V di MVC.

3.3. Il modello

Qui esaminiamo la M di MVC. Il modello è costituito dai seguenti elementi:

  1. le classi di business
  2. le classi di accesso ai dati
  3. il database

3.3.1. Il database

Il database contiene una sola tabella denominata ARTICLES. Questa è stata generata con i seguenti comandi SQL:

CREATE TABLE ARTICLES (
    ID            INTEGER NOT NULL,
    NOM           VARCHAR(30) NOT NULL,
    PRIX          NUMERIC(15,2) NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);


/* vincoli */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');

/* chiave primaria */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
chiave primaria che identifica in modo univoco un articolo
nom
nome dell’articolo
prix
il suo prezzo
stockactuel
disponibilità attuale
stockminimum
la quantità di magazzino al di sotto della quale è necessario effettuare un ordine di rifornimento

Nei test che seguono è stato utilizzato un database [Firebird]. [Firebird] è un SGBD "open source". Il driver JDBC [firebirdsql-full.jar] è collocato nella cartella [WEB-INF/lib] dell'applicazione web.

3.3.2. I pacchetti del modello

Il modello M è qui fornito sotto forma di tre archivi:

  • istia.st.articles.dao: contiene le classi di accesso ai dati del livello [dao]
  • istia.st.articles.exception: contiene una classe di eccezioni per la gestione degli articoli
  • istia.st.articles.domain: contiene le classi di business del livello [domain]
archive
contenu
rôle
istia.st.articles.dao
- contiene il pacchetto [istia.st.articles.dao], che a sua volta contiene i seguenti elementi:
- [IArticlesDao]: l'interfaccia di accesso al livello Dao. È l'unica interfaccia visibile al livello [domain]. Non ne vede altre.
- [Article]: classe che definisce un articolo
- [ArticlesDaoSqlMap]: classe di implementazione dell’interfaccia [IArticlesDao] tramite lo strumento SqlMap
livello di accesso ai dati – si trova interamente nel livello [dao] dell’architettura a tre livelli dell’applicazione web
istia.st.articles.domain
- contiene il pacchetto [istia.st.articles.domain], che a sua volta contiene i seguenti elementi:
- [IArticlesDomain]: l'interfaccia di accesso al livello [domain]. È l'unica interfaccia visibile al livello web. Non ne vede altre.
- [AchatsArticles]: una classe che implementa [IArticlesDomain]
- [Achat]: classe che rappresenta l’acquisto di un cliente
- [Panier]: classe che rappresenta l'insieme degli acquisti di un cliente
rappresenta il modello degli acquisti sul web - si trova interamente nel livello [domain] dell'architettura a tre livelli dell'applicazione web
istia.st.articles.exception
- contiene il pacchetto [istia.st.articles.exception], che a sua volta contiene i seguenti elementi:
- [UncheckedAccessArticlesException]: classe che definisce un'eccezione di tipo [RuntimeException]. Questo tipo di eccezione viene generata dal livello [dao] non appena si verifica un problema di accesso ai dati.
 

3.3.3. Il pacchetto [istia.st.articles.dao]

La classe che definisce un articolo è la seguente:

package istia.st.articles.dao;
import istia.st.articles.exception.UncheckedAccessArticlesException;

/**
 * @author ST - ISTIA
 *  
 */
public class Article {
  private int id;
  private String nom;
  private double prix;
  private int stockActuel;
  private int stockMinimum;

  /**
   * constructeur par défaut
   */
  public Article() {
  }

  public Article(int id, String nom, double prix, int stockActuel,
      int stockMinimum) {
     // inizializzazione degli attributi di istanza
    setId(id);
    setNom(nom);
    setPrix(prix);
    setStockActuel(stockActuel);
    setStockMinimum(stockMinimum);
  }

     // getter - setter
  public int getId() {
    return id;
  }

  public void setId(int id) {
     // ID valido?
    if (id < 0)
      throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
    this.id = id;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
     // nome valido?
    if(nom==null || nom.trim().equals("")){
      throw new UncheckedAccessArticlesException("Le nom est [null] ou vide");
    }
    this.nom = nom;
  }

  public double getPrix() {
    return prix;
  }

  public void setPrix(double prix) {
     // prezzo valido?
    if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
    this.prix = prix;
  }

  public int getStockActuel() {
    return stockActuel;
  }

  public void setStockActuel(int stockActuel) {
     // scorte valide?
    if (stockActuel < 0)
      throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
    this.stockActuel = stockActuel;
  }

  public int getStockMinimum() {
    return stockMinimum;
  }

  public void setStockMinimum(int stockMinimum) {
     // scorte valide?
    if (stockMinimum < 0)
      throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
    this.stockMinimum = stockMinimum;
  }

  public String toString() {
    return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
        + stockMinimum + "]";
  }
}

Questa classe offre:

  1. un costruttore che consente di impostare le 5 informazioni relative a un articolo
  2. metodi di accesso, spesso denominati getter/setter, che servono a leggere e scrivere le 5 informazioni. I nomi di questi metodi seguono lo standard JavaBean. È prassi comune utilizzare oggetti JavaBean nel livello DAO per interfacciarsi con i dati del SGBD.
  3. una verifica dei dati inseriti nell’articolo. In caso di dati errati, viene generata un’eccezione.
  4. un metodo toString che consente di ottenere il valore di un articolo sotto forma di stringa di caratteri. Ciò è spesso utile per il debug di un'applicazione.

L'interfaccia [IArticlesDao] è definita come segue:

package istia.st.articles.dao;

import istia.st.articles.domain.Article;
import java.util.List;

/**
 * @author ST-ISTIA
 *
 */
public interface IArticlesDao {

  /**
   * @return : liste de tous les articles
   */
  public List getAllArticles();

  /**
   * @param unArticle :
   *          l'article à ajouter
   */
  public int ajouteArticle(Article unArticle);

  /**
   * @param idArticle :
   *          id de l'article à supprimer
   */
  public int supprimeArticle(int idArticle);

  /**
   * @param unArticle :
   *          l'article à modifier
   */
  public int modifieArticle(Article unArticle);

  /**
   * @param idArticle :
   *          id de l'article cherché
   * @return : l'article trouvé ou null
   */
  public Article getArticleById(int idArticle);

  /**
   * vide la table des articles
   */
  public void clearAllArticles();

  /**
   *
   * @param idArticle id de l'article dont on change le stock
   * @param mouvement valeur à ajouter au stock (valeur signée)
   */
  public int changerStockArticle(int idArticle, int mouvement);
}

Il ruolo dei diversi metodi dell’interfaccia è il seguente:

getAllArticles
restituisce tutti gli articoli della tabella ARTICLES in un elenco di oggetti [Article]
clearAllArticles
svuota la tabella ARTICLES
getArticleById
restituisce l'oggetto [Article] identificato dalla sua chiave primaria
ajouteArticle
consente di aggiungere un articolo alla tabella ARTICLES
modifieArticle
consente di modificare un articolo della tabella [ARTICLES]
supprimerArticle
consente di eliminare un articolo dalla tabella [ARTICLES]
changerStockArticle
consente di modificare lo stock di un articolo della tabella [ARTICLES]

L'interfaccia mette a disposizione dei programmi client una serie di metodi definiti esclusivamente dalle loro firme. Non si occupa del modo in cui tali metodi saranno effettivamente implementati. Ciò garantisce flessibilità all'applicazione. Il programma client effettua le chiamate su un'interfaccia e non su una sua specifica implementazione.

La scelta di una specifica implementazione avverrà tramite un file di configurazione Spring. In questa sede proponiamo di implementare l’interfaccia IArticlesDao utilizzando un prodotto open source denominato SqlMap. Ciò ci consentirà di rimuovere qualsiasi istruzione SQL dal codice Java.

La classe di implementazione [ArticlesDaoSqlMap] è definita come segue:

package istia.st.articles.dao;

// Importazioni
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;

public class ArticlesDaoSqlMap implements IArticlesDao {

   // Campi
  private SqlMapClient sqlMap;

   // Costruttori
  public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }

   // Metodi
  public SqlMapClient getSqlMap() {}
  public void setSqlMap(SqlMapClient sqlMap) { }
  public synchronized List getAllArticles() {}
  public synchronized int ajouteArticle(Article unArticle) {}
  public synchronized int supprimeArticle(int idArticle) {}
  public synchronized int modifieArticle(Article unArticle) {}
  public synchronized Article getArticleById(int idArticle) {}
  public synchronized void clearAllArticles() { }
  public synchronized int changerStockArticle(int idArticle, int mouvement) {}
}

Tutti i metodi di accesso ai dati sono stati sincronizzati per evitare problemi di accesso concorrente alla fonte dei dati. In un dato momento, un solo thread ha accesso a un determinato metodo.

La classe [ArticlesDaoSqlMap] utilizza lo strumento [Ibatis SqlMap]. L’utilità di questo strumento consiste nel consentire di estrarre il codice SQL relativo all’accesso ai dati dal codice Java. Esso viene quindi inserito in un file di configurazione. Avremo modo di tornarci sopra. Per essere inizializzata, la classe [ArticlesDaoSqlMap] necessita di un file di configurazione il cui nome viene passato come parametro al costruttore della classe. Questo file di configurazione definisce le informazioni necessarie per:

  • accedere al SGBD in cui si trovano gli articoli
  • gestire un pool di connessioni
  • gestire le transazioni

Nel nostro esempio, si chiamerà [sqlmap-config-firebird.xml] e definirà l’accesso a un database Firebird:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
  <transactionManager type="JDBC">
      <dataSource type="SIMPLE">
            <property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
                <property name="JDBC.ConnectionURL"
                    value="jdbc:firebirdsql:localhost/3050:D:/data/Databases/firebird/dbarticles.gdb"/>
                <property name="JDBC.Username" value="sysdba"/>
                <property name="JDBC.Password" value="masterkey"/>
                <property name="JDBC.DefaultAutoCommit" value="true"/>
        </dataSource>
  </transactionManager>
  <sqlMap resource="articles.xml"/>
</sqlMapConfig>

Il file di configurazione [articles.xml] citato sopra consente di definire come creare un'istanza della classe [istia.st.articles.dao.Article] a partire da una riga della tabella [ARTICLES] del SGBD. Definisce inoltre le query SQL che consentiranno al livello [dao] di ottenere i dati dalla fonte dati Firebird.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">

     <!-- un alias sulla classe istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- il mapping ORM : riga della tabella ARTICLES - istanza della classe Articolo -->
  <resultMap id="article" class="article">
    <result property="id" column="ID"/>
    <result property="nom" column="NOM"/>
    <result property="prix" column="PRIX"/>
    <result property="stockActuel" column="STOCKACTUEL"/>
    <result property="stockMinimum" column="STOCKMINIMUM"/>
  </resultMap>

     <!-- la query SQL per ottenere tutti gli articoli -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- la query SQL per eliminare tutti gli articoli -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- la query SQL per inserire un articolo -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#(id#,#nome#,#prezzo#,#stockattuale#,#stockminimo#)
</statement>

     <!-- la query SQL per eliminare un determinato articolo -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- la richiesta SQL per modificare un determinato articolo -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nome#,
    prix=#prezzo#,stockattuale=#stockattuale#,stockminimo=#stockminimo# where
    id=#id#
</statement>

     <!-- la query SQL per recuperare un determinato articolo -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- la richiesta SQL per modificare la giacenza di un determinato articolo -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#movimento#
    where id=#id# e stockActuel+#movimento#&gt;=0
</statement>
</sqlMap>

Il codice del pacchetto [dao] è riportato in allegato.

3.3.4. Il pacchetto [istia.st.articles.domain]

L'interfaccia [IArticlesDomain] disaccoppia il livello [métier] dal livello [web]. Quest'ultimo accede al livello [métier/domain] tramite questa interfaccia senza preoccuparsi della classe che lo implementa effettivamente. L'interfaccia definisce le seguenti azioni per l'accesso al livello di business:

 package istia.st.articles.domain;

// Importazioni
import java.util.ArrayList;
import java.util.List;

public abstract interface IArticlesDomain {

   // Metodi
  void acheter(Panier panier);
  List getAllArticles();
  Article getArticleById(int idArticle);
  ArrayList getErreurs();
}
List getAllArticles()
restituisce l’elenco di oggetti [Article] da presentare al cliente
Article getArticleById(int idArticle)
restituisce l’oggetto [Article] identificato da [idArticle]
void acheter(Panier panier)
conferma il carrello del cliente sottraendo dalle scorte degli articoli acquistati la quantità acquistata - l'operazione potrebbe non andare a buon fine se le scorte sono insufficienti
ArrayList getErreurs()
restituisce l'elenco degli errori verificatisi - vuoto se non ci sono errori

In questo caso, l’interfaccia [IArticlesDomain] verrà implementata dalla seguente classe [AchatsArticles]:

package istia.st.articles.domain;

// Importazioni
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.List;

public class AchatsArticles implements IArticlesDomain {

   // Campi
  private IArticlesDao articlesDao;
  private ArrayList erreurs;

   // Costruttori
  public AchatsArticles(IArticlesDao articlesDao) { }

   // Metodi
  public ArrayList getErreurs() {}
  public List getAllArticles() {}
  public Article getArticleById(int id) {}
  public void acheter(Panier panier) { }
}

Questa classe implementa i quattro metodi dell'interfaccia [IArticlesDomain]. Ha due campi privati:

IArticlesDao articlesDao
l'oggetto di accesso ai dati fornito dal livello di accesso ai dati
ArrayList erreurs
l'elenco degli eventuali errori

Per creare un'istanza della classe, è necessario fornire l'oggetto che consente l'accesso ai dati di SGBD:

public AchatsArticles(IArticlesDao articlesDao)
costruttore

La classe [Achat] rappresenta un acquisto effettuato dal cliente:

package istia.st.articles.domain;

public class Achat {

   // Campi
  private Article article;
  private int qte;

  // Costruttori
  public Achat(Article article, int qte) { }

  // Metodi
  public double getTotal() {}
  public Article getArticle() {}
  public void setArticle(Article article) { }
  public int getQte() {}
  public void setQte() { }
  public String toString() {}
}

La classe [Achat] è una JavaBean con i seguenti campi e metodi:

article
l'articolo acquistato
qte
la quantità acquistata
double getTotal()
indica l'importo dell'acquisto
String toString()
stringa di identificazione dell'oggetto

La classe [Panier] rappresenta l'insieme degli acquisti del cliente:

package istia.st.articles.domain;

// Importazioni
import java.util.ArrayList;

public class Panier {

  // Campi
  private ArrayList achats;

   // Costruttori
  public Panier() { }

   // Metodi
  public ArrayList getAchats() {}
  public void ajouter(Achat unAchat) { }
  public void enlever(int idAchat) { }
  public double getTotal() {}
  public String toString() { }
}

La classe [Panier] è una JavaBean con i seguenti campi e metodi:

achats
l'elenco degli acquisti del cliente - elenco di oggetti di tipo [Achat]
void ajouter(Achat unAchat)
aggiunge un acquisto all'elenco degli acquisti
void enlever(int idArticle)
rimuove l'acquisto dell'articolo idArticle
double getTotal()
calcola l'importo totale degli acquisti
String toString()
restituisce la stringa di identificazione del carrello
ArrayList getAchats()
restituisce l'elenco degli acquisti

Il codice del pacchetto [domain] è riportato in allegato.

3.3.5. Il pacchetto [istia.st.articles.exception]

Questo pacchetto contiene la classe che definisce l’eccezione generata dal livello [dao] quando si verifica un problema di accesso alla fonte dati:

package istia.st.articles.exception;

public class UncheckedAccessArticlesException
    extends RuntimeException {

  public UncheckedAccessArticlesException() {
    super();
  }

  public UncheckedAccessArticlesException(String mesg) {
    super(mesg);
  }

  public UncheckedAccessArticlesException(String mesg, Throwable th) {
    super(mesg, th);
  }
}

3.3.6. Test del modello

Il modello M è stato testato in Eclipse con la seguente configurazione:

Image

Commenti:

  • in [WEB-INF/lib] si trovano:
    • gli archivi necessari allo strumento [ibatis SqlMap] incaricato di accedere al Firebird SGBD: ibatis-*.jar
    • quello necessario per lo strumento [spring]: spring.jar
    • il driver JDBC per SGBD e [Firebird]: firebirdsql-full.jar
    • gli archivi necessari per i log: log4-*.jar, commons-logging.jar
    • i tre file del modello testato: istia.st.articles.*.jar
    • l'archivio necessario per lo strumento di test [junit]
  • in [WEB-INF/src] si trovano i file di configurazione che verranno automaticamente copiati in [WEB-INF/classes] da Eclipse:
    • i file di configurazione dello strumento [sqlmap]: sqlmap-config-firebird.xml, articles.xml
    • quello dello strumento [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
    • il file di configurazione dello strumento [log4j]: log4j.properties
  • nel pacchetto [istia.st.articles.tests] sono presenti le classi di test del modello

3.3.6.1. Test del livello [dao]

La classe di test JUnit del livello [dao] è la seguente. La sua lettura permette di comprendere come vengono utilizzati i metodi dell’interfaccia [IArticlesDao]:

package istia.st.articles.tests.dao;

import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.dao.Article;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

// Test della classe ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {

     // un'istanza della classe sottoposta a test
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // recupera un'istanza di accesso ai dati
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }

    public void testGetAllArticles() {
         // visualizza gli articoli
        listArticles();
    }

    public void testClearAllArticles() {
         // svuota la tabella degli articoli
        articlesDao.clearAllArticles();
         // legge la tabella ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
    }

    public void testAjouteArticle() {
         // cancella il contenuto di ARTICLES
        articlesDao.clearAllArticles();
         // legge la tabella ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserimento
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // legge la tabella ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         //la visualizza
        listArticles();
    }

    public void testSupprimeArticle() {
         // cancella il contenuto di ARTICLES
        articlesDao.clearAllArticles();
         // legge la tabella ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserimento
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // legge la tabella ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // eliminazione
        articlesDao.supprimeArticle(4);
         // legge la tabella ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(1, articles.size());
         // visualizza la tabella
        listArticles();
    }

    public void testModifieArticle() {
         // cancella il contenuto di ARTICLES
        articlesDao.clearAllArticles();
         // legge la tabella ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserimento
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // legge la tabella ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
         // modifica
        articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
         // getById
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getPrix(), 44, 1e-6);
         // visualizza la tabella
        listArticles();
    }

    public void testGetArticleById() {
         // cancella il contenuto di ARTICLES
        articlesDao.clearAllArticles();
         // legge la tabella ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserimento
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // legge la tabella ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

    private void listArticles() {
         // legge la tabella ARTICLES
        List articles = articlesDao.getAllArticles();
         // vengono visualizzati gli articoli letti
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

    public void testChangerStockArticle() throws InterruptedException {
         // cancella il contenuto di ARTICLES
        articlesDao.clearAllArticles();
         // inserimento
        int nbArticles = articlesDao.ajouteArticle(new Article(3, "article3",
                30, 101, 3));
        assertEquals(nbArticles, 1);
        nbArticles = articlesDao.ajouteArticle(new Article(4, "article4", 40,
                40, 4));
        assertEquals(nbArticles, 1);
         // creazione di 100 thread per l'aggiornamento delle scorte dell'articolo 3
        Thread[] taches = new Thread[100];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
            taches[i].start();
        }
         // si attende il completamento dei thread
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // recuperare l'articolo 3 e verificarne le scorte
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        assertEquals(1, unArticle.getStockActuel());
         // modifica delle scorte dell'articolo 4
        boolean erreur = false;
        int nbLignes = articlesDao.changerStockArticle(4, -100);
        assertEquals(0, nbLignes);
         // visualizza la tabella
        listArticles();
    }
}

Commenti:

  • la classe di test memorizza, tramite il suo metodo setUp, un'istanza della classe da testare:
1
2
3
4
5
6
7
8
     // un'istanza della classe testata
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // recupera un'istanza di accesso ai dati
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }
  • L'oggetto da testare viene fornito da [Spring]. Nel codice sopra riportato, viene richiesto il bean Spring denominato [articlesDao]. Questo bean è definito nel file di configurazione di Spring [spring-config-test-dao.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la classe di accesso ai dati -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
</beans>

Come si vede sopra, il bean [articlesDao] è un'istanza della classe [istia.st.articles.dao.ArticlesDaoSqlMap]. Questa classe dispone di un costruttore che richiede come parametro il nome del file di configurazione dello strumento [SqlMap]. Tale nome viene qui fornito: si tratta di [sqlmap-config-firebird.xml], già descritto in precedenza. Esso fornisce tutte le informazioni necessarie per accedere ai dati di SGBD.

Il metodo [testChangerStockArticle] crea 100 thread incaricati di decrementare lo stock di un determinato articolo. Lo scopo è qui quello di testare gli accessi concorrenti al SGBD. Poiché il metodo [changerStockArticle] della classe [istia.st.articles.dao.ArticlesDaoSqlMap] è stato sincronizzato, questo test viene superato. Se si rimuove la sincronizzazione, il test non viene più superato. La classe incaricata di aggiornare le scorte è la seguente:

package istia.st.articles.tests;

import istia.st.articles.dao.IArticlesDao;

public class ThreadMajStock extends Thread {

    /**
     * nom du thread
     */
    private String name;

    /**
     * objet d'accès aux données
     */
    private IArticlesDao articlesDao;

    /**
     * 
     * @param name
     *            le nom du thread afin de l'identifier
     * @param articlesDao
     *            l'objet d'accès aux données du sgbd
     */
    public ThreadMajStock(String name, IArticlesDao articlesDao) {
        this.name = name;
        this.articlesDao = articlesDao;
    }

    /**
     * décrémente le stock de l'article 3 d'une unité fait un suivi écran des
     * opérations
     */
    public void run() {
         // monitoraggio
        System.out.println(name + " lancé");
         // modifica delle scorte dell'articolo 3
        articlesDao.changerStockArticle(3, -1);
         // monitoraggio
        System.out.println(name + " terminé");
    }
}
  • la classe sopra indicata decrementa di 1 lo stock dell’articolo n. 3

3.3.6.2. Test del livello [domain]

La classe di test JUnit del livello [domain] è la seguente:

package istia.st.articles.tests.domain;

import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.Article;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

// test della classe ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {

     // un'istanza della classe di accesso al dominio
    private IArticlesDomain articlesDomain;

     // un'istanza della classe di accesso ai dati
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // recupera un'istanza di accesso al dominio
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // recupera un'istanza di accesso ai dati
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }

     // recupero di un articolo specifico
    public void testGetArticleById() {
         // eliminazione del contenuto di ARTICLES
        articlesDao.clearAllArticles();
         // legge la tabella ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserimento
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // legge la tabella ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDomain.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

     // visualizzazione sullo schermo
    private void listArticles() {
         // legge la tabella ARTICLES
        List articles = articlesDomain.getAllArticles();
         // vengono visualizzati gli articoli letti
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

     // acquisti di articoli
    public void testAchatPanier(){
         // eliminazione del contenuto di ARTICLES
        articlesDao.clearAllArticles();
         // legge la tabella ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserimento
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // legge la tabella ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // creazione di un carrello con due articoli
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // verifiche
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // conferma del carrello
        articlesDomain.acheter(panier);
         // verifiche
        assertEquals(0,articlesDomain.getErreurs().size());
        assertEquals(0,panier.getAchats().size());
         // cerca l'articolo n. 3
        article3=articlesDomain.getArticleById(3);
        assertEquals(20,article3.getStockActuel());
         // cerca articolo n. 4
        article4=articlesDomain.getArticleById(4);
        assertEquals(30,article4.getStockActuel());
         // nuovo carrello
        panier.ajouter(new Achat(article3,100));
         // conferma carrello
        articlesDomain.acheter(panier);
         // verifiche - si è acquistato troppo
         // deve esserci un errore
        assertEquals(1,articlesDomain.getErreurs().size());
         // cerca articolo n. 3
        article3=articlesDomain.getArticleById(3);
         // le scorte non dovrebbero essere cambiate
        assertEquals(20,article3.getStockActuel());    
    }

     // rimuovere gli acquisti
    public void testRetirerAchats(){
         // eliminazione del contenuto di ARTICLES
        articlesDao.clearAllArticles();
         // legge la tabella ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserimento
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // legge la tabella ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // creazione di un carrello con due articoli
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // verifiche
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // aggiunge un articolo già acquistato
        panier.ajouter(new Achat(article3,10));
         // verifiche
         // il totale deve essere portato a 1000
        assertEquals(1000.0,panier.getTotal(),1e-6);
         // ci sono ancora 2 articoli nel carrello
        assertEquals(2,panier.getAchats().size());
         // la quantità dell'articolo 3 deve essere portata a 20
        Achat achat=(Achat)panier.getAchats().get(0);
        assertEquals(20,achat.getQte());
         // l'articolo 3 viene rimosso dal carrello
        panier.enlever(3);
         // verifiche
         // il totale deve essere stato portato a 400
        assertEquals(400.0,panier.getTotal(),1e-6);
         // Solo 1 articolo nel carrello
        assertEquals(1,panier.getAchats().size());
         // deve trattarsi dell'articolo n. 4
        assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
    }
}

Commenti:

  • la classe di test memorizza, tramite il suo metodo setUp, un'istanza della classe da testare e un'istanza della classe di accesso ai dati. Quest'ultimo punto è controverso. In teoria, la classe di test non dovrebbe aver bisogno di accedere al livello [dao], che non dovrebbe nemmeno conoscere. In questo caso, abbiamo ignorato questa “etica” che, se rispettata, ci avrebbe costretti a creare nuovi metodi nella nostra interfaccia [IArticlesDomain].
     // un'istanza della classe di accesso al dominio
    private IArticlesDomain articlesDomain;

     // un'istanza della classe di accesso ai dati
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // recupera un'istanza di accesso al dominio
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // recupera un'istanza di accesso ai dati
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }
  • L’oggetto da testare è fornito da [Spring]. Sopra, viene richiesto il bean Spring denominato [articlesDomain]. Questo bean è definito nel file di configurazione di Spring [spring-config-test-domain.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la classe di accesso ai dati -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la classe di business -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Come si può vedere sopra, il bean [articlesDomain] è un'istanza della classe [istia.st.articles.domain.AchatsArticles]. Questa classe dispone di un costruttore che richiede come parametro un oggetto di accesso al livello [dao] di tipo [IArticlesDao]. In questo caso, il file di configurazione indica che tale oggetto è il bean denominato [articlesDao]. Ciò obbliga Spring a istanziare questo bean. L’istanziazione del bean [articlesDao] è stata spiegata in precedenza. Pertanto, alla fine, sono stati istanziati due bean:

  • [articlesDao] di tipo [istia.st.articles.dao.ArticlesDaoSqlMap]
  • [articlesDomain] di tipo [ istia.st.articles.domain.AchatsArticles]

Queste due istanze sono state create dalla prima chiamata a Spring:

1
2
3
4
         // recupera un'istanza di accesso al dominio
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");

Viene quindi recuperato il bean [articlesDomain]. Al secondo richiamo a Spring:

1
2
3
         // recupera un'istanza di accesso ai dati
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");

[Spring] si limita a restituire un riferimento al bean [articlesDao], già creato durante la chiamata precedente. È il principio del singleton. Se si richiede un bean a Spring, questo lo istanzia se non esiste già, altrimenti restituisce un riferimento al bean esistente.

3.4. Applicazione web MVC a tre livelli

Di seguito, intendiamo realizzare la seguente applicazione web a tre livelli:

L’applicazione avrà un’architettura MVC. Il modello M è stato scritto e testato. È quello descritto in precedenza. Ci viene fornito in tre archivi [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Dobbiamo scrivere il controller C e le viste V.

Consideriamo innanzitutto un approccio classico, in cui:

  • il controller C è gestito da un unico servlet
  • le viste V sono gestite da pagine JSP

3.5. Architettura MVC basata su un servlet di controllo e su pagine JSP

L’architettura MVC dell’applicazione sarà la seguente:

M=modèle
le classi di business, le classi di accesso ai dati e il database
V=vues
le pagine JSP
C=contrôleur
il servlet per l'elaborazione delle richieste dei clienti

3.5.1. Il modello

È stato presentato in precedenza. È costituito dagli archivi Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.5.2. Le viste

Le viste corrispondono a quelle presentate all’inizio del documento:

LISTE
liste.jsp
Le viste sono raccolte nella cartella [vues] dell’applicazione
INFOS
infos.jsp
PANIER
panier.jsp
PANIERVIDE
paniervide.jsp
ERREURS
erreurs.jsp

3.5.3. Il controller

Il controller sarà costituito da un unico servlet denominato [WebArticles]. Esso elaborerà le diverse richieste dei client. Queste saranno formalizzate dalla presenza di un parametro [action] nella richiesta HTTP del client:

requête
significato
azione del controller
risposte possibili
action=liste
il cliente desidera l'elenco degli
articoli
- richiede l'elenco degli articoli al livello
di business
- [LISTE]
- [ERREURS]
action=infos
il cliente richiede
informazioni su uno degli articoli visualizzati nella vista
[LISTE]
- richiede l'articolo al livello business
- [INFOS]
- [ERREURS]
action=achat
il cliente acquista un articolo
- richiede l'articolo al livello applicativo e
lo aggiunge al carrello del cliente
- [INFOS] in caso di errore nella quantità
- [LISTE] se non c'è errore
action=retirerachat
il cliente desidera eliminare un
acquisto dal proprio carrello
- recupera il carrello dalla sessione e lo modifica
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
il cliente desidera visualizzare il proprio
carrello
- recupera il carrello dalla sessione
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
il cliente ha completato gli acquisti
e passa alla fase di pagamento
- aggiorna nel database le giacenze degli
articoli acquistati
- svuota il carrello del cliente dagli articoli di cui
il cui acquisto è stato confermato
- [LISTE]
- [ERREURS]

3.5.4. Configurazione dell'applicazione

Cercheremo di configurare l'applicazione in modo da renderla il più flessibile possibile rispetto a modifiche quali:

  1. la modifica degli URL delle diverse viste
  2. la modifica delle classi che implementano le interfacce [IArticlesDao] e [IArticlesDomain]
  3. la modifica del SGBD, del database, della tabella degli articoli

3.5.5. Modifiche agli URL

I nomi degli URL delle viste saranno inseriti nel file di configurazione dell’applicazione [web.xml] insieme ad alcuni altri parametri:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
        <servlet-name>webarticles</servlet-name>
        <servlet-class>istia.st.articles.web.WebArticles</servlet-class>
        <init-param>
            <param-name>springConfigFileName</param-name>
            <param-value>spring-config-sqlmap-firebird.xml</param-value>
        </init-param>
        <init-param>
            <param-name>urlMain</param-name>
            <param-value>/main</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlListe</param-name>
            <param-value>/vues/liste.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlInfos</param-name>
            <param-value>/vues/infos.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlPanier</param-name>
            <param-value>/vues/panier.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlPanierVide</param-name>
            <param-value>/vues/paniervide.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlDebug</param-name>
            <param-value>/vues/debug.jsp</param-value>
        </init-param>
    </servlet>
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
    <servlet-mapping>
        <servlet-name>webarticles</servlet-name>
        <url-pattern>/main</url-pattern>
    </servlet-mapping>
</web-app>

Nel file [web.xml] si trovano

  • gli URL delle diverse viste dell'applicazione
  • il nome [springConfigFileName] del file di configurazione di Spring che consentirà la creazione degli oggetti singleton per l'accesso ai livelli business e DAO
  • la vista [/vues/index.jsp] che verrà visualizzata quando l'URL richiesto dal client sarà /<context>, dove <context> è il contesto dell'applicazione

3.5.6. La modifica delle classi di implementazione delle interfacce

In linea con le architetture a tre livelli, i livelli devono essere isolati l'uno dall'altro. Tale isolamento si ottiene nel modo seguente:

  • i livelli comunicano tra loro tramite interfacce e non tramite classi concrete
  • il codice di un livello non istanzia mai direttamente la classe di un altro livello per utilizzarla. Si limita semplicemente a richiedere a uno strumento esterno, in questo caso [Spring], un'istanza di implementazione dell'interfaccia del livello che intende utilizzare. A tal fine, sappiamo che non ha bisogno di conoscere il nome della classe di implementazione, ma solo il nome del bean Spring di cui desidera un riferimento.

Nella nostra applicazione, il file di configurazione Spring potrebbe essere il seguente:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la classe di accesso ai dati -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la classe di business -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Per accedere al livello [métier], una classe del livello [Interface utilisateur, UI] potrà richiedere il bean [articlesDomain]. Spring istanzierà quindi un oggetto di tipo [istia.st.articles.domain.AchatsArticles]. Per questa istanziazione, ha bisogno di un bean di tipo [articlesDao], ovvero di un oggetto di tipo [istia.st.articles.dao.ArticlesDaoSqlMap]. Spring istanzierà quindi un oggetto di questo tipo. Tale istanziazione avverrà sulla base delle informazioni contenute nel file [sqlmap-config-firebird.xml], file di configurazione per l’accesso ai dati tramite SqlMap. Al termine dell’operazione, la classe [UI] che ha richiesto il bean [articlesDomain] dispone dell’intera catena che la collega ai dati del SGBD:

3.5.7. Modifiche relative al SGBD o al database

L’indipendenza dell’applicazione web rispetto alle modifiche relative al SGBD o al database è garantita in questo caso dai file di configurazione del SqlMap. Ce ne sono due:

  1. il file [sql-map-config-firebird.xml]
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <transactionManager type="JDBC">
            <dataSource type="SIMPLE">
            <property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
            <property name="JDBC.ConnectionURL"
                value="jdbc:firebirdsql:localhost/3050:d:/data/databases/firebird/dbarticles.gdb"/>
            <property name="JDBC.Username" value="sysdba"/>
            <property name="JDBC.Password" value="masterkey"/>
            <property name="JDBC.DefaultAutoCommit" value="true"/>
        </dataSource>
    </transactionManager>
    <sqlMap resource="articles.xml"/>
</sqlMapConfig>

Questo file fa riferimento a un database Firebird. È sufficiente modificare il nome del driver JDBC per lavorare con un altro driver, ad esempio SGBD.

  1. il file [articles.xml] che raccoglie le diverse istruzioni SQL necessarie all’applicazione:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">

     <!-- un alias della classe istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- il mapping ORM : riga della tabella ARTICLES - istanza della classe Articolo -->
  <resultMap id="article" class="article">
    <result property="id" column="ID"/>
    <result property="nom" column="NOM"/>
    <result property="prix" column="PRIX"/>
    <result property="stockActuel" column="STOCKACTUEL"/>
    <result property="stockMinimum" column="STOCKMINIMUM"/>
  </resultMap>

     <!-- la query SQL per ottenere tutti gli articoli -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- la query SQL per eliminare tutti gli articoli -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- la query SQL per inserire un articolo -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#(id#,#nome#,#prezzo#,#stockattuale#,#stockminimo#)
</statement>

     <!-- la query SQL per eliminare un determinato articolo -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- la richiesta SQL per modificare un determinato articolo -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nome#,
    prix=#prezzo#,stockattuale=#stockattuale#,stockminimo=#stockminimo# where
    id=#id#
</statement>

     <!-- la query SQL per recuperare un determinato articolo -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- la richiesta SQL per modificare la giacenza di un determinato articolo -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#movimento#
    where id=#id# e stockActuel+#movimento#&gt;=0
</statement>
</sqlMap>

Se i nomi della tabella degli articoli o delle colonne dovessero cambiare, si dovrebbe riscrivere le query in questo file di configurazione senza dover modificare il codice Java. Ciò varrebbe anche nel caso in cui una query venisse sostituita da una procedura memorizzata per motivi di prestazioni.

3.5.8. L'architettura complessiva dell'applicazione [webarticles]

Un’applicazione web Java è un puzzle composto da numerosi elementi. Dotarla di un’architettura MVC ne aumenta in genere il numero. La struttura dell’applicazione [webarticles] sotto [eclipse] è la seguente:

structure générale - on voit ci-dessous les
 archives Java utilisées par le projet
 Eclipse.
spring : pour Spring
ibatis : pour SqlMap
log4j, commons-logging : pour les logs
 de Spring et Sqlmap
firebird : pour le SGBD firebird
mysql : pour le SGBD MySQL
jstl, standard : pour la bibliothèque
 de balises JSTL
le dossier des sources java : contient le code Java
 ainsi que les fichiers de configuration
 spring et sqlmap. Eclipse recopie
 automatiquement ces fichiers 
dans [WEB-INF/classes]. 
C'est là que l'application les trouvera.
le dossier [WEB-INF] de l'application : contient le
 descripteur [web.xml] de l'application ainsi que le
 fichiers de définition de la bibliothèques JSTL
les vues

3.5.9. Le viste JSP

Le viste JSP utilizzano la libreria di tag JSTL.

3.5.9.1. entete.jsp

Per garantire una certa uniformità alle diverse viste, queste condivideranno la stessa intestazione, quella che mostra il nome dell’applicazione con il menu:

Il menu è dinamico e definito dal controller. Quest’ultimo inserisce nella richiesta inviata alla pagina JSP un attributo chiave "actions" il cui valore associato è un array di tipo Hastable[]. Ogni elemento di questo array è un dizionario destinato a generare un’opzione del menu dell’intestazione. Ogni dizionario ha due chiavi:

  • href: l'URL associato all'opzione di menu
  • lien: il testo del menu

Le altre viste dell’applicazione utilizzeranno l’intestazione definita da [entete.jsp] tramite il seguente tag JSP:

<jsp:include page="entete.jsp"/>

Una volta eseguito, questo tag avrà l'effetto di includere nel codice della pagina JSP, che lo contiene, il codice della pagina [entete.jsp]. Poiché l'URL della pagina è relativo (manca il /), la pagina [entete.jsp] verrà cercata nella stessa cartella della pagina che contiene il tag <jsp:include>.

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
        <title>webarticles</title>
    </head>
    <body>
        <table>
            <tr>
                <td><h2>Magasin virtuel</h2></td>
                <c:forEach items="${actions}" var="action">
                    <td>|</td>
                    <td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
                </c:forEach>
            </tr>
        </table>
        <hr>

3.5.9.2. liste.jsp

Questa vista mostra l’elenco degli articoli disponibili per la vendita:

Viene visualizzata a seguito di una richiesta /main?action=liste o /main?action=validationpanier. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l'array delle opzioni del menu
listarticles
ArrayList oggetti di tipo [Article]
message
oggetto String - messaggio da visualizzare in fondo alla pagina

Ogni link [Infos] dell'array HTML degli articoli ha un URL della forma [?action=infos&id=ID], dove ID è il campo id dell'articolo visualizzato.

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Liste des articles</h2>
        <table border="1">
            <tr>
                <th>NOM</th><th>PRIX</th>
            </tr>
            <c:forEach var="article" items="${listarticles}">
                <tr>
                    <td><c:out value="${article.nom}"/></td>
                    <td><c:out value="${article.prix}"/></td>
                    <td><a href="<c:out value="?action=infos&id=${article.id}"/>">Infos</a></td>
                </tr>
            </c:forEach>
        </table>
        <p>
        <c:out value="${message}"/>
    </body>
</html>

3.5.9.3. infos.jsp

Questa vista mostra le informazioni relative a un articolo e ne consente anche l'acquisto:

Image

Viene visualizzata a seguito di una richiesta /main?action=infos&id=ID o di una richiesta /main?action=acquisto&id=ID quando la quantità acquistata è errata. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l'array delle opzioni del menu
article
oggetto di tipo [Article] - articolo da visualizzare
msg
oggetto String - messaggio da visualizzare in caso di errore sulla quantità
qte
oggetto String - valore da visualizzare nel campo di immissione [Qte]

I campi [msg] e [qte] vengono utilizzati in caso di errore di immissione della quantità:

Image

Questa pagina contiene un modulo che viene inviato tramite il pulsante [Acheter]. L'URL di destinazione di POST è [?action=achat&id=ID], dove ID è l'ID dell'articolo acquistato.

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Article d'id [<c:out value="${article.id}"/>]</h2>
        <table border="1">
            <tr>
                <th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
            </tr>
                <tr>
                    <td><c:out value="${article.nom}"/></td>
                    <td><c:out value="${article.prix}"/></td>
                    <td><c:out value="${article.stockActuel}"/></td>
                    <td><c:out value="${article.stockMinimum}"/></td>
                </tr>
        </table>
        <p>
        <form method="post" action="?action=achat&id=<c:out value="${article.id}"/>"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
                    <td><c:out value="${msg}"/></td>
                </tr>
            </table>
        </form>
    </body>
</html>

3.5.9.4. panier.jsp

Questa schermata mostra il contenuto del carrello:

Image

Viene visualizzata a seguito di una richiesta /main?action=carrello o /main?action=ritira&id=ID. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l'array delle opzioni del menu
panier
oggetto di tipo [Panier] - il carrello da visualizzare

Ogni link [Retirer] dell'array HTML degli acquisti nel carrello ha un URL della forma [?action=retirerachat&id=ID], dove ID è il campo [id] dell'articolo che si desidera rimuovere dal carrello.

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Contenu de votre panier</h2>
        <table border="1">
            <tr>
                <td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
            </tr>
            <c:forEach var="achat" items="${panier.achats}">
                <tr>
                    <td><c:out value="${achat.article.nom}"/></td>
                    <td><c:out value="${achat.qte}"/></td>
                    <td><c:out value="${achat.article.prix}"/></td>
                    <td><c:out value="${achat.total}"/></td>
                    <td><a href="<c:out value="?action=retirerachat&id=${achat.article.id}"/>">Retirer</a></td>
                </tr>
            </c:forEach>
        </table>
        <p>
        Total de la commande : <c:out value="${panier.total}"/> euros
    </body>
</html>

3.5.9.5. paniervide.jsp

Questa schermata mostra l'informazione che indica che il carrello è vuoto:

Image

Viene visualizzata a seguito di una richiesta /main?action=carrello o /main?action=ritira&id=ID. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l'array delle opzioni del menu

Codice:

1
2
3
4
5
6
7
8
9
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Contenu de votre panier</h2>
        <p>
        Votre panier est vide.
    </body>
</html>

3.5.9.6. erreurs.jsp

Questa schermata viene visualizzata in caso di errori:

Image

Viene visualizzata a seguito di qualsiasi richiesta che generi un errore, ad eccezione dell'azione di acquisto con una quantità errata, gestita dalla vista [INFOS]. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l’array delle opzioni del menu
erreurs
ArrayList oggetti String che rappresentano i messaggi di errore da visualizzare

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Les erreurs suivantes se sont produites</h2>
        <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li><c:out value="${erreur}"/></li>
            </c:forEach>
        </ul>
    </body>
</html>

3.5.9.7. index.jsp

Questa pagina è definita come pagina iniziale dell'applicazione nel file [web.xml] dell'applicazione:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
....
    </servlet>
    <servlet-mapping>
....
    </servlet-mapping>
        <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>    
</web-app>

La vista [index.jsp] si limita a reindirizzare il client al punto di ingresso dell'applicazione:

1
2
3
4
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main?action=liste"/>

3.5.10. Il controller

Resta da scrivere il cuore della nostra applicazione web, il controller. Il suo ruolo consiste nel:

  • recuperare la richiesta del client,
  • elaborare l'azione richiesta dal cliente utilizzando le classi di business,
  • inviare in risposta la vista appropriata.

3.5.10.1. Inizializzazione del controller

Quando la classe del controller viene caricata dal server dei servlet, viene eseguito il suo metodo [init]. Ciò avverrà una sola volta. Una volta caricato in memoria, il controller vi rimarrà e gestirà le richieste dei diversi client. Ogni client è oggetto di un thread di esecuzione e i metodi del controller vengono quindi eseguiti simultaneamente da thread diversi. Si ricorda che, per questo motivo, il controller non deve avere campi che i suoi metodi possano modificare. I suoi campi devono essere di sola lettura. Essi vengono inizializzati dal metodo [init], il cui ruolo principale è proprio questo. Questo metodo ha infatti la particolarità di essere eseguito una sola volta da un unico thread. Non sussistono quindi problemi di accesso concorrente ai campi del controller in questo metodo. Il metodo [init] ha lo scopo di inizializzare gli oggetti necessari all’applicazione web, che saranno condivisi in sola lettura da tutti i thread client. Questi oggetti condivisi possono essere collocati in due posizioni:

  • i campi privati del controller
  • il contesto di esecuzione dell’applicazione (ServletContext)

Il metodo [init] dell’applicazione [webarticles] eseguirà le seguenti azioni:

  • verificherà la presenza, nel file [web.xml], dei parametri necessari al corretto funzionamento dell’applicazione. Questi sono stati descritti nel paragrafo 3.5.5.
  • imposterà un campo privato [ArrayList erreurs] contenente l’elenco degli eventuali errori. Tale elenco sarà vuoto in assenza di errori, ma sarà comunque presente.
  • Se si sono verificati errori, il metodo [init] termina qui. In caso contrario, crea un oggetto di tipo [IArticlesDomain] che costituirà l’oggetto di business che il controller utilizzerà per le proprie esigenze. Come spiegato al punto 3.5.6, il controller richiederà al framework Spring il bean di cui ha bisogno. Questa operazione di istanziazione può causare diversi errori. In tal caso, anche questi saranno memorizzati nel campo [erreurs] del controller.

3.5.10.2. Metodi doGet, doPost

Questi due metodi gestiscono le richieste HTTP, GET e POST provenienti dai clienti. Queste richieste verranno elaborate in modo indifferenziato. Il metodo [doPost] potrà quindi reindirizzare al metodo [doGet] o viceversa. La richiesta del cliente verrà elaborata nel modo seguente:

  • verrà verificato il campo [erreurs]. Se non è vuoto, significa che si sono verificati degli errori durante l’inizializzazione dell’applicazione e che questa non può funzionare. Verrà quindi inviata, in risposta, la vista [ERREURS].
  • il parametro [action] della richiesta verrà recuperato e verificato. Se non corrisponde a un’azione nota, verrà inviata la vista [ERREURS] con un messaggio di errore appropriato.
  • Se il parametro [action] è valido, la richiesta del client viene inoltrata a una procedura specifica dell’azione per l’elaborazione. La procedura che gestisce l’azione [uneAction] avrà la seguente firma:
1
2
3
4
5
6
7
8
/**
   * @param request la requête du client
   * @param response la réponse au client
   * @throws IOException
   * @throws ServletException
   */
  private void doUneAction(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException; 

3.5.10.3. Elaborazione delle diverse azioni

I metodi che gestiscono le diverse azioni possibili dell’applicazione sono i seguenti:

metodo
richiesta
elaborazione
risposte possibili
doListe
GET /main?action=liste
- richiedere l'elenco degli articoli
alla classe di business
- visualizzarla
[LISTE] o [ERREURS]
doInfos
GET /main?action=infos&id=ID
- richiedere l'articolo con id=ID alla
classe di business
- visualizzarlo
[INFOS] o [ERREURS]
doAchat
POST /main?action=acquisto&id=ID
- la quantità acquistata fa parte dei parametri inviati
- richiedere l'articolo con id=ID a
classe di business
- inserirlo nel carrello durante
sessione del cliente
[LISTE] o [INFOS]
o [ERREURS]
doRetirerAchat
GET /main?action=ritiro&id=ID
- rimuovere l'articolo con id=ID dall'
lista degli acquisti nel carrello
sessione del cliente
[PANIER]
doPanier
GET /main?action=carrello
- visualizza il carrello della
sessione cliente
[PANIER] o [PANIERVIDE]
doValidationPanier
GET /main?action=confermacarrello
- ridurre nel database le
scorte di tutti gli articoli
presenti nel carrello della
sessione del cliente
[LISTE] o [ERREURS]

3.5.10.4. Il codice

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
package istia.st.articles.web;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import istia.st.articles.domain.Achat;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST
 *  
 */
public class WebArticles extends HttpServlet {

     // campi privati
    private ArrayList erreurs = new ArrayList();
    private IArticlesDomain articlesDomain = null;
    private final String URL_MAIN = "urlMain";
    private final String URL_ERREURS = "urlErreurs";
    private final String URL_LISTE = "urlListe";
    private final String URL_INFOS = "urlInfos";
    private final String URL_PANIER = "urlPanier";
    private final String URL_PANIER_VIDE = "urlPanierVide";
    private final String URL_DEBUG = "urlDebug";
    private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
    private final String[] parameters =
        {
            URL_MAIN,
            URL_ERREURS,
            URL_LISTE,
            URL_INFOS,
            URL_PANIER,
            URL_PANIER_VIDE,
            URL_DEBUG,
            SPRING_CONFIG_FILENAME };
    private ServletConfig config;
    private final String ACTION_LISTE = "liste";
    private final String ACTION_PANIER = "panier";
    private final String ACTION_ACHAT = "achat";
    private final String ACTION_INFOS = "infos";
    private final String ACTION_RETIRER_ACHAT = "retirerachat";
    private final String ACTION_VALIDATION_PANIER = "validationpanier";
    private String urlActionListe;
    private final String lienActionListe = "Liste des articles";
    private String urlActionPanier;
    private final String lienActionPanier = "Voir le panier";
    private String urlActionValidationPanier;
    private final String lienActionValidationPanier = "Valider le panier";
    private Hashtable hActionListe = new Hashtable(2);
    private Hashtable hActionPanier = new Hashtable(2);
    private Hashtable hActionValidationPanier = new Hashtable(2);

    public void init() {
         // si recuperano i parametri di inizializzazione del servlet
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // si memorizza l'errore
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // errori?
        if (erreurs.size() != 0) {
            return;
        }
         // si crea un oggetto IArticlesDomain per l'accesso al livello business
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // si registra l'errore
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // si memorizzano alcuni URL dell'applicazione
        hActionListe.put("href", "?action=" + ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", "?action=" + ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put(
            "href",
            "?action=" + ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // È tutto
        return;
    }


    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

         // si verifica l'esito dell'inizializzazione del servlet
        if (erreurs.size() != 0) {
             // si dispone dell'URL della pagina degli errori?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // si visualizza la pagina degli errori
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fine
            return;
        }
         // si elabora l'azione
        String action = request.getParameter("action");
        if (action == null) {
             // elenco degli articoli
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // elenco degli articoli
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // informazioni su un articolo
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // acquisto di un articolo
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // visualizzazione del carrello
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // rimozione di un articolo dal carrello
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // conferma del carrello
            doValidationPanier(request, response);
            return;
        }
         // azione sconosciuta
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // viene visualizzata la pagina degli errori
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // fine
        return;
    }


    private void doValidationPanier(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // l'acquirente ha confermato il carrello
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // conferma del carrello
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // anomalia
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // vengono recuperati gli errori
        ArrayList erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
            request.setAttribute(
                "actions",
                new Hashtable[] { hActionListe, hActionPanier });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // si visualizza l'elenco degli articoli
        request.setAttribute("message", "Votre panier a été validé");
        doListe(request, response);
         // fine
        return;
    }


    private void doRetirerAchat(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // si rimuove un articolo dal carrello
        try {
            Panier panier =
                (Panier) request.getSession().getAttribute("panier");
            String strIdAchat = request.getParameter("id");
            panier.enlever(Integer.parseInt(strIdAchat));
        } catch (NumberFormatException ignored) {
        } catch (NullPointerException ignored) {
        }
         // visualizza il carrello
        doPanier(request, response);
    }


    private void doPanier(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // visualizza il carrello
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // carrello vuoto?
        if (panier == null || panier.getAchats().size() == 0) {
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
                .forward(request, response);
             // fine
            return;
        }
         // c'è qualcosa nel carrello
        request.setAttribute("panier", panier);
        request.setAttribute(
            "actions",
            new Hashtable[] { hActionListe, hActionValidationPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_PANIER))
            .forward(request, response);
         // fine
        return;
    }


    private void doAchat(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // acquisto di un articolo
         // si recupera la quantità
        int qté = 0;
        try {
            qté = Integer.parseInt(request.getParameter("qte"));
            if (qté <= 0)
                throw new NumberFormatException();
        } catch (NumberFormatException ex) {
             // quantità errata
            request.setAttribute("msg", "Quantité incorrecte");
            request.setAttribute("qte", request.getParameter("qte"));
            String url =
                config.getInitParameter(URL_MAIN)
                    + "?action=infos&id="
                    + request.getParameter("id");
            getServletContext().getRequestDispatcher(url).forward(
                request,
                response);
             // fine
            return;
        }
         // recupero della sessione del cliente
        HttpSession session = request.getSession();
         // si crea l'acquisto
        Article article = (Article) session.getAttribute("article");
        Achat achat = new Achat(article, qté);
         // si aggiunge l'acquisto al carrello del cliente
        Panier panier = (Panier) session.getAttribute("panier");
        if (panier == null) {
            panier = new Panier();
            session.setAttribute("panier", panier);
        }
        panier.ajouter(achat);
         // si torna all'elenco degli articoli
        String url = config.getInitParameter(URL_MAIN) + "?action=liste";
        getServletContext().getRequestDispatcher(url).forward(
            request,
            response);
         // fine
        return;
    }


    private void afficheDebugInfos(
        HttpServletRequest request,
        HttpServletResponse response,
        ArrayList infos)
        throws ServletException, IOException {

         // viene visualizzato l'elenco degli articoli
        request.setAttribute("infos", infos);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_DEBUG))
            .forward(request, response);
         // fine
        return;
    }


    public void doPost(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // come sopra get
        doGet(request, response);
    }


    private void doInfos(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // l'elenco degli errori
        ArrayList erreurs = new ArrayList();
         // si recupera l'ID richiesto
        String strId = request.getParameter("id");
         // qualcosa?
        if (strId == null) {
             // non è normale
            erreurs.add("action incorrecte([infos,id=null]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // si trasforma strId in un numero intero
        int id = 0;
        try {
            id = Integer.parseInt(strId);
        } catch (Exception ex) {
             // non è normale
            erreurs.add("action incorrecte([infos,id=" + strId + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // si richiede l'ID della voce
        Article article = null;
        try {
            article=articlesDomain.getArticleById(id);
        } catch (UncheckedAccessArticlesException ex) {
             // non normale
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
        if (article == null) {
             // non corretto
            erreurs.add("Article de clé [" + id + "] inexistant");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // si inserisce l'articolo nella sessione
        request.getSession().setAttribute("article", article);
         // viene visualizzata la pagina informativa
        request.setAttribute("actions", new Hashtable[] { hActionListe });
         /   / request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_INFOS))
            .forward(request, response);
         // fine
        return;
    }

    private void afficheErreurs(
        HttpServletRequest request,
        HttpServletResponse response,
        ArrayList erreurs)
        throws ServletException, IOException {

         // viene visualizzata la pagina degli errori
        request.setAttribute("erreurs", erreurs);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
            .forward(request, response);
         // fine
        return;
    }


    private void doListe(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // l'elenco degli errori
        ArrayList erreurs = new ArrayList();
         // viene richiesta la lista degli articoli
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // si memorizza l'errore
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // Ci sono errori?
        if (erreurs.size() != 0) {
             // viene visualizzata la pagina degli errori
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fine
            return;
        }
         // visualizza l'elenco degli articoli
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // fine
        return;
    }

    /**
     * suivi console pour débogage
     * @param message : le message à afficher
     */
    private void affiche(String message) {
        System.out.println(message);
    }
}

Lasciamo al lettore il tempo necessario per leggere e comprendere questo codice. Speriamo che i commenti possano essergli d’aiuto.

3.5.10.5. Test dell’applicazione

Mostriamo alcuni screenshot dei test. Innanzitutto, la pagina iniziale dell’applicazione:

Image

L'URL richiesto era in realtà [http://localhost:8080/webarticles]. Il lettore noterà che nel file [web.xml] definiamo una pagina iniziale per l'applicazione:

    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>    

La vista [index.jsp] è definita come segue:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main?action=liste"/>

Si è quindi verificato un reindirizzamento all'URL [http://localhost:8080/webarticles/main?action=liste], come mostra l'URL del browser nella schermata. È stato quindi richiesto l’URL [/main?action=liste]. Sempre in [web.xml], l’URL /main è associato al servlet [webarticles]:

    <servlet-mapping>
        <servlet-name>webarticles</servlet-name>
        <url-pattern>/main</url-pattern>
    </servlet-mapping>

Sempre in [web.xml], il servlet [webarticles] è associato al servlet [ istia.st.articles.web.WebArticles]:

        <servlet-name>webarticles</servlet-name>
        <servlet-class>istia.st.articles.web.WebArticles</servlet-class>

La servlet [ istia.st.articles.web.WebArticles] viene quindi caricata dal contenitore di servlet Tomcat, se non lo era già, e viene eseguito il suo metodo [init]:

    public void init() {
         // si recuperano i parametri di inizializzazione del servlet
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // si memorizza l'errore
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // Ci sono errori?
        if (erreurs.size() != 0) {
            return;
        }
         // si crea un oggetto IArticlesDomain per l'accesso al livello business
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // si memorizza l'errore
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // si memorizzano alcuni URL dell'applicazione
        hActionListe.put("href", "?action=" + ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", "?action=" + ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put(
            "href",
            "?action=" + ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // operazione completata
        return;
    }

Note: il metodo [init]

  • verifica la presenza di determinati parametri di configurazione
  • istanzia un servizio di accesso al dominio dell’applicazione tramite Spring
  • imposta un elenco di errori per segnalare eventuali errori di inizializzazione
  • alcuni campi privati:
    • errori: l'elenco degli errori rilevati da [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: dizionari. Ciascuno di essi contiene le informazioni necessarie per visualizzare un'opzione del menu principale visualizzato dalla vista [entete.jsp]
    • acticlesDomain: il servizio di accesso al modello dell'applicazione

Il metodo [init] viene eseguito una sola volta, al caricamento iniziale del servlet. Successivamente, viene eseguito uno dei metodi [doGet, doPost] in base al tipo [GET, POST] della richiesta del client. In questo caso, entrambi i metodi eseguono la stessa operazione e il codice è stato inserito in [doGet]:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
         // si verifica l'esito dell'inizializzazione del servlet
        if (erreurs.size() != 0) {
             // si dispone dell'URL della pagina degli errori?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // si visualizza la pagina degli errori
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fine
            return;
        }
         // si elabora l'azione
        String action = request.getParameter("action");
        if (action == null) {
             // elenco degli articoli
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // elenco degli articoli
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // informazioni su un articolo
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // acquisto di un articolo
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // visualizzazione del carrello
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // rimozione di un articolo dal carrello
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // conferma del carrello
            doValidationPanier(request, response);
            return;
        }
         // azione sconosciuta
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // viene visualizzata la pagina degli errori
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // fine
        return;
    }
  • il metodo [doGet] inizia verificando se si sono verificati errori di inizializzazione al termine del metodo [init]. In caso affermativo, visualizza la vista [ERREURS] e il processo termina.
  • In caso contrario, recupera il parametro [action] dalla richiesta del client. Ricordiamo che l’applicazione è stata progettata per rispondere a richieste in cui è obbligatoriamente presente un parametro [action].
  • L'applicazione esegue il metodo associato all'azione. In questo caso si tratterà del metodo [doListe].

Il metodo [doListe] è il seguente:

    private void doListe(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // elenco degli errori
        ArrayList erreurs = new ArrayList();
         // viene richiesta la lista degli articoli
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // si memorizza l'errore
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // Ci sono errori?
        if (erreurs.size() != 0) {
             // viene visualizzata la pagina degli errori
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fine
            return;
        }
         // visualizza l'elenco degli articoli
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // fine
        return;
    }
  • Ricordiamo che il metodo [init] ha memorizzato il servizio di accesso al modello dell’applicazione (livello di dominio) in un campo privato del servlet:
     // campi privati
    private IArticlesDomain articlesDomain = null;
  • A partire da questo servizio di accesso, è possibile richiedere l'elenco degli articoli:
         // l'elenco degli errori
        ArrayList erreurs = new ArrayList();
         // si richiede l'elenco degli articoli
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // si memorizza l'errore
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
  • Se si verificano degli errori, viene inviata la vista [ERREURS]:
         // Ci sono errori?
        if (erreurs.size() != 0) {
             // viene visualizzata la pagina degli errori
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fine
            return;
        }
  • altrimenti viene inviata la vista [LISTE]:
1
2
3
4
5
6
7
         // visualizza l'elenco degli articoli
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);

Nel nostro esempio, tutto è andato a buon fine e abbiamo ottenuto la vista [LISTE]. Si invita il lettore a rileggere il codice della vista [LISTE] per verificare che i parametri dinamici richiesti da questa vista siano effettivamente forniti dal controller come sopra indicato. Lo stesso tipo di verifica va effettuato per ogni vista:

  • individuare i parametri dinamici della vista
  • assicurarsi che il controller li inserisca correttamente negli attributi della richiesta trasmessa alla vista

Ora illustriamo semplicemente il percorso delle schermate che un utente dell’applicazione si trova ad affrontare. Il lettore è invitato a ripetere ogni volta un ragionamento analogo al precedente:

Partendo dall’elenco degli articoli, l’utente può scegliere un articolo:

Qui l’acquirente può acquistare l’articolo n. 3. Commettiamo un errore di digitazione nella quantità:

L'errore è stato segnalato. Ora acquistiamo alcuni articoli:

L'acquisto è stato registrato e l'elenco degli articoli è stato aggiornato. Controlliamo il carrello:

L'acquisto è presente nel carrello. Rimuoviamolo:

L'articolo è stato rimosso dal carrello, che è stato aggiornato. Ora è vuoto.

Acquistiamo 100 articoli n. 3 e 2 articoli n. 4:

L'acquisto dell'articolo n. 3 non è stato possibile perché si volevano acquistarne 100 e ce n'erano solo 30 in magazzino. Questo articolo è rimasto nel carrello:

L'articolo n. 4 è stato invece acquistato, come dimostra la sua nuova disponibilità pari a 39 (40-1):

3.6. Architettura MVC con Struts

3.6.1. Architettura generale dell’applicazione

Torniamo all’architettura MVC dell’applicazione:

Nella versione precedente:

  • il controllo era gestito da un servlet
  • le viste erano gestite da pagine JSP
  • il modello era gestito da un insieme di tre file .jar

Nella versione Struts:

  • il controller sarà gestito da un servlet derivato dal servlet generico [ActionServlet] di Struts
  • le viste saranno gestite dalle stesse pagine JSP di prima, con qualche piccola differenza
  • il modello sarà gestito dagli stessi tre file .jar

Scopriremo che il passaggio all'applicazione Struts rispetto a quella precedente consiste nelle seguenti operazioni:

  • le azioni che venivano gestite in metodi specifici del servlet/controller sono ora gestite da istanze di classi derivate dalla classe [Action] di Struts
  • scrivere i file di configurazione [web.xml] e [struts-config.xml]
  • apportare alcune modifiche alle pagine JSP

Ricordiamo l’architettura generica MVC utilizzata da STRUTS:

M=modèle
le classi di business, le classi di accesso ai dati e il database
V=vues
le pagine JSP
C=contrôleur
il servlet di elaborazione delle richieste dei clienti, gli oggetti [Action] e i bean [ActionForm] associati ai moduli.
  • Il controller è il cuore dell’applicazione. Tutte le richieste del client passano attraverso di esso. Si tratta di un servlet generico fornito da STRUTS. In alcuni casi potrebbe essere necessario derivarla. Per i casi semplici, ciò non è necessario. Questo servlet generico preleva le informazioni di cui ha bisogno da un file solitamente denominato struts-config.xml.
  • Se la richiesta del client contiene parametri di modulo, questi vengono inseriti dal controller in un oggetto Bean. Gli oggetti Bean così creati nel corso del tempo vengono memorizzati nella sessione o nella richiesta del client. Questo aspetto è configurabile. Non è necessario ricrearli se sono già stati creati.
  • Nel file di configurazione struts-config.xml, a ogni URL che deve essere elaborato dal programma (e che quindi non corrisponde a una vista JSP che si potrebbe richiedere direttamente) vengono associate alcune informazioni:
    • il nome della classe di tipo Action incaricata di elaborare la richiesta. Anche in questo caso, l’oggetto Action istanziato può essere conservato nella sessione o nella richiesta.
    • se la URL richiesta è parametrizzata (nel caso dell’invio di un modulo al controller), viene indicato il nome del bean incaricato di memorizzare le informazioni del modulo.
  • Grazie a queste informazioni fornite dal proprio file di configurazione, alla ricezione di una richiesta di URL da parte di un client, il controller è in grado di determinare se esiste un bean da creare e quale. Una volta istanziato, il bean può verificare se i dati che ha memorizzato, provenienti dal modulo, sono validi o meno. Un metodo del bean denominato `validate` viene chiamato automaticamente dal controller. Il bean viene implementato dallo sviluppatore, il quale inserisce nel metodo `validate` il codice che verifica la validità dei dati del modulo. Se i dati risultano non validi, il controller non proseguirà oltre, ma passerà il controllo a una vista il cui nome troverà nel proprio file di configurazione. A questo punto lo scambio è terminato. Si noti che lo sviluppatore può richiedere che la validità del modulo non venga verificata. Lo fa anch’in questo caso nel file struts-config.xml. In tal caso, il controller non chiama il metodo validate del bean.
  • Se i dati del bean sono corretti, se non è prevista alcuna verifica o se non esiste alcun bean, il controller passa il controllo all’oggetto di tipo Action associato a URL. Lo fa richiedendo l’esecuzione del metodo `execute` di tale oggetto, al quale trasmette il riferimento al bean che ha eventualmente costruito. È qui che lo sviluppatore svolge il proprio compito: dovrà eventualmente ricorrere a classi di business o a classi di accesso ai dati. Al termine dell’elaborazione, l’oggetto `Action` restituisce al controller il nome della vista che deve inviare in risposta al client.
  • Nel proprio file di configurazione, il controller troverà l’oggetto URL associato al nome della vista che gli è stato chiesto di visualizzare. A quel punto invia quest’ultima. Lo scambio con il client è terminato.

Nella nostra applicazione non utilizzeremo oggetti [Bean] come oggetti buffer tra il client e le classi [Action]. L’oggetto [Action] recupererà direttamente i parametri della richiesta del client dall’oggetto [HttpServletRequest] che riceverà. Ciò facilita il porting della nostra applicazione iniziale. L’architettura finale della nostra applicazione sarà quindi la seguente:

M=modèle
le classi di business, le classi di accesso ai dati e il database
V=vues
le pagine JSP
C=contrôleur
il servlet di elaborazione delle richieste dei clienti, gli oggetti [Action]

3.6.2. Il modello

È stato presentato in precedenza. È costituito dagli archivi Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.6.3. Configurazione dell'applicazione

3.6.3.1. Architettura generale

L'architettura generale del progetto Eclipse è la seguente:

Image

3.6.3.2. Configurazione dell'accesso ai dati

Poiché l'interfaccia di accesso ai dati rimane invariata, i file di configurazione associati sono gli stessi della versione precedente. Sono definiti in [WEB-INF/src]:

Image

Nello screenshot sopra riportato, i file [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] sono quelli della versione precedente.

3.6.3.3. La directory degli archivi

In [WEB-INF/lib] si trovano gli stessi archivi della versione precedente, più quello necessario a Struts:

Image

3.6.3.4. Configurazione dell’applicazione

L'applicazione viene configurata tramite due file: [web.xml, struts-config.xml] nella cartella [WEB-INF]:

Image

Il file [web.xml] è il seguente:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
        <servlet-name>strutswebarticles</servlet-name>
        <servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
        <init-param>
            <param-name>config</param-name>
            <param-value>/WEB-INF/struts-config.xml</param-value>
        </init-param>
        <init-param>
            <param-name>springConfigFileName</param-name>
            <param-value>spring-config-sqlmap-firebird.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>strutswebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
        <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>        
</web-app>

Cosa contiene questo file?

  • la pagina iniziale dell'applicazione è [vues/index.jsp] (welcome-file)
  • le richieste URL del tipo *.do verranno reindirizzate al servlet [strutswebarticles] (servlet-mapping)
  • il servlet [strutswebarticles] è un'istanza della classe [ istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
  • questo servlet accetta due parametri di inizializzazione
    • il nome del file di configurazione di Struts (config)
    • il nome del file di configurazione di Spring (springConfigFileName)

Il file [struts-config.xml] è il seguente:

<?xml version="1.0" encoding="ISO-8859-1" ?>

<!DOCTYPE struts-config PUBLIC
          "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
          "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">

<struts-config>
    <action-mappings>
        <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action 
            path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherListeArticles" path="/main.do"/>
        </action>
        <action 
            path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
            <forward name="afficherPanier" path="/vues/panier.jsp"/>
            <forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
        </action>
        <action 
            path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
            <forward name="afficherPanier" path="/panier.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action 
            path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
            <forward name="afficherListeArticles" path="/main.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
    </action-mappings>
    <message-resources parameter="ApplicationResources" null="false" />
</struts-config>

Cosa indica questo file di configurazione?

  • che il nostro controller gestirà i seguenti URL:
main.do
per visualizzare l'elenco degli articoli
liste.do
per visualizzare l'elenco degli articoli
infos.do
per visualizzare le informazioni su un determinato articolo
achat.do
per acquistare un determinato articolo
panier.do
per visualizzare il carrello
retirerachat.do
per rimuovere un articolo dal carrello
validerpanier.do
per confermare il carrello
  • le azioni precedenti corrispondono una a una alle azioni gestite dal servlet nella versione precedente. Per ciascuna di esse sono specificate le seguenti informazioni:
  • il nome della classe incaricata di gestire tale azione
  • le possibili risposte (= viste) dopo l'elaborazione dell'azione. Il controller ne sceglierà una sola.
  • il nome di un file di messaggi per l’applicazione (message-resources). In questo caso, il file esisterà ma sarà vuoto. Non verrà utilizzato. Deve essere collocato nel [ClassPath] dell’applicazione. Qui verrà collocato nel [WEB-INF/classes]. In Eclipse, si ottiene questo risultato collocandolo nel [WEB-INF/src]:

Image

3.6.4. Le viste JSP

Anche le viste JSP utilizzate sono quelle della versione precedente. Sono state modificate pochissime cose: si tratta degli URL del tipo [?action=XX?id=YY& ...] che diventano [/XX.do?id=YY&....]. Riportiamo qui alcune spiegazioni già fornite in precedenza per evitare che l’utente debba tornare indietro. È importante comprendere che le informazioni trasmesse alla vista dal controller sono esattamente le stesse in entrambe le versioni. Nulla è stato modificato su questo punto.

3.6.4.1. entete.jsp

Per garantire una certa omogeneità alle diverse viste, queste condivideranno la stessa intestazione, quella che mostra il nome dell’applicazione con il menu:

Il menu è dinamico e definito dal controller. Quest’ultimo inserisce nella richiesta inviata alla pagina JSP un attributo chiave "actions" il cui valore associato è un array di tipo Hastable[]. Ogni elemento di questo array è un dizionario destinato a generare un’opzione del menu dell’intestazione. Ogni dizionario ha due chiavi:

  • href: l'URL associato all'opzione di menu
  • lien: il testo del menu

Le altre viste dell’applicazione utilizzeranno l’intestazione definita da [entete.jsp] tramite il seguente tag JSP:

<jsp:include page="entete.jsp"/>

All'esecuzione, questo tag avrà l'effetto di includere nel codice della pagina JSP che la contiene, quello della pagina [entete.jsp]. Poiché l'URL della pagina è relativo (manca il /), la pagina [entete.jsp] verrà cercata nella stessa cartella della pagina che contiene il tag <jsp:include>.

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
        <title>webarticles</title>
    </head>
    <body>
        <table>
            <tr>
                <td><h2>Magasin virtuel</h2></td>
                <c:forEach items="${actions}" var="action">
                    <td>|</td>
                    <td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
                </c:forEach>
            </tr>
        </table>
        <hr>

Commenti: nessuna modifica rispetto alla versione precedente

3.6.4.2. liste.jsp

Questa vista mostra l'elenco degli articoli disponibili per la vendita:

Viene visualizzata a seguito di una richiesta /main.do o /validerpanier.do. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l'array delle opzioni del menu
listarticles
ArrayList di oggetti di tipo [Article]
message
oggetto String - messaggio da visualizzare in fondo alla pagina

Ogni link [Infos] dell'array HTML degli articoli ha un URL della forma [/infos.do?id=ID], dove ID è il campo id dell'articolo visualizzato.

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Liste des articles</h2>
        <table border="1">
            <tr>
                <th>NOM</th><th>PRIX</th>
            </tr>
            <c:forEach var="article" items="${listarticles}">
                <tr>
                    <td><c:out value="${article.nom}"/></td>
                    <td><c:out value="${article.prix}"/></td>
                    <td><a href="<c:out value="infos.do?id=${article.id}"/>">Infos</a></td>
                </tr>
            </c:forEach>
        </table>
        <p>
        <c:out value="${message}"/>
    </body>
</html>

Commenti: una modifica (evidenziata sopra)

3.6.4.3. infos.jsp

Questa pagina mostra le informazioni relative a un articolo e consente anche di acquistarlo:

Image

Viene visualizzata a seguito di una richiesta /infos.do?id=ID o di una richiesta /achat.do?id=ID quando la quantità acquistata è errata. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l'array delle opzioni del menu
article
oggetto di tipo [Article] - articolo da visualizzare
msg
oggetto String - messaggio da visualizzare in caso di errore sulla quantità
qte
Oggetto String - valore da visualizzare nel campo di immissione [Qte]

I campi [msg] e [qte] vengono utilizzati in caso di errore di immissione della quantità:

Image

Questa pagina contiene un modulo che viene inviato tramite il pulsante [Acheter]. L'URL di destinazione di POST è [/achat.do?id=ID], dove ID è l'ID dell'articolo acquistato.

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Article d'id [<c:out value="${article.id}"/>]</h2>
        <table border="1">
            <tr>
                <th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
            </tr>
                <tr>
                    <td><c:out value="${article.nom}"/></td>
                    <td><c:out value="${article.prix}"/></td>
                    <td><c:out value="${article.stockActuel}"/></td>
                    <td><c:out value="${article.stockMinimum}"/></td>
                </tr>
        </table>
        <p>
        <form method="post" action="achat.do?id=<c:out value="${article.id}"/>"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
                    <td><c:out value="${msg}"/></td>
                </tr>
            </table>
        </form>
    </body>
</html>

Commenti: una modifica (evidenziata sopra)

3.6.4.4. panier.jsp

Questa schermata mostra il contenuto del carrello:

Image

Viene visualizzata a seguito di una richiesta /panier.do o /retirerachat.do?id=ID. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l'array delle opzioni del menu
panier
oggetto di tipo [Panier] - il carrello da visualizzare

Ogni link [Retirer] dell'array HTML relativo agli acquisti nel carrello ha un URL della forma [retirerachat.do?id=ID], dove ID è il campo [id] dell'articolo che si desidera rimuovere dal carrello.

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Contenu de votre panier</h2>
        <table border="1">
            <tr>
                <td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
            </tr>
            <c:forEach var="achat" items="${panier.achats}">
                <tr>
                    <td><c:out value="${achat.article.nom}"/></td>
                    <td><c:out value="${achat.qte}"/></td>
                    <td><c:out value="${achat.article.prix}"/></td>
                    <td><c:out value="${achat.total}"/></td>
                    <td><a href="<c:out value="retirerachat.do?id=${achat.article.id}"/>">Retirer</a></td>
                </tr>
            </c:forEach>
        </table>
        <p>
        Total de la commande : <c:out value="${panier.total}"/> euros
    </body>
</html>

Commenti: una modifica (evidenziata sopra)

3.6.4.5. paniervide.jsp

Questa schermata mostra l'informazione che indica che il carrello è vuoto:

Image

Viene visualizzata a seguito di una richiesta /panier.do o /retirerachat.do?id=ID. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l'array delle opzioni del menu

Codice:

1
2
3
4
5
6
7
8
9
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Contenu de votre panier</h2>
        <p>
        Votre panier est vide.
    </body>
</html>

Commenti: nessuna modifica.

3.6.4.6. erreurs.jsp

Questa schermata viene visualizzata in caso di errori:

Image

Viene visualizzata a seguito di qualsiasi richiesta che generi un errore, ad eccezione dell'azione di acquisto con una quantità errata, gestita dalla vista [INFOS]. Gli elementi della richiesta del controller sono i seguenti:

actions
oggetto Hashtable[] - l’array delle opzioni del menu
erreurs
ArrayList oggetti String che rappresentano i messaggi di errore da visualizzare

Codice:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Les erreurs suivantes se sont produites</h2>
        <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li><c:out value="${erreur}"/></li>
            </c:forEach>
        </ul>
    </body>
</html>

Commenti: nessuna modifica.

3.6.4.7. index.jsp

Questa pagina è definita come pagina iniziale dell'applicazione nel file [web.xml] dell'applicazione:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
....
    </servlet>
    <servlet-mapping>
....
    </servlet-mapping>
        <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>    
</web-app>

La vista [index.jsp] si limita a reindirizzare il client al punto di ingresso dell'applicazione:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>

Commenti: una modifica (evidenziata sopra)

3.6.5. Il controller Struts

Struts dispone di un controller generico denominato [ActionServlet]. È noto che un servlet dispone di un metodo [init] che consente di inizializzare l'applicazione all'avvio. Se utilizziamo il controller generico di Struts [ActionServlet], non abbiamo accesso al suo metodo [init]. In questo caso, abbiamo alcune operazioni da eseguire all’avvio dell’applicazione, essenzialmente la creazione di un’istanza di un oggetto di accesso al modello. Abbiamo quindi bisogno di un metodo [init]. Deriviamo quindi la classe [Actionservlet] dalla seguente classe [MainServlet]:

package istia.st.articles.web.struts;

import istia.st.articles.domain.IArticlesDomain;

import java.util.ArrayList;
import java.util.Hashtable;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;

import org.apache.struts.action.ActionServlet;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST - ISTIA
 *  
 */
public class MainServlet extends ActionServlet {
   // campi privati
  private ArrayList erreurs = new ArrayList();
  private IArticlesDomain articlesDomain = null;
  private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
  private final String[] parameters = { SPRING_CONFIG_FILENAME };
  private ServletConfig config;
  private final String ACTION_LISTE = "liste.do";
  private final String ACTION_PANIER = "panier.do";
  private final String ACTION_ACHAT = "achat.do";
  private final String ACTION_INFOS = "infos.do";
  private final String ACTION_RETIRER_ACHAT = "retirerachat.do";
  private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
  private String urlActionListe;
  private final String lienActionListe = "Liste des articles";
  private String urlActionPanier;
  private final String lienActionPanier = "Voir le panier";
  private String urlActionValidationPanier;
  private final String lienActionValidationPanier = "Valider le panier";
  private Hashtable hActionListe = new Hashtable(2);
  private Hashtable hActionPanier = new Hashtable(2);
  private Hashtable hActionValidationPanier = new Hashtable(2);

     // getter - setter
  public IArticlesDomain getArticlesDomain() {
    return articlesDomain;
  }
  public void setArticlesDomain(IArticlesDomain articlesDomain) {
    this.articlesDomain = articlesDomain;
  }

  public ArrayList getErreurs() {
    return erreurs;
  }
  public void setErreurs(ArrayList erreurs) {
    this.erreurs = erreurs;
  }

  public Hashtable getHActionListe() {
    return hActionListe;
  }
  public void setHActionListe(Hashtable actionListe) {
    hActionListe = actionListe;
  }

  public Hashtable getHActionPanier() {
    return hActionPanier;
  }
  public void setHActionPanier(Hashtable actionPanier) {
    hActionPanier = actionPanier;
  }

  public Hashtable getHActionValidationPanier() {
    return hActionValidationPanier;
  }
  public void setHActionValidationPanier(Hashtable actionValidationPanier) {
    hActionValidationPanier = actionValidationPanier;
  }

  public void init() throws ServletException{

     // inizializzazione della classe padre
    super.init();
     // si recuperano i parametri di inizializzazione del servlet
    config = getServletConfig();
    String param = null;
    for (int i = 0; i < parameters.length; i++) {
      param = config.getInitParameter(parameters[i]);
      if (param == null) {
         // si memorizza l'errore
        erreurs.add("Paramètre [" + parameters[i]
            + "] absent dans le fichier [web.xml]");
      }
    }
     // Ci sono errori?
    if (erreurs.size() != 0) {
      return;
    }
     // si crea un oggetto IArticlesDomain per l'accesso al livello business
    try {
      articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
          new ClassPathResource((String) config
              .getInitParameter(SPRING_CONFIG_FILENAME))))
          .getBean("articlesDomain");
    } catch (Exception ex) {
       // si memorizza l'errore
      erreurs.add("Erreur de configuration de l'accès aux données : "
          + ex.toString());
      return;
    }
     // si memorizzano alcuni URL dell'applicazione
    hActionListe.put("href", ACTION_LISTE);
    hActionListe.put("lien", lienActionListe);
    hActionPanier.put("href", ACTION_PANIER);
    hActionPanier.put("lien", lienActionPanier);
    hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
    hActionValidationPanier.put("lien", lienActionValidationPanier);

     // È tutto
    return;
  }
}

Commenti:

  • l’utilità della classe risiede nel suo metodo [init] e nei suoi campi privati
  • il metodo [init] svolge la stessa funzione del metodo [init] del controller della versione precedente:
    • verifica la presenza di determinati parametri di configurazione
    • istanzia un servizio di accesso al dominio dell'applicazione tramite Spring
    • imposta un elenco di errori per segnalare eventuali errori di inizializzazione
  • prima di iniziare a funzionare, il metodo [init] chiama il metodo [init] della classe padre [ActionServlet]. È quest’ultimo che gestirà il file di configurazione di Struts [struts-config.xml].
  • Sono definiti alcuni campi privati con i relativi accessori:
    • errori: l’elenco degli errori rilevati da [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: dizionari. Ciascuno di essi contiene le informazioni necessarie per visualizzare un'opzione del menu principale visualizzato dalla vista [entete.jsp]
    • acticlesDomain: il servizio di accesso al modello dell'applicazione
  • il controller di un'applicazione Struts è accessibile alle classi [Action] incaricate di gestire le diverse azioni possibili. Queste classi avranno accesso ai campi privati precedenti poiché questi sono dotati di accessori pubblici.

L'istanziazione di questo controller è garantita dal file [web.xml]:

<web-app>
    <servlet>
        <servlet-name>strutswebarticles</servlet-name>
        <servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
        <init-param>
            <param-name>config</param-name>
            <param-value>/WEB-INF/struts-config.xml</param-value>
        </init-param>
        <init-param>
            <param-name>springConfigFileName</param-name>
            <param-value>spring-config-sqlmap-firebird.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>strutswebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

Ogni URL che termina con .do verrà elaborato da un'istanza della classe [istia.st.articles.web.struts.MainServlet]

3.6.6. Le azioni dell'applicazione Struts

3.6.6.1. Introduzione

Ogni azione Struts sarà oggetto di una classe. Nella versione precedente, ogni azione era oggetto di un metodo nel controller dell’applicazione. La scrittura della classe [Action] consiste il più delle volte nel:

  • copiare e incollare il metodo utilizzato nella versione precedente
  • adattare il codice alle convenzioni di Struts

3.6.6.2. main.do, liste.do

Queste due azioni sono identiche e definite in [struts-config.xml] tramite:

1
2
3
4
5
6
7
8
        <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Quando una di queste azioni [main.do, liste.do] viene eseguita in un browser, si ottiene il seguente risultato:

Image

Il codice della classe [ListeArticlesAction] è il seguente:

package istia.st.articles.web.struts;

import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST - ISTIA
 *  
 */
public class ListeArticlesAction extends Action {

  /**
   * affichage de la liste des articles - s'appuie sur la couche [domain]
   * 
   * @param mapping :
   *          configuration de l'action dans struts-config.xml
   * @param form :
   *          le formulaire passé à l'action - ici aucun
   * @param request :
   *          la requête HTTP du client
   * @param response :
   *          la réponse HTTP au client
   */
  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // il servlet di controllo
    MainServlet mainServlet = (MainServlet) this.getServlet();

     // errori di inizializzazione?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // viene visualizzata la pagina degli errori
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

     // l'oggetto di accesso al dominio
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();

     // l'elenco degli errori
    ArrayList erreurs = new ArrayList();
     // viene richiesta la lista degli articoli
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // si memorizza l'errore
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
     // di errori?
    if (erreurs.size() != 0) {
       // viene visualizzata la pagina degli errori
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // viene visualizzato l'elenco degli articoli
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");
  }
}

Commenti:

  • scrivere il codice di una classe [Action] consiste essenzialmente nello scrivere il codice del suo metodo [execute]
  • alcune informazioni sono state memorizzate nell’istanza del controller. Si recupera un riferimento a quest’ultima tramite:
     // il servlet di controllo
    MainServlet mainServlet = (MainServlet) this.getServlet();
  • si recupera l'elenco degli errori di inizializzazione memorizzati dal controller. Se tale elenco non è vuoto, la vista [ERREURS] viene inviata al client:
1
2
3
4
5
6
7
8
     // errori di inizializzazione?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // viene visualizzata la pagina degli errori
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

La vista che verrà effettivamente inviata al cliente ci viene fornita da [struts-config.xml]:

        <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Si tratta della vista [/vues/erreurs.jsp]. Il lettore è invitato a verificare cosa ci si aspetta da questa vista. Queste informazioni sono fornite dall'azione nell'oggetto [request] sotto forma di attributi.

  • Sempre grazie al controller, l’azione può recuperare l’oggetto di accesso al modello dell’applicazione (livello di dominio):
     // l'oggetto di accesso al dominio
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • Fatto ciò, è possibile richiedere l’elenco degli articoli:
     // l'elenco degli errori
    ArrayList erreurs = new ArrayList();
     // viene richiesta la lista degli articoli
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // si memorizza l'errore
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • Se si verificano degli errori, viene inviata la vista [ERREURS]:
1
2
3
4
5
6
7
8
     // di errori?
    if (erreurs.size() != 0) {
       // viene visualizzata la pagina degli errori
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • altrimenti viene inviata la vista [LISTE]:
1
2
3
4
5
6
     // viene visualizzato l'elenco degli articoli
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");

La vista che verrà effettivamente inviata al cliente ci viene fornita da [struts-config.xml]:

        <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Si tratta della vista [/vues/liste.jsp]. Si invita il lettore a verificare cosa ci si aspetta da questa vista. Queste informazioni sono fornite dall’azione nell’oggetto [request] sotto forma di attributi.

3.6.6.3. infos.do

Questa azione serve a fornire informazioni su uno degli articoli visualizzati nella vista [LISTE]:

Questa azione è configurata come segue in [struts-config.xml]:

        <action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Il codice della classe [InfosArticleAction] è il seguente:

package istia.st.articles.web.struts;

import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST-ISTIA
 *  
 */
public class InfosArticleAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // il servlet di controllo
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // errori di inizializzazione?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // viene visualizzata la pagina degli errori
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // l'oggetto di accesso al dominio
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // l'elenco degli errori
    ArrayList erreurs = new ArrayList();
     // si recupera l'ID richiesto
    String strId = request.getParameter("id");
     // qualcosa?
    if (strId == null) {
       // non è normale
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // si trasforma strId in un numero intero
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // non è normale
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // si richiede l'ID della voce
    Article article = null;
    try {
      article = articlesDomain.getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // non normale
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
    if (article == null) {
       // non corretto
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // si inserisce l'articolo nella sessione
    request.getSession().setAttribute("article", article);
     // viene visualizzata la pagina informativa
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionListe() });
    return mapping.findForward("afficherInfosArticle");
  }
}

Commenti:

  • l'inizio del metodo [execute] è identico a quello esaminato in precedenza. Lo stesso vale anche per le altre azioni.
  • Il metodo recupera il parametro [id] che normalmente dovrebbe trovarsi nell'URL. Quest'ultimo deve infatti avere la forma [/infos.do?id=X]. Vengono effettuati diversi test per verificare la presenza e la validità del parametro [id]. In caso di problemi, viene inviata la vista [ERREURS].
  • Se [id] è valido, l’articolo corrispondente viene richiesto al livello [domain]. Se quest’ultimo genera un’eccezione o se l’articolo non viene trovato, anche in questo caso viene inviata la vista [ERREURS].
  • Se tutto va bene, l’articolo ottenuto viene inserito nella sessione. Si tratta di un punto discutibile. In questo caso, si parte dal presupposto che il cliente possa acquistare quell’articolo. Se lo fa, lo si recupererà dalla sessione, anziché richiederlo nuovamente al livello [domain].
  • Infine, viene visualizzata la vista [INFOS]. La vista che verrà effettivamente inviata al cliente ci viene fornita da [struts-config.xml]:
        <action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Si tratta della vista [/vues/infos.jsp]. Il lettore è invitato a verificare cosa ci si aspetta da questa vista. Queste informazioni sono fornite dall’azione nell’oggetto [request] sotto forma di attributi.

3.6.6.4. achat.do

Questa azione serve per acquistare l’articolo visualizzato dalla vista precedente [INFOS]:

Una volta acquistato l’articolo, viene nuovamente visualizzata la vista [LISTE] (vista a destra). Se si osserva il codice HTML della vista a sinistra sopra riportata, si nota che il tag <form> è definito come segue:

1
2
3
4
5
6
7
8
9
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>

Si nota quindi che il modulo viene inviato al controller con l’azione [achat.do].

Questa azione è configurata come segue in [struts-config.xml]:

        <action 
            path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherListeArticles" path="/main.do"/>
        </action>

Il codice della classe [AchatArticleAction] è il seguente:

package istia.st.articles.web.struts;

import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST
 *  
 */
public class AchatArticleAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // il servlet di controllo
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // errori di inizializzazione?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // si visualizza la pagina degli errori
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // l'elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
     // si recupera la quantità acquistata
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // quantità errata
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
     // si recupera la sessione del cliente
    HttpSession session = request.getSession();
     // si recupera l'articolo aggiunto alla sessione
    Article article = (Article) session.getAttribute("article");
     // sessione scaduta?
    if(article==null){
       // viene visualizzata la pagina degli errori
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // si crea il nuovo acquisto
    Achat achat = new Achat(article, qté);
     // si aggiunge l'acquisto al carrello del cliente
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // si torna all'elenco degli articoli
    return mapping.findForward("afficherListeArticles");
  }
}

Commenti:

  • l'inizio del metodo [execute] è identico a quelli esaminati in precedenza.
  • Ricordiamo la struttura del modulo inviato al controller:
1
2
3
4
5
6
7
8
9
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>
  • Nella richiesta sono presenti due parametri: [id]: numero dell'articolo acquistato, [qte]: quantità acquistata.
  • Vengono verificate la presenza e la validità del parametro [qte]. Se tale parametro viene riconosciuto come errato, all’utente viene restituita la vista [INFOS] accompagnata da un messaggio di errore:
     // elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
     // si recupera la quantità acquistata
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // quantità errata
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
  • l'articolo acquistato viene recuperato dalla sessione. È possibile che la sessione sia scaduta. In tal caso, viene inviata la vista [ERREURS]:
     // si recupera la sessione del cliente
    HttpSession session = request.getSession();
     // recupera l'articolo aggiunto alla sessione
    Article article = (Article) session.getAttribute("article");
     // sessione scaduta?
    if(article==null){
       // viene visualizzata la pagina degli errori
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
  • se la sessione non è scaduta, l'articolo viene aggiunto al carrello, anch'esso recuperato dalla sessione:
     // si recupera la sessione del cliente
    HttpSession session = request.getSession();
     // si recupera l'articolo aggiunto al carrello
    Article article = (Article) session.getAttribute("article");
     // sessione scaduta?
    if(article==null){
       // viene visualizzata la pagina degli errori
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // si crea il nuovo acquisto
    Achat achat = new Achat(article, qté);
     // si aggiunge l'acquisto al carrello del cliente
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • infine, si invia la vista [LISTE]:
     // si torna all'elenco degli articoli
    return mapping.findForward("afficherListeArticles");
  • La vista che verrà effettivamente inviata al cliente ci viene fornita da [struts-config.xml]:
        <action 
            path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherListeArticles" path="/main.do"/>
        </action>

Si tratta della vista [/main.do]. Questa vista non è una vista, ma un'azione. L'azione [/main.do] descritta in precedenza verrà quindi eseguita e visualizzerà l'elenco degli articoli.

3.6.6.5. panier.do

Questa azione serve a visualizzare tutti gli acquisti del cliente. È disponibile tramite l’opzione di menu [Voir le panier]:

Il codice HTML associato al link [Voir le panier] è il seguente:

<a href="panier.do">Voir le panier</a>

L'azione [panier.do] è configurata come segue in [struts-config.xml]:

        <action 
            path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
            <forward name="afficherPanier" path="/vues/panier.jsp"/>
            <forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
        </action>

Il codice della classe [VoirPanierAction] è il seguente:

package istia.st.articles.web.struts;

import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST-ISTIA
 *  
 */
public class VoirPanierAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // il servlet di controllo
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // errori di inizializzazione?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // viene visualizzata la pagina degli errori
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // visualizzazione del carrello
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // carrello vuoto
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    } else {
       // c'è qualcosa nel carrello
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(), mainServlet.getHActionValidationPanier() });
      return mapping.findForward("afficherPanier");
    }
  }
}

Commenti:

  • l'inizio del metodo [execute] è identico a quelli esaminati in precedenza.
  • Il carrello viene prelevato dalla sessione in cui si trova normalmente. La sessione potrebbe essere scaduta e quindi non si dispone di alcun carrello. Non si considera questo caso come un errore, ma si presume semplicemente che il carrello sia vuoto.
  • Se il carrello è vuoto, viene visualizzata la vista [PANIERVIDE]
  • altrimenti, viene visualizzata la vista [PANIER]

Le viste effettivamente inviate al cliente sono definite dall’azione:

        <action 
            path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
            <forward name="afficherPanier" path="/vues/panier.jsp"/>
            <forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
        </action>

3.6.6.6. retirerachat.do

Questa azione serve a rimuovere un acquisto dal carrello:

Dopo l'azione [retirerachat.do], il carrello viene visualizzato nuovamente (immagine a destra sopra). Se si osserva il codice HTML del link [Valider le panier] nell'immagine a sinistra sopra, si ottiene quanto segue:

<a href="retirerachat.do?id=3">Retirer</a>

L'azione [retirerachat.do] riceve quindi come parametro l'ID dell'articolo da rimuovere dal carrello. Questa azione è configurata come segue in [struts-config.xml]:

        <action 
            path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
            <forward name="afficherPanier" path="/panier.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Il codice della classe [RetirerAchatAction] è il seguente:

package istia.st.articles.web.struts;

import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST-ISTIA
 *  
 */
public class RetirerAchatAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // il servlet di controllo
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // errori di inizializzazione?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // viene visualizzata la pagina degli errori
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // l'elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
     // si recupera il carrello
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sessione scaduta
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    }
     // si recupera l'ID dell'articolo da rimuovere
    String strId = request.getParameter("id");
     // qualcosa?
    if (strId == null) {
       // non è normale
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // si trasforma strId in un numero intero
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // non è normale
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // si rimuove l'acquisto
    panier.enlever(id);
     // si visualizza nuovamente il carrello
    return mapping.findForward("afficherPanier");
  }
}

Commenti:

  • l'inizio del metodo [execute] è identico a quelli esaminati in precedenza.
  • il codice che segue ha lo scopo di verificare la presenza e la validità del parametro [id]. Se risulta errato, viene inviata la vista [ERREURS].
  • In caso contrario, l'acquisto viene rimosso dal carrello:
     // si rimuove l'acquisto
    panier.enlever(id);
  • quindi il carrello viene visualizzato nuovamente:
     // si visualizza nuovamente il carrello
    return mapping.findForward("afficherPanier");

La vista effettivamente inviata al client è definita dall'azione:

        <action 
            path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
            <forward name="afficherPanier" path="/panier.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Si nota che, per quanto riguarda la vista, verrà attivata l'azione [/panier.do]. Questa è già stata descritta. Visualizzerà la vista [PANIER] o [PANIERVIDE] a seconda dello stato del carrello.

3.6.6.7. validerpanier.do

Questa azione serve a confermare gli acquisti effettuati dal cliente. In pratica, ciò si traduce in un'unica operazione: le scorte degli articoli acquistati vengono ridotte delle quantità acquistate nel database. Questa azione proviene dal seguente menu:

 

Il codice HTML del link [Valider le panier] è il seguente:

<a href="validerpanier.do">Valider le panier</a>

Quando questo link viene attivato, le scorte vengono decrescute e l'elenco degli articoli viene nuovamente visualizzato.

Questa azione è configurata come segue in [struts-config.xml]:

        <action 
            path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
            <forward name="afficherListeArticles" path="/main.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Il codice della classe [ValiderPanierAction] è il seguente:

package istia.st.articles.web.struts;

import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST-ISTIA
 *  
 */
public class ValiderPanierAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // il servlet di controllo
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // errori di inizializzazione?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // visualizza la pagina degli errori
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // l'oggetto di accesso al dominio
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // l'elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
         // l'acquirente ha confermato il carrello
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sessione scaduta
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
         // conferma del carrello
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // non normale
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
        }
         // si recuperano eventuali errori
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
         // tutto sembra a posto OK - si visualizza l'elenco degli articoli
    return mapping.findForward("afficherListeArticles");
  }
}

Commenti:

  • l'inizio del metodo [execute] è identico a quelli esaminati in precedenza.
  • Si recupera il carrello dalla sessione. Se questa è scaduta, si invia la vista [ERREURS]:
     // elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
         // l'acquirente ha confermato il carrello
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sessione scaduta
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • Si confermano gli acquisti nel carrello. Possono verificarsi errori se le scorte sono insufficienti a soddisfare gli acquisti. In questo caso, si invia la vista [ERREURS]:
         // conferma del carrello
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // anomalia
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
        }
         // si recuperano eventuali errori
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
  • se tutto è andato a buon fine, viene nuovamente visualizzato l’elenco degli articoli:
         // tutto sembra a posto OK - si visualizza l'elenco degli articoli
    return mapping.findForward("afficherListeArticles");

La vista effettivamente inviata al cliente è definita dall’azione:

        <action 
            path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
            <forward name="afficherListeArticles" path="/main.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Si nota che, per quanto riguarda la vista, verrà attivata l'azione [/main.do]. Questa è già stata descritta. Visualizzerà la vista [LISTE].

3.7. Architettura di MVC con Spring

3.7.1. Architettura generale dell’applicazione

Torniamo all’architettura MVC dell’applicazione:

Nella prima versione:

  • il controllo era gestito da un servlet
  • le viste erano gestite da pagine JSP
  • il modello era gestito da un insieme di tre file .jar

Nella versione Struts:

  • il controller era gestito da un servlet derivato dal servlet generico [ActionServlet] di Struts
  • le viste erano gestite dalle stesse pagine JSP della versione [Struts]
  • il modello era gestito dagli stessi tre file .jar

Nella versione Spring:

  • il controller sarà gestito da una servlet fornita da Spring [DispatcherServlet]
  • le viste saranno gestite dalle stesse pagine JSP di prima, con qualche piccola differenza
  • il modello sarà gestito dagli stessi tre file

Scopriremo che il passaggio da Struts a Spring della nostra applicazione è semplice se si accetta di non utilizzare tutti gli elementi raccomandati per un'architettura Spring MVC ortodossa. Le principali modifiche sono le seguenti:

  • le azioni che in Struts venivano gestite in metodi specifici del servlet/controller, oppure tramite istanze di classi derivate dalla classe [Action], sono ora gestite da istanze di classi che implementano l’interfaccia Spring [Controller]
  • i file di configurazione necessari sono i seguenti:
    • [web.xml], poiché si tratta di un’applicazione web. Questo file contiene un Listener che, al momento dell’inizializzazione dell’applicazione, utilizzerà il file [applicationContext.xml]
    • [applicationContext.xml] che consentirà di creare i bean necessari all’applicazione, in particolare quello del servizio di accesso al modello
  • le viste JSP saranno identiche a quelle di Struts. Sarà necessario crearne una nuova.

Ricordiamo l’architettura MVC STRUTS utilizzata nella versione precedente:

M=modèle
le classi di business, le classi di accesso ai dati e il database
V=vues
le pagine JSP
C=contrôleur
il servlet di elaborazione delle richieste dei clienti, gli oggetti [Action]

Con Spring, adottiamo un'architettura identica:

M=modèle
le classi di business, le classi di accesso ai dati e il database
V=vues
le pagine JSP
C=contrôleur
il servlet per l'elaborazione delle richieste dei clienti, gli oggetti che implementano l'interfaccia [Controller]
  • il controller è il cuore dell’applicazione. Tutte le richieste del client transitano attraverso di esso. Si tratta di un servlet generico fornito da SPRING. È di tipo [DispatcherServlet]. D’ora in poi chiameremo questo controller «controller [Spring]».
  • Il controller [Spring] indirizzerà la richiesta del client verso una delle istanze di tipo [Controller]. Ci sarà un’istanza di questo tipo per ogni azione da elaborare. Questa è definita nell’URL richiesto, come avviene con Struts. In questo modo sapremo che l’azione richiesta è l’azione [liste] perché l’URL richiesto è [liste.do]
  • se C è il contesto dell’applicazione, il controller [Spring] utilizza un file [C-servlet.xml] che svolge il ruolo del file di configurazione struts-config.xml, nella versione Struts. A ogni azione che deve essere elaborata dall’applicazione viene associato il nome della classe di tipo Controller incaricata di gestire la richiesta.
  • Il controller passa il controllo all'oggetto di tipo Controller associato all'azione. Lo fa richiedendo l'esecuzione del metodo handleRequest di tale oggetto, al quale trasmette la richiesta del client. È qui che lo sviluppatore svolge il proprio compito: dovrà eventualmente ricorrere a classi di business o a classi di accesso ai dati. Al termine dell’elaborazione, l’oggetto Controller restituisce al controller il nome della vista che deve inviare in risposta al client.
  • Nel proprio file di configurazione, il controller troverà la classe URL associata al nome della vista che gli è stato chiesto di visualizzare. A quel punto invia quest'ultima. Lo scambio con il client è terminato.

3.7.2. Il modello

È lo stesso delle due versioni precedenti. È costituito dagli archivi Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.7.3. Configurazione dell’applicazione

3.7.3.1. Architettura generale

L’architettura generale del progetto Eclipse è la seguente:

Image

3.7.3.2. Configurazione dell'accesso ai dati

Poiché l'interfaccia di accesso ai dati rimane invariata, i file di configurazione associati sono gli stessi della versione precedente. Sono definiti in [WEB-INF/src]:

Image

Nello screenshot sopra riportato, i file [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] sono quelli delle versioni precedenti.

3.7.3.3. La directory degli archivi

In [WEB-INF/lib] si trovano gli stessi archivi della versione precedente, ad eccezione di quelli relativi a Struts, che non è più necessario:

Image

3.7.3.4. Configurazione dell’applicazione

L'applicazione viene configurata tramite tre file: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] nella cartella [WEB-INF]:

Image

Il file [web.xml] è il seguente:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
     <!-- caricatore del contesto Spring dell'applicazione -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- il servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- il mapping degli URL -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- il documento di input -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

Cosa contiene questo file?

  • la pagina iniziale dell’applicazione è [/vues/index.jsp] (welcome-file)
  • le richieste URL del tipo *.do verranno reindirizzate al servlet [springwebarticles] (servlet-mapping)
  • il servlet [springwebarticles] è un'istanza della classe [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class) fornita da Spring.
  • Il listener [org.springframework.web.context.ContextLoaderListener] verrà avviato all'avvio dell'applicazione. Il suo ruolo principale sarà quello di istanziare i bean Spring definiti nel file [applicationContext.xml]

Il file [applicationContext.xml] è il seguente:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la classe di accesso ai dati -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la classe di business -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- la configurazione dell'applicazione web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Si ritrovano elementi noti e altri meno conosciuti. Tre bean verranno istanziati durante l’inizializzazione dell’applicazione:

  • articlesDao: servizio di accesso al livello [dao]
  • articlesDomain: servizio di accesso al modello
  • config: un bean in cui raccoglieremo le informazioni che tutti i client devono condividere. Questo bean svolgerà il ruolo che tradizionalmente ricopre il contesto dell’applicazione, ma con informazioni tipizzate anziché non tipizzate.

L'ultimo file [springwebarticles.xml] definisce le azioni accettate dall'applicazione in modo molto simile a quello utilizzato dal file Struts [struts-config.xml]. Il suo contenuto è il seguente:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- i gestori delle azioni = controller -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

     <!-- mappatura dell'applicazione-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

     <!-- gestore delle viste -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

     <!-- il file dei messaggi -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>

Cosa indica questo file di configurazione?

  • che il nostro controller gestirà i seguenti URL:
main.do
per visualizzare l'elenco degli articoli
liste.do
per visualizzare l'elenco degli articoli
infos.do
per visualizzare le informazioni su un determinato articolo
achat.do
per acquistare un determinato articolo
panier.do
per visualizzare il carrello
retirerachat.do
per rimuovere un articolo dal carrello
validerpanier.do
per confermare il carrello
  • le azioni precedenti corrispondono una a una alle azioni gestite dal controller nelle versioni precedenti. Per ciascuna di esse è specificato il nome della classe incaricata di gestirla. Prendiamo l'esempio dell'azione [/panier.do]:
  • deve essere gestita dal bean [VoirPanierController]. Questo nome è arbitrario. Si tratta di una semplice chiave.
                <prop key="/panier.do">VoirPanierController</prop>
  • la chiave [VoirPanierController] è il nome di un bean definito nello stesso file di configurazione:
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
  • Il bean [VoirPanierController] definisce:
  • la classe da istanziare [ istia.st.articles.web.spring.VoirPanierController] per gestire l'azione
  • come istanziarla. In questo caso, il bean [config], definito da [applicationContext.xml] e istanziato all'avvio dell'applicazione, viene fornito come parametro. Ciò avverrà per tutte le azioni [Controller]. In questo modo, ciascuna di esse disporrà, in un campo privato, dell’oggetto [config], nel quale troverà tutte le informazioni condivise tra tutti i client.
  • Come devono essere risolti i nomi delle viste:
     <!-- gestore delle viste -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

Come con Struts, l'istanza [Controller] che elabora un'azione restituirà, al termine dell'elaborazione, una chiave al controller Spring per indicargli quale vista deve visualizzare. A partire da questa chiave, possono esistere diverse strategie per generare la vista associata alla chiave. La strategia scelta è quella definita dal bean [viewResolver]. In questo caso, tale bean è associato alla classe [org.springframework.web.servlet.view.InternalResourceViewResolver] con diversi parametri di inizializzazione. Senza entrare nei dettagli, il bean [viewResolver] specifica in questo caso che se la chiave della vista è "XX", allora la vista generata sarà [/vues/XX.jsp]. Il tipo di viste inviate al client può essere modificato in vari modi:

  • cambiando la classe di implementazione del bean [viewResolver]
  • modificando i parametri di inizializzazione della classe di implementazione

In questo modo, è possibile passare da una vista HTML a una vista XML semplicemente modificando il valore del bean [viewResolver]

  • il nome di un file di messaggi per l’applicazione (messageSource). In questo caso, il file esisterà ma sarà vuoto. Non verrà utilizzato. Deve essere collocato nel [ClassPath] dell’applicazione. Qui verrà collocato in [WEB-INF/classes]. In Eclipse, si ottiene questo risultato collocandolo in [WEB-INF/src]:

Image

3.7.4. Le viste JSP

Le viste JSP utilizzate saranno quelle impiegate da Struts. Nessuna di esse viene modificata:

Image

Viene creata una sola nuova vista: redirpanier.jsp. Serve esclusivamente a reindirizzare il client all’azione [/panier.do]. Il suo codice è il seguente:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/panier.do"/>

Si invita il lettore a rileggere la definizione delle diverse viste nella versione Struts.

3.7.5. Elaborazione delle azioni

Le classi necessarie per l'elaborazione delle diverse azioni sono state raggruppate nel pacchetto [istia.st.articles.web.spring]:

Image

Ricordiamo il funzionamento dell'applicazione Spring con un esempio:

  • l'utente richiede l'URL [http://localhost:8080/springwebarticles/main.do]

Image

Cosa è successo?

  • è stato consultato il file [web.xml] dell’applicazione [springwebarticles]:
<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
     <!-- caricatore del contesto Spring dell'applicazione -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- il servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- mappatura degli URL -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- il documento di input -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  • Se si trattava della prima richiesta all'applicazione, sono state attivate alcune operazioni:
    • è stato caricato il listener [ org.springframework.web.context.ContextLoaderListener]
    • ha utilizzato il file di configurazione [applicationContext.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la classe di accesso ai dati -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la classe di business -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- la configurazione dell'applicazione web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>
  • i bean sopra indicati sono stati creati nel contesto dell'applicazione
  • è stato quindi utilizzato il file [springwebarticles-servlet.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- i gestori delle azioni = controller -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

     <!-- mappatura dell'applicazione-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

     <!-- gestore delle viste -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

     <!-- il file dei messaggi -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>
  • sono stati creati anche i bean [Controller] definiti da questo file
  • Ora è tutto pronto per elaborare la richiesta del client. La richiesta era: [http://localhost:8080/springwebarticles]. In questo caso, non viene richiesto un URL del contesto, ma il contesto stesso. Viene quindi utilizzata la sezione [welcome-file] del file [web.xml].
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
  • la vista [index.jsp] è la seguente:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>
  • il client viene quindi invitato a reindirizzarsi all'URL [http://localhost:8080/springwebarticles/main.do]. E lo fa.
  • Il controller Spring riceve quindi una nuova richiesta. Utilizza il proprio file [springwebarticles-servlet.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- gestori di azioni = controller -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

     <!-- mappatura dell'applicazione-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

</beans>
  • questo file gli indica che l’azione [/main.do] deve essere gestita dal bean [ListController].
  • La richiesta del client viene inoltrata al metodo [handleRequest] del bean [ListController]. Quest’ultimo esegue la propria operazione e restituisce al controller la chiave della vista da visualizzare. In questo caso, se tutto va bene, tale chiave sarà [liste].
  • Il controller Spring utilizza il bean [viewResolver] del file di configurazione [springwebarticles-servlet.xml] per determinare la vista associata a tale chiave. In questo caso, sarà la vista [/vues/liste.jsp]
  • la vista [/vues/liste.jsp] viene inviata al client

3.7.6. Inizializzazione dell’applicazione Spring

Abbiamo detto che all’avvio dell’applicazione vengono istanziati i bean del file [applicationContext.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la classe di accesso ai dati -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la classe di business -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- la configurazione dell'applicazione web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Conosciamo i bean [articlesDao, articlesDomain] ma non il bean [config]. Quest’ultimo è definito dalla seguente classe Java:

package istia.st.articles.web.spring;

import java.util.Hashtable;
import istia.st.articles.domain.IArticlesDomain;

/**
 * @author ST - ISTIA
 */

public class Config {

     // campi privati
    private IArticlesDomain articlesDomain = null;
    private final String ACTION_LISTE = "liste.do";
    private final String ACTION_PANIER = "panier.do";
    private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
    private final String lienActionListe = "Liste des articles";
    private final String lienActionPanier = "Voir le panier";
    private final String lienActionValidationPanier = "Valider le panier";
    private Hashtable hActionListe = new Hashtable(2);
    private Hashtable hActionPanier = new Hashtable(2);
    private Hashtable hActionValidationPanier = new Hashtable(2);

     // getter e setter
    public IArticlesDomain getArticlesDomain() {
        return articlesDomain;
    }

    public void setArticlesDomain(IArticlesDomain articlesDomain) {
        this.articlesDomain = articlesDomain;
    }

    public Hashtable getHActionListe() {
        return hActionListe;
    }

    public Hashtable getHActionPanier() {
        return hActionPanier;
    }

    public Hashtable getHActionValidationPanier() {
        return hActionValidationPanier;
    }

     // inizializzazione dell'applicazione web
    public void init() {
         // si memorizzano alcuni URL dell'applicazione
        hActionListe.put("href", ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // È tutto
        return;
    }
}

Questa classe esegue ciò che fa il metodo [init] di un servlet di un’applicazione web. Inizializza l’applicazione. In questo caso, ciò avviene nel modo seguente:

  • poiché il bean [config] è definito come segue in [applicationContext.xml]:
     <!-- configurazione dell'applicazione web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>

al momento della creazione, il suo campo privato [articlesDomain] è stato inizializzato

  • Successivamente, a causa dell'attributo [init-method="init"] del bean sopra indicato, viene eseguito il metodo [init] della classe associata al bean. Qui, il metodo inizializza i tre dizionari [hActionListe, hActionPanier, hActionValidationPanier] utilizzati per generare i tre possibili collegamenti del menu offerto all’utente.
  • Vengono creati dei metodi di accesso pubblici per rendere accessibili questi campi privati alle istanze di tipo [Controller] che gestiranno le azioni.

3.7.7. Le azioni [Controller] dell’applicazione Spring

3.7.7.1. Introduzione

Ogni azione Spring sarà oggetto di una classe di tipo [Controller]. Nella versione Struts, ogni azione era oggetto di una classe di tipo [Action]. La scrittura della classe [Controller] consiste, nella maggior parte dei casi, nel:

  • nel copiare e incollare la classe [Action] utilizzata nella versione Struts
  • ad adattare il codice alle convenzioni di Spring

3.7.7.2. main.do, liste.do

Queste due azioni sono identiche e definite in [springwebarticles-servlet.xml] da:

     <!-- mappatura dell'applicazione-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
...
            </props>
        </property>
    </bean>

    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Sono associate alla classe [ istia.st.articles.web.spring.ListController] che descriveremo in dettaglio tra poco. Quando una di queste azioni viene eseguita in un browser, si ottiene il seguente risultato:

Image

Il codice della classe [istia.st.articles.web.spring.ListController] è il seguente:

package istia.st.articles.web.spring;

import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class ListController implements Controller {

     // configurazione dell'applicazione web
    Config config;

    public void setConfig(Config config) {
        this.config = config;
    }

     // elaborazione della richiesta
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

         // viene richiesta la lista degli articoli
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // si registra l'errore
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // viene visualizzata la pagina degli errori
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // invia la pagina degli errori
            return new ModelAndView("erreurs");
        }
         // viene visualizzato l'elenco degli articoli
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // invia la vista
        return new ModelAndView("liste");
    }

}

Commenti:

  • la classe ha un campo privato [config]. Questo campo è stato inizializzato da Spring al momento dell’istanziazione del bean [ListController]:
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Come si può vedere sopra, il campo [config] di [ListController] viene inizializzato con il bean [config]. Di cosa si tratta? Si tratta del bean [config] definito in [applicationContext.xml], c.a.d e di un’istanza di [istia.st.articles.web.spring.Config] descritta in precedenza.

  • Scrivere il codice di una classe [Controller] consiste essenzialmente nello scrivere il codice del suo metodo [handleRequest]
  • si richiede l’elenco degli articoli al modello. Quest’ultimo è accessibile tramite [config.getArticlesDomain()]. Se si verifica un’eccezione, viene inviata la vista [ERREURS]. Il risultato restituito da [handleRequest] deve essere di tipo [ModelAndView]. È possibile istanziare questa classe in diversi modi. In questo caso, e così sarà sempre, si crea un’istanza di [ModelAndView] passandole la chiave della vista da visualizzare. Si ricorda che, in base alla configurazione del bean [viewResolver], la richiesta di visualizzazione della vista con chiave XX comporterà l’invio della vista [/vues/XX.jsp].
         // si richiede l'elenco degli articoli
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // si registra l'errore
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // visualizza la pagina degli errori
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // invia la pagina degli errori
            return new ModelAndView("erreurs");
        }
  • se non ci sono errori, viene inviata la vista [LISTE]:
1
2
3
4
5
6
7
         // visualizza l'elenco degli articoli
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // invia la vista
        return new ModelAndView("liste");

3.7.7.3. infos.do

Questa azione serve a fornire informazioni su uno degli articoli visualizzati nella vista [LISTE]:

Questa azione è definita in [springwebarticles-servlet.xml] da:

     <!-- la mappatura dell'applicazione-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/infos.do">InfosController</prop>
...
            </props>
        </property>
    </bean>
...
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Il codice della classe [InfosController] è il seguente:

package istia.st.articles.web.spring;

import istia.st.articles.dao.Article;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class InfosController implements Controller {

     // la configurazione dell'applicazione web
    Config config;

    public void setConfig(Config config) {
        this.config = config;
    }

     // elaborazione della richiesta
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

     // l'elenco degli errori
    ArrayList erreurs = new ArrayList();
     // si recupera l'ID richiesto
    String strId = request.getParameter("id");
     // qualcosa?
    if (strId == null) {
       // non è normale
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // si trasforma strId in un numero intero
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // non è normale
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // si richiede l'ID della voce
    Article article = null;
    try {
      article = config.getArticlesDomain().getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // non normale
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
    if (article == null) {
       // non corretto
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // si inserisce l'articolo nella sessione
    request.getSession().setAttribute("article", article);
     // viene visualizzata la pagina informativa
    request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
        return new ModelAndView("infos");
    }
}

Commenti:

  • il metodo [handleRequest] recupera il parametro [id] che normalmente dovrebbe trovarsi nell'URL. Quest'ultimo deve infatti avere la forma [/infos.do?id=X]. Vengono effettuati diversi test per verificare la presenza e la validità del parametro [id]. In caso di problema, viene inviata la vista [ERREURS].
  • Se [id] è valido, l’articolo corrispondente viene richiesto al livello [domain]. Se quest’ultimo genera un’eccezione o se l’articolo non viene trovato, anche in questo caso viene inviata la vista [ERREURS].
  • Se tutto va bene, l’articolo ottenuto viene inserito nella sessione. Si tratta di un punto discutibile. In questo caso, si parte dal presupposto che il cliente possa acquistare quell’articolo. Se lo fa, lo si recupererà dalla sessione, anziché richiederlo nuovamente al livello [domain].
  • Infine, viene visualizzata la vista [INFOS].

3.7.7.4. achat.do

Questa azione serve ad acquistare l’articolo visualizzato dalla vista precedente [INFOS]:

Image

Se si esamina il codice HTML di questa vista, si nota che il tag <form> è definito come segue:

        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>

Si vede quindi che il modulo viene inviato al controller con l'azione [achat.do].

Tale azione è configurata come segue in [springwebarticles-servlet.xml]:

     <!-- mappatura dell'applicazione-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/achat.do">AchatController</prop>
...
            </props>
        </property>
    </bean>

    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Il codice della classe [AchatController] è il seguente:

package istia.st.articles.web.spring;

import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class AchatController implements Controller {

   // la configurazione dell'applicazione web
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // elaborazione della richiesta
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // l'elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
     // si recupera la quantità acquistata
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // quantità errata
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
     // recupero della sessione del cliente
    HttpSession session = request.getSession();
     // si recupera l'articolo aggiunto alla sessione
    Article article = (Article) session.getAttribute("article");
     // sessione scaduta?
    if (article == null) {
       // viene visualizzata la pagina degli errori
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // si crea il nuovo acquisto
    Achat achat = new Achat(article, qté);
     // si aggiunge l'acquisto al carrello del cliente
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // si torna all'elenco degli articoli
    return new ModelAndView("index");
  }
}

Commenti:

  • ricordiamo la struttura del modulo inviato al controller:
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>
  • Nella richiesta sono presenti due parametri: [id]: numero dell'articolo acquistato, [qte]: quantità acquistata.
  • Vengono verificate la presenza e la validità del parametro [qte]. Se tale parametro viene riconosciuto come errato, all’utente viene restituita la vista [INFOS] accompagnata da un messaggio di errore:
     // elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
     // si recupera la quantità acquistata
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // quantità errata
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
  • l'articolo acquistato viene recuperato dalla sessione. È possibile che la sessione sia scaduta. In tal caso, viene inviata la vista [ERREURS]:
     // si recupera la sessione del cliente
    HttpSession session = request.getSession();
     // si recupera l'articolo aggiunto alla sessione
    Article article = (Article) session.getAttribute("article");
     // sessione scaduta?
    if (article == null) {
       // viene visualizzata la pagina degli errori
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • se la sessione non è scaduta, l'articolo viene aggiunto al carrello, anch'esso recuperato dalla sessione:
1
2
3
4
5
6
7
8
9
     // si crea il nuovo acquisto
    Achat achat = new Achat(article, qté);
     // si aggiunge l'acquisto al carrello del cliente
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • infine, si invia la vista [LISTE]:
     // si torna all'elenco degli articoli
    return new ModelAndView("index");
  • Sopra, viene inviata la vista [/vues/index.jsp]. Sappiamo che questa richiede al browser client di reindirizzarsi all’URL [/main.do]. È questo reindirizzamento che visualizzerà l’elenco degli articoli.

3.7.7.5. panier.do

Questa azione serve a visualizzare tutti gli acquisti del cliente. È disponibile tramite il menu:

Image

Il codice HTML associato al link sopra indicato è il seguente:

<a href="panier.do">Voir le panier</a>

La pagina a cui rimanda questo link è la seguente:

Image

Questa azione è configurata come segue in [springwebarticles-servlet.xml]:

     <!-- mappatura dell'applicazione-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/panier.do">VoirPanierController</prop>
...
            </props>
        </property>
    </bean>
...
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Il codice della classe [VoirPanierController] è il seguente:

package istia.st.articles.web.spring;

import istia.st.articles.domain.Panier;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class VoirPanierController implements Controller {

   // configurazione dell'applicazione web
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // elaborazione della richiesta
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // visualizzazione del carrello
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // carrello vuoto
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("paniervide");
    } else {
       // c'è qualcosa nel carrello
      request.setAttribute("actions", new Hashtable[] {
          config.getHActionListe(), config.getHActionValidationPanier() });
      return new ModelAndView("panier");
    }
  }
}

Commenti:

  • il carrello viene prelevato dalla sessione in cui si trova normalmente. È possibile che la sessione sia scaduta e quindi non sia presente alcun carrello. In questo caso non viene generato un errore, ma si considera semplicemente che il carrello sia vuoto.
  • Se il carrello è vuoto, viene visualizzata la vista [PANIERVIDE]
  • altrimenti, viene visualizzata la vista [PANIER]

3.7.7.6. retirerachat.do

Questa azione serve a rimuovere un articolo dal carrello:

Image

Se si osserva il codice HTML del link sopra riportato, si ottiene quanto segue:

<a href="retirerachat.do?id=3">Retirer</a>

L'azione [retirerachat.do] riceve quindi come parametro l'ID dell'articolo da rimuovere dal carrello. Questa azione è configurata come segue in [springwebarticles-servlet.xml]:

     <!-- mappatura dell'applicazione-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/retirerachat.do">RetirerAchatController</prop>
            </props>
        </property>
    </bean>
...
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Il codice della classe [RetirerAchatController] è il seguente:

package istia.st.articles.web.spring;

import istia.st.articles.domain.Panier;

import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class RetirerAchatController implements Controller {

   // la configurazione dell'applicazione web
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // elaborazione della richiesta
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // l'elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
     // recupero del carrello
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sessione scaduta
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // recupero dell'ID dell'articolo da rimuovere
    String strId = request.getParameter("id");
     // qualcosa?
    if (strId == null) {
       // non è normale
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // si trasforma strId in un numero intero
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // non è normale
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // si rimuove l'acquisto
    panier.enlever(id);
     // si visualizza nuovamente il carrello
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");
  }
}

Commenti:

  • il codice verifica la presenza e la validità del parametro [id]. Se risulta errato, viene inviata la vista [ERREURS].
  • Altrimenti, l’acquisto viene rimosso dal carrello:
     // si rimuove l'acquisto
    panier.enlever(id);
  • quindi il carrello viene visualizzato nuovamente:
     // si visualizza nuovamente il carrello
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");

Ricordiamo il codice della vista [/vues/redirpanier.jsp]:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/panier.do"/>

Si nota che il cliente verrà reindirizzato all’azione [/panier.do]. Quest’ultima è già stata descritta. Visualizzerà la vista [PANIER] o [PANIERVIDE] a seconda dello stato del carrello.

3.7.7.7. validerpanier.do

Questa azione serve a confermare gli acquisti effettuati dal cliente. In pratica, ciò si traduce in un’unica operazione: le scorte degli articoli acquistati vengono ridotte delle quantità acquistate nel database. Questa azione proviene dal seguente menu:

Image

Il codice HTML del link [Valider le panier] è il seguente:

<a href="validerpanier.do">Valider le panier</a>

Quando questo collegamento viene attivato, le scorte vengono ridotte e l’elenco degli articoli viene nuovamente visualizzato.

Questa azione è configurata come segue in [springwebarticles-servlet.xml]:

     <!-- mappatura dell'applicazione-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>
...
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Il codice della classe [ValiderPanierController] è il seguente:

package istia.st.articles.web.spring;

import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class ValiderPanierController implements Controller {

   // configurazione dell'applicazione web
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // elaborazione della richiesta
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // l'elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
     // l'acquirente ha confermato il carrello
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sessione scaduta
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // conferma del carrello
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // non normale
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // si recuperano eventuali errori
    erreurs = config.getArticlesDomain().getErreurs();
    if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe(), config.getHActionPanier() });
      return new ModelAndView("erreurs");
    }
     // tutto sembra a posto OK - si visualizza l'elenco degli articoli
    return new ModelAndView("index");
  }
}

Commenti:

  • si recupera il carrello dalla sessione. Se questa è scaduta, si invia la vista [ERREURS]:
     // elenco degli errori relativi a questa azione
    ArrayList erreurs = new ArrayList();
     // l'acquirente ha confermato il carrello
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sessione scaduta
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • Si confermano gli acquisti nel carrello. Possono verificarsi errori se le scorte sono insufficienti a soddisfare gli acquisti. In questo caso, si invia la vista [ERREURS]:
     // conferma del carrello
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // anomalia
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // si recuperano eventuali errori
    erreurs = config.getArticlesDomain().getErreurs();
    if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe(), config.getHActionPanier() });
      return new ModelAndView("erreurs");
    }
  • se tutto è andato a buon fine, viene nuovamente visualizzato l’elenco degli articoli:
     // tutto sembra a posto OK - si visualizza l'elenco degli articoli
    return new ModelAndView("index");

Sappiamo che la vista [/vues/index.jsp] reindirizza il cliente all’azione [/main.do]. Questa azione visualizzerà la vista [LISTE].