Skip to content

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

Obiettivi di questo articolo:

  • Architetture a tre livelli
  • Architettura web MVC di base
  • Architettura MVC Struts
  • Architettura MVC Spring

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/: DBMS, driver JDBC. In realtà, qualsiasi sorgente JDBC andrà bene.
  • IBExpert, Personal Edition: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (marzo 2005). IBExpert consente di amministrare graficamente il DBMS 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 questi si trovano nei documenti che ho scritto. In tali casi, li cito. Ovviamente, questo è solo un suggerimento e i lettori sono liberi di utilizzare le risorse che preferiscono.

  • Linguaggio Java: [https://tahe.developpez.com/java/cours]
  • Programmazione web in Java: [https://tahe.developpez.com/java/web/]
  • Programmazione web con Java, Eclipse e 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 di 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 che ho letto nell'estate del 2004, un'opera magnifica di Rod Johnson: J2EE Development without EJB, pubblicato da Wrox.


3.1. L'applicazione webarticles

Qui vorremmo presentare alcuni componenti di un'applicazione web di e-commerce. Questa applicazione consentirà ai client web

  • di visualizzare un elenco di articoli provenienti da un database
  • aggiungerne alcuni a un carrello elettronico
  • e confermare il carrello. Questa conferma aggiornerà semplicemente i livelli di inventario degli articoli acquistati nel database.

Le diverse schermate presentate all'utente saranno le seguenti:

  • la vista [LIST], che mostra un elenco di articoli in vendita

Image

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

Image

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

Image

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

Image

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

Image

3.2. Architettura generale dell'applicazione

Vogliamo realizzare un'applicazione con la seguente architettura a tre livelli:

  • I tre livelli sono resi indipendenti grazie all'uso di interfacce Java
  • L'integrazione dei diversi livelli è gestita da Spring
  • Ogni livello è contenuto in pacchetti separati: web (livello dell'interfaccia utente), dominio (livello di business) e DAO (livello di accesso ai dati).

Partiamo dal presupposto che i livelli [domain] e [DAO] siano già presenti. Ci concentreremo esclusivamente sul livello [web], che proponiamo di realizzare in diversi modi:

  • utilizzando la classica tecnologia dei controller servlet — pagine JSP
  • utilizzando la tecnologia Struts MVC
  • utilizzando la tecnologia Spring MVC

In tutti i casi, l'applicazione seguirà un'architettura MVC (Model-View-Controller). Se ci riferiamo al diagramma a livelli sopra riportato, l'architettura MVC si inserisce in esso come segue:

L'elaborazione di una richiesta del client segue questi passaggi:

  1. Il client invia una richiesta al controller. Questo controller è un servlet che gestisce tutte le richieste dei client. È il punto di ingresso dell'applicazione. È la C in MVC.
  2. Il controller elabora questa richiesta. Per farlo, potrebbe aver bisogno dell’assistenza del livello business, noto come modello M nella struttura MVC.
  3. Il controller riceve una risposta dal livello business. La richiesta del client è stata elaborata. Ciò può innescare diverse possibili risposte. Un classico esempio è
    • 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 client. Si tratta molto spesso di una pagina contenente elementi dinamici. Il controller fornisce questi alla vista.
  5. La vista viene inviata al client. Questa è la V in MVC.

3.3. Il Modello

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

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

3.3.1. Il database

Il database contiene una sola tabella denominata ARTICLES. Questa tabella è stata generata utilizzando 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
);


/* constraints */
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<>'');

/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
chiave primaria che identifica in modo univoco un articolo
nome
nome dell'elemento
prezzo
il suo prezzo
disponibilità attuale
scorte attuali
scorte minime
il livello di scorte al di sotto del quale è necessario effettuare un riordino

Nei test seguenti è stato utilizzato un database [Firebird]. [Firebird] è un DBMS "open source". Il driver JDBC [firebirdsql-full.jar] si trova nella cartella [WEB-INF/lib] dell'applicazione web.

3.3.2. I pacchetti modello

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

  • istia.st.articles.dao: contiene le classi di accesso ai dati per il livello [DAO]
  • istia.st.articles.exception: contiene una classe di eccezioni per la gestione di questo articolo
  • istia.st.articles.domain: contiene le classi di business del livello [domain]
archivio
contenuto
ruolo
istia.st.articles.dao
- contiene il pacchetto [istia.st.articles.dao], che a sua volta contiene i seguenti elementi:
- [IArticlesDao]: l'interfaccia per accedere al livello Dao. Questa è l'unica interfaccia visibile al livello [domain]. Non ne vede altre.
- [Article]: classe che definisce un articolo
- [ArticlesDaoSqlMap]: classe di implementazione per l'interfaccia [IArticlesDao] che utilizza lo strumento SqlMap
livello di accesso ai dati – si trova interamente all'interno del livello [dao] dell'architettura a 3 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 per l'accesso al livello [domain]. Questa è l'unica interfaccia visibile al livello web. Non ne vede altre.
- [ArticlePurchases]: una classe che implementa [IArticlesDomain]
- [Purchase]: una classe che rappresenta l'acquisto di un cliente
- [ShoppingCart]: una classe che rappresenta il totale degli acquisti di un cliente
rappresenta il modello di acquisto web - si trova interamente all'interno del livello [domain] dell'architettura a 3 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 [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) {
     // init instance attributes
    setId(id);
    setNom(nom);
    setPrix(prix);
    setStockActuel(stockActuel);
    setStockMinimum(stockMinimum);
  }

     // getters - setters
  public int getId() {
    return id;
  }

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

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
     // valid name?
    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) {
     // valid price?
    if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
    this.prix = prix;
  }

  public int getStockActuel() {
    return stockActuel;
  }

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

  public int getStockMinimum() {
    return stockMinimum;
  }

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

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

Questa classe fornisce:

  1. un costruttore per impostare le 5 informazioni relative a un elemento
  2. accessori, spesso chiamati getter/setter, utilizzati per leggere e scrivere le 5 informazioni. I nomi di questi metodi seguono lo standard JavaBean. L'uso di oggetti JavaBean nel livello DAO per interfacciarsi con i dati del DBMS è una pratica standard.
  3. la convalida dei dati inseriti per l'elemento. Se i dati non sono validi, viene generata un'eccezione.
  4. Un metodo toString che restituisce il valore di un elemento come stringa. Questo è 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);
}

I ruoli dei vari metodi nell'interfaccia sono i seguenti:

getAllArticles
restituisce tutti gli elementi della tabella ARTICLES in un elenco di oggetti [Article]
clearAllArticles
cancella la tabella ARTICLES
getArticleById
restituisce l'oggetto [Article] identificato dalla sua chiave primaria
addArticle
consente di aggiungere un articolo alla tabella ARTICLES
modifyArticle
consente di modificare una voce nella tabella [ARTICLES]
deleteItem
consente di eliminare un articolo dalla tabella [ARTICLES]
aggiornaStockArticolo
consente di modificare la disponibilità di un articolo nella tabella [ARTICLES]

L'interfaccia fornisce ai programmi client una serie di metodi definiti esclusivamente dalle loro firme. Non si occupa di come questi metodi saranno effettivamente implementati. Ciò garantisce flessibilità all'applicazione. Il programma client effettua chiamate a un'interfaccia piuttosto che a una specifica implementazione di tale interfaccia.

La scelta di una specifica implementazione avverrà tramite un file di configurazione Spring. In questo caso, proponiamo di implementare l'interfaccia IArticlesDao utilizzando un prodotto open source chiamato SqlMap. Ciò ci consentirà di eliminare tutte le istruzioni SQL dal codice Java.

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

package istia.st.articles.dao;

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

public class ArticlesDaoSqlMap implements IArticlesDao {

  // Fields
  private SqlMapClient sqlMap;

   // Constructors
  public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }

   // Methods
  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 simultaneo alla fonte dei dati. In qualsiasi momento, solo un thread ha accesso a un determinato metodo.

La classe [ArticlesDaoSqlMap] utilizza lo strumento [Ibatis SqlMap]. Il vantaggio di questo strumento è che consente di separare il codice SQL per l'accesso ai dati dal codice Java. Viene quindi inserito in un file di configurazione. Ne riparleremo più avanti. Per essere istanziata, la classe [ArticlesDaoSqlMap] richiede 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 DBMS contenente gli articoli
  • gestire un pool di connessioni
  • gestire le transazioni

Nel nostro esempio, il file 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 definisce come costruire un'istanza della classe [istia.st.articles.dao.Article] a partire da una riga della tabella [ARTICLES] del DBMS. Definisce inoltre le query SQL che consentiranno al livello [dao] di recuperare i dati dall'origine 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">

     <!-- an alias to the istia.st.articles.dao.Article class -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- mapping ORM :  row table ARTICLES - instance class Article -->
  <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>

     <!-- query SQL to obtain all items -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- query SQL to delete all items -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- the SQL query to insert an article -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>

     <!-- the SQL query to delete a given item -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- query SQL to modify a given item -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nom#,
    prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
    id=#id#
</statement>

     <!-- query SQL to obtain a given item -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- query SQL to modify the stock of a given item -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#mouvement#
    where id=#id# and stockActuel+#mouvement#&gt;=0
</statement>
</sqlMap>

Il codice del pacchetto [dao] è disponibile in appendice.

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

L'interfaccia [IArticlesDomain] disaccoppia il livello [business] dal livello [web]. Quest'ultimo accede al livello [business/domain] tramite questa interfaccia senza preoccuparsi della classe che la implementa effettivamente. L'interfaccia definisce le seguenti azioni per accedere al livello business:

 package istia.st.articles.domain;

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

public abstract interface IArticlesDomain {

   // Methods
  void acheter(Panier panier);
  List getAllArticles();
  Article getArticleById(int idArticle);
  ArrayList getErreurs();
}
List getAllArticles()
restituisce l'elenco degli oggetti [Article] da visualizzare al client
Article getArticleById(int idArticle)
restituisce l'oggetto [Article] identificato da [idArticle]
void acquista(Carrello carrello)
elabora il carrello del cliente decrementando le scorte degli articoli acquistati della quantità acquistata — potrebbe fallire se le scorte sono insufficienti
ArrayList getErrors()
restituisce l'elenco degli errori verificatisi; è vuoto se non ci sono errori

Qui, l'interfaccia [IArticlesDomain] sarà implementata dalla seguente classe [PurchaseItems]:

package istia.st.articles.domain;

// Imports
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 {

   // Fields
  private IArticlesDao articlesDao;
  private ArrayList erreurs;

   // Manufacturers
  public AchatsArticles(IArticlesDao articlesDao) { }

   // Methods
  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 errors
l'elenco di eventuali errori

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

public PurchasesItems(IArticlesDao articlesDao)
costruttore

La classe [Purchase] rappresenta un acquisto effettuato da un cliente:

package istia.st.articles.domain;

public class Achat {

   // Fields
  private Article article;
  private int qte;

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

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

La classe [Purchase] è un JavaBean con i seguenti campi e metodi:

item
l'articolo acquistato
quantità
la quantità acquistata
doppio getTotal()
restituisce l'importo dell'acquisto
String toString()
rappresentazione stringa dell'oggetto

La classe [Cart] rappresenta il totale degli acquisti del cliente:

package istia.st.articles.domain;

// Imports
import java.util.ArrayList;

public class Panier {

  // Fields
  private ArrayList achats;

   // Manufacturers
  public Panier() { }

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

La classe [Cart] è un JavaBean con i seguenti campi e metodi:

acquisti
l'elenco degli acquisti del cliente - un elenco di oggetti di tipo [Purchase]
void add(Purchase purchase)
aggiunge un acquisto all'elenco degli acquisti
void rimuovi(int idArticolo)
rimuove l'acquisto relativo all'ID articolo idArticle
double getTotal()
restituisce l'importo totale degli acquisti
String toString()
restituisce la rappresentazione stringa del carrello
ArrayList getPurchases()
restituisce l'elenco degli acquisti

Il codice per il pacchetto [domain] è disponibile in appendice.

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

Questo pacchetto contiene la classe che definisce l'eccezione generata dal livello [dao] quando incontra un problema nell'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] troverete:
    • gli archivi richiesti dallo strumento [ibatis SqlMap] responsabile dell'accesso al DBMS Firebird: ibatis-*.jar
    • il file richiesto dallo strumento [Spring]: spring.jar
    • il driver JDBC per il DBMS [Firebird]: firebirdsql-full.jar
    • gli archivi necessari per la registrazione: log4-*.jar, commons-logging.jar
    • i tre archivi per il modello testato: istia.st.articles.*.jar
    • l'archivio richiesto per lo strumento di test [junit]
  • In [WEB-INF/src] si trovano i file di configurazione che verranno copiati automaticamente in [WEB-INF/classes] da Eclipse:
    • i file di configurazione per lo strumento [sqlmap]: sqlmap-config-firebird.xml, articles.xml
    • i file di configurazione dello strumento [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
    • il file di configurazione per lo strumento [log4j]: log4j.properties
  • Nel pacchetto [istia.st.articles.tests] troverete le classi di test del modello

3.3.6.1. Test per il livello [dao]

La classe di test JUnit per il livello [dao] è la seguente. Leggerla aiuta a 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 the ArticlesDaoSqlMap class
public class JunitModeleDaoArticles extends TestCase {

     // an instance of the class under test
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // retrieves a data access instance
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }

    public void testGetAllArticles() {
         // displays articles
        listArticles();
    }

    public void testClearAllArticles() {
         // empties item table
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
    }

    public void testAjouteArticle() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         //the poster
        listArticles();
    }

    public void testSupprimeArticle() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // delete
        articlesDao.supprimeArticle(4);
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(1, articles.size());
         // displays the table
        listArticles();
    }

    public void testModifieArticle() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        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");
         // modification
        articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
         // getById
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getPrix(), 44, 1e-6);
         // displays the table
        listArticles();
    }

    public void testGetArticleById() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        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() {
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
         // display read articles
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

    public void testChangerStockArticle() throws InterruptedException {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // insertion
        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);
         // creation of 100 threads to update the stock of item 3
        Thread[] taches = new Thread[100];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
            taches[i].start();
        }
         // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // retrieve item 3 and check stock
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        assertEquals(1, unArticle.getStockActuel());
         // modification stock article 4
        boolean erreur = false;
        int nbLignes = articlesDao.changerStockArticle(4, -100);
        assertEquals(0, nbLignes);
         // displays the table
        listArticles();
    }
}

Commenti:

  • La classe di test memorizza, utilizzando il suo metodo setUp, un'istanza della classe sottoposta a test:
1
2
3
4
5
6
7
8
    // une instance de la classe testée
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }
  • L'oggetto da testare è fornito da [Spring]. Sopra, richiediamo il bean Spring denominato [articlesDao]. Questo bean è definito nel file di configurazione 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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
</beans>

Come mostrato sopra, il bean [articlesDao] è un'istanza della classe [istia.st.articles.dao.ArticlesDaoSqlMap]. Questa classe ha un costruttore che accetta come parametro il nome del file di configurazione dello strumento [SqlMap]. Tale nome è fornito qui. Si tratta di [sqlmap-config-firebird.xml]. Questo file è già stato descritto. Fornisce tutte le informazioni necessarie per accedere ai dati del DBMS.

Il metodo [testChangerStockArticle] crea 100 thread responsabili di decrementare lo stock di un determinato articolo. Lo scopo qui è testare l'accesso concorrente al DBMS. Poiché il metodo [changerStockArticle] della classe [istia.st.articles.dao.ArticlesDaoSqlMap] è stato sincronizzato, questo test viene superato. Se rimuoviamo la sincronizzazione, il test non viene più superato. La classe responsabile dell'aggiornamento delle 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() {
         // follow-up
        System.out.println(name + " lancé");
         // modification stock article 3
        articlesDao.changerStockArticle(3, -1);
         // follow-up
        System.out.println(name + " terminé");
    }
}
  • La classe sopra riportata decrementa di 1 lo stock dell'articolo n. 3

3.3.6.2. [dominio] test di livello

La classe di test JUnit per il livello [dominio] è 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 the ArticlesDaoSqlMap class
public class JunitModeleDomainArticles extends TestCase {

     // an instance of the domain access class
    private IArticlesDomain articlesDomain;

     // an instance of the data access class
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // retrieves a domain access instance
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // retrieves a data access instance
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }

     // retrieve a specific item
    public void testGetArticleById() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        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");
    }

     // screen display
    private void listArticles() {
         // reads the ARTICLES table
        List articles = articlesDomain.getAllArticles();
         // display read articles
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

     // article purchases
    public void testAchatPanier(){
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // reads the ARTICLES table
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // create a basket with two purchases
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // checks
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // shopping cart validation
        articlesDomain.acheter(panier);
         // checks
        assertEquals(0,articlesDomain.getErreurs().size());
        assertEquals(0,panier.getAchats().size());
         // search article n° 3
        article3=articlesDomain.getArticleById(3);
        assertEquals(20,article3.getStockActuel());
         // search article n° 4
        article4=articlesDomain.getArticleById(4);
        assertEquals(30,article4.getStockActuel());
         // new basket
        panier.ajouter(new Achat(article3,100));
         // shopping cart validation
        articlesDomain.acheter(panier);
         // checks - we bought too much
         // we must have an error
        assertEquals(1,articlesDomain.getErreurs().size());
         // search article n° 3
        article3=articlesDomain.getArticleById(3);
         // its stock must not have changed
        assertEquals(20,article3.getStockActuel());    
    }

     // withdraw purchases
    public void testRetirerAchats(){
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // reads the ARTICLES table
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // create a basket with two purchases
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // checks
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // add a previously purchased item
        panier.ajouter(new Achat(article3,10));
         // checks
         // the total must be increased to 1000
        assertEquals(1000.0,panier.getTotal(),1e-6);
         // always 2 items in the basket
        assertEquals(2,panier.getAchats().size());
         // qty item 3 increased to 20
        Achat achat=(Achat)panier.getAchats().get(0);
        assertEquals(20,achat.getQte());
         // article 3 is removed from the basket
        panier.enlever(3);
         // checks
         // the total must be increased to 400
        assertEquals(400.0,panier.getTotal(),1e-6);
         // 1 item only in basket
        assertEquals(1,panier.getAchats().size());
         // this must be article no. 4
        assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
    }
}

Commenti:

  • La classe di test utilizza il proprio metodo setUp per memorizzare un'istanza della classe sottoposta a test 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], di cui non dovrebbe nemmeno essere a conoscenza. In questo caso, abbiamo ignorato questa "etica", che, se seguita, ci avrebbe richiesto di creare nuovi metodi nella nostra interfaccia [IArticlesDomain].
    // une instance de la classe d'accès au domaine
    private IArticlesDomain articlesDomain;

    // une instance de la classe d'accès aux données
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
        // récupère une instance d'accès au domaine
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }
  • L'oggetto da testare è fornito da [Spring]. Sopra, richiediamo il bean Spring denominato [articlesDomain]. Questo bean è definito nel file di configurazione 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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Come mostrato sopra, il bean [articlesDomain] è un'istanza della classe [istia.st.articles.domain.AchatsArticles]. Questa classe ha un costruttore che richiede, come parametro, un oggetto che fornisce l'accesso al livello [dao] di tipo [IArticlesDao]. Qui, il file di configurazione specifica che questo oggetto è il bean denominato [articlesDao]. Ciò costringe Spring a istanziare questo bean. L'istanziazione del bean [articlesDao] è stata spiegata in precedenza. Quindi, in definitiva, sono stati istanziati due bean:

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

Queste due istanze vengono attivate dalla prima chiamata a Spring:

1
2
3
4
        // récupère une instance d'accès au domaine
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");

Viene quindi recuperato il bean [articlesDomain]. Durante la seconda chiamata a Spring:

1
2
3
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");

[Spring] restituisce semplicemente un riferimento al bean [articlesDao] che era già stato creato durante la chiamata precedente. Questo è il principio del singleton. Se si richiede un bean a Spring, esso lo istanzia se non esiste già; altrimenti, restituisce un riferimento al bean esistente.

3.4. Applicazione web MVC a tre livelli

Successivamente, vogliamo 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.

Per prima cosa, consideriamo un approccio classico, in cui:

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

3.5. Architettura MVC basata su un servlet controller e pagine JSP

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

M = Modello
Classi di business, classi di accesso ai dati e il database
V = Viste
Pagine JSP
C = Controller
il servlet che elabora le richieste dei client

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 di questo documento:

LIST
list.jsp
Le viste si trovano nella cartella [vues] dell'applicazione
INFO
info.jsp
CARRELLO
carrello.jsp
CARRELLO VUOTO
empty-cart.jsp
ERRORI
errori.jsp

3.5.3. Il controller

Il controller sarà costituito da un unico servlet denominato [WebArticles]. Si occuperà di gestire varie richieste dei client. Queste richieste saranno identificate dalla presenza di un parametro [action] nella richiesta HTTP del client:

richiesta
significato
azione del controller
risposte possibili
azione=elenco
il client desidera l'elenco di
elementi
- richiede l'elenco degli articoli al
azienda
- [LIST]
- [ERRORI]
azione=info
Il client richiede
Informazioni su uno degli elementi visualizzati nella vista
[LIST]
- richiede l'elemento dal livello business
- [INFO]
- [ERRORI]
azione=acquisto
Il cliente acquista un articolo
- richiede l'articolo al livello aziendale e
lo aggiunge al carrello del cliente
- [INFO] in caso di errore nella quantità
- [LIST] se non ci sono errori
azione=rimuovi acquisto
il cliente desidera rimuovere un
acquisto dal proprio carrello
- recupera il carrello dalla sessione e modificalo
- [CARRELLO]
- [SVUOTA CARRELLO]
- [ERRORI]
azione=carrello
il cliente desidera visualizzare il proprio
carrello
- recupera il carrello dalla sessione
- [CARRELLO]
- [CARRELLO VUOTO]
- [ERRORI]
azione=convalida-carrello
Il cliente ha terminato gli acquisti
e procede al checkout
- aggiorna il database con i livelli di stock degli
articoli acquistati
- rimuove dal carrello del cliente gli articoli per i quali
sono stati confermati
- [LISTA]
- [ERRORI]

3.5.4. Configurazione dell'applicazione

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

  1. modifiche agli URL delle varie viste
  2. modifiche alle classi che implementano le interfacce [IArticlesDao] e [IArticlesDomain]
  3. modifiche al DBMS, al database o alla tabella degli articoli

3.5.5. Modifiche agli URL

I nomi degli URL delle viste saranno inseriti nel file di configurazione [web.xml] dell'applicazione 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>

In [web.xml]

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

3.5.6. Modifica delle classi che implementano le interfacce

In linea con i principi delle 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, non classi concrete
  • Il codice di un livello non istanzia mai direttamente la classe di un altro livello per utilizzarla. Si limita a richiedere un'istanza dell'implementazione dell'interfaccia da uno strumento esterno — in questo caso, [Spring] — per il livello che desidera utilizzare. Per farlo, sappiamo che non è necessario conoscere il nome della classe di implementazione, ma solo il nome del bean Spring di cui si desidera un riferimento.

Nella nostra applicazione, il file di configurazione di Spring potrebbe apparire così:

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

Per accedere al livello [business], una classe nel livello [Interfaccia utente, UI] può richiedere il bean [articlesDomain]. Spring istanzierà quindi un oggetto di tipo [istia.st.articles.domain.AchatsArticles]. Per questa istanziazione, richiede un bean di tipo [articlesDao], ovvero un oggetto di tipo [istia.st.articles.dao.ArticlesDaoSqlMap]. Spring istanzierà quindi tale oggetto. Questa istanziazione si baserà sulle informazioni contenute nel file [sqlmap-config-firebird.xml], il 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 DBMS:

3.5.7. Modifiche relative al DBMS o al database

L'indipendenza dell'applicazione web dalle modifiche relative al DBMS o al database è garantita in questo caso dai file di configurazione di 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 utilizzare un DBMS diverso.

  1. Il file [articles.xml], che contiene le varie istruzioni SQL richieste dall'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">

     <!-- an alias to the istia.st.articles.dao.Article class -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- mapping ORM :  row table ARTICLES - instance class Article -->
  <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>

     <!-- query SQL to obtain all items -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- query SQL to delete all items -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- the SQL query to insert an article -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>

     <!-- the SQL query to delete a given item -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- query SQL to modify a given item -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nom#,
    prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
    id=#id#
</statement>

     <!-- the SQL query to obtain a given item -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- query SQL to modify the stock of a given item -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#mouvement#
    where id=#id# and stockActuel+#mouvement#&gt;=0
</statement>
</sqlMap>

Se i nomi della tabella dei prodotti o delle colonne dovessero cambiare, dovremmo riscrivere le query in questo file di configurazione senza dover modificare il codice Java. Ciò varrebbe anche nel caso in cui una query dovesse essere sostituita da una stored procedure per motivi di prestazioni.

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

Un'applicazione web Java è un puzzle composto da molti pezzi. Dotarla di un'architettura MVC generalmente aumenta il numero di questi pezzi. La struttura dell'applicazione [webarticles] in [Eclipse] è la seguente:

struttura generale - di seguito sono riportati gli
 archivi Java utilizzati da
 Eclipse.
spring: per Spring
ibatis: per SqlMap
log4j, commons-logging: per
 da Spring e SqlMap
firebird: per il DBMS Firebird
mysql: per il DBMS MySQL
jstl, standard: per il
 Libreria di tag JSTL
la cartella dei sorgenti Java: contiene il codice Java
 e i file di configurazione
 Eclipse copia automaticamente
 questi file
in [WEB-INF/classes].
È qui che l'applicazione li troverà.
la cartella [WEB-INF] dell'applicazione: contiene il
 descrittore [web.xml] dell'applicazione e i
 file di definizione della libreria JSTL
le viste

3.5.9. Viste JSP

Le viste JSP utilizzano la libreria di tag JSTL.

3.5.9.1. header.jsp

Per garantire la coerenza tra le diverse viste, queste condivideranno la stessa intestazione, che mostra il nome dell'applicazione insieme al menu:

Il menu è dinamico e viene impostato dal controller. Il controller include un attributo chiave "actions" nella richiesta inviata alla pagina JSP, con un valore associato costituito da un array Hastable[]. Ogni elemento di questo array è un dizionario destinato a generare un'opzione di menu nell'intestazione. Ogni dizionario ha due chiavi:

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

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

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

In fase di esecuzione, questo tag includerà il codice della pagina [entete.jsp] nella pagina JSP che lo contiene. Poiché l'URL della pagina è un URL relativo (senza / finale), la pagina [entete.jsp] verrà cercata nella stessa directory della pagina contenente 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. list.jsp

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

Viene visualizzata in seguito a una richiesta a /main?action=list o /main?action=cartvalidation. I parametri della richiesta al controller sono i seguenti:

azioni
Oggetto Hashtable[] - l'array delle opzioni di menu
listarticles
ArrayList di oggetti di tipo [Item]
messaggio
Oggetto String - messaggio da visualizzare in fondo alla pagina

Ogni link [Info] nella tabella HTML degli articoli ha un URL nel formato [?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 l'acquisto:

Image

Viene visualizzata in seguito a una richiesta /main?action=infos&id=ID o a una richiesta /main?action=achat&id=ID quando la quantità acquistata non è corretta. I parametri di richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable[] - l'array delle opzioni di menu
item
oggetto di tipo [Article] - elemento da visualizzare
msg
Oggetto stringa - messaggio da visualizzare in caso di errore con la quantità
qte
Oggetto stringa - valore da visualizzare nel campo di immissione [Qty]

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

Image

Questa pagina contiene un modulo che viene inviato tramite il pulsante [Acquista]. L'URL di destinazione POST è [?action=purchase&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. cart.jsp

Questa vista mostra il contenuto del carrello:

Image

Viene visualizzata in seguito a una richiesta a /main?action=cart o /main?action=remove&id=ID. I parametri di richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable[] - l'array delle opzioni di menu
cart
oggetto di tipo [Cart] - il carrello da visualizzare

Ogni link [Rimuovi] nella tabella HTML del carrello ha un URL nella forma [?action=removeitem&id=ID], dove ID è il campo [id] dell'articolo da 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. emptycart.jsp

Questa vista mostra le informazioni che indicano che il carrello è vuoto:

Image

Viene visualizzata in seguito a una richiesta a /main?action=cart o /main?action=remove&id=ID. I parametri di richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable[] - l'array delle opzioni di 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. errors.jsp

Questa vista 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, che viene gestita dalla vista [INFOS]. Gli elementi della richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable[] - l'array delle opzioni di menu
errori
ArrayList di 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] reindirizza semplicemente 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

Dobbiamo ancora scrivere il cuore della nostra applicazione web, il controller. Il suo ruolo è quello di:

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

3.5.10.1. Inizializzazione del controller

Quando la classe del controller viene caricata dal server servlet, viene eseguito il suo metodo [init]. Ciò avverrà una sola volta. Una volta caricato in memoria, il controller rimarrà lì ed elaborerà le richieste provenienti da vari client. Ogni client viene gestito da un thread di esecuzione separato, quindi i metodi del controller vengono eseguiti simultaneamente da thread diversi. Si noti che, per questo motivo, il controller non deve avere alcun campo che i suoi metodi possano modificare. I suoi campi devono essere di sola lettura. Essi vengono inizializzati dal metodo [init], che è la sua funzione principale. Questo metodo ha la caratteristica unica di essere eseguito una sola volta da un singolo thread. Pertanto, non ci sono problemi di accesso concorrente ai campi del controller all'interno di questo metodo. Lo scopo del metodo [init] è inizializzare gli oggetti richiesti dall'applicazione web, che saranno condivisi in modalità di 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à il file [web.xml] alla ricerca dei parametri necessari per il corretto funzionamento dell'applicazione. Questi sono stati descritti nella sezione 3.5.5.
  • inizializzerà un campo privato [ArrayList errors] contenente un elenco di eventuali errori. Questo elenco sarà vuoto se non ci sono errori, ma esisterà comunque.
  • Se si sono verificati errori, il metodo [init] si ferma lì. Altrimenti, crea un oggetto di tipo [IArticlesDomain], che sarà l'oggetto di business che il controller utilizza per le proprie esigenze. Come spiegato nella sezione 3.5.6, il controller richiederà il bean di cui ha bisogno dal framework Spring. Questa operazione di istanziazione può causare vari errori. In tal caso, questi saranno, ancora una volta, memorizzati nel campo [errors] del controller.

3.5.10.2. Metodi doGet, doPost

Questi due metodi gestiscono le richieste HTTP GET e POST provenienti dai client. Saranno gestiti in modo intercambiabile. Il metodo [doPost] potrà quindi reindirizzare al metodo [doGet] o viceversa. La richiesta del client verrà elaborata come segue:

  • Verrà controllato il campo [errors]. Se non è vuoto, significa che si sono verificati degli errori durante l'inizializzazione dell'applicazione e che questa non può essere eseguita. Verrà quindi inviata la vista [ERRORS] come risposta.
  • Il parametro [action] della richiesta verrà recuperato e controllato. Se non corrisponde a un'azione conosciuta, la vista [ERRORS] viene inviata con un messaggio di errore appropriato.
  • Se il parametro [action] è valido, la richiesta del client viene passata a una procedura specifica per l'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. Gestione delle diverse azioni

I metodi che gestiscono le varie azioni possibili dell'applicazione sono i seguenti:

metodo
richiesta
elaborazione
possibili risposte
doList
GET /main?action=list
- richiedi l'elenco degli elementi
dalla classe di business
- visualizzarlo
[LIST] o [ERRORS]
doInfo
GET /main?action=info&id=ID
- recupera l'elemento con id=ID dalla
classe di business
- visualizzarlo
[INFO] o [ERRORS]
doPurchase
POST /main?action=purchase&id=ID
- La quantità acquistata è inclusa nei parametri inviati
- richiedi l'articolo con id=ID da
classe business
- Aggiungilo al carrello nella
sessione del cliente
[LIST] o [INFO]
o [ERRORI]
doRemovePurchase
GET /main?action=removePurchase&id=ID
- rimuove l'articolo con id=ID dalla
lista della spesa nella
sessione del cliente
[CART]
doCart
GET /main?action=cart
- Visualizza la
sessione del cliente
[CART] o [EMPTY_CART]
doCartValidation
GET /main?action=cartvalidation
- Ridurre le
livelli di stock di tutti gli articoli
nel
[LIST] o [ERRORS]
[LIST] o [ERRORS]

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 {

     // private fields
    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() {
         // retrieve servlet initialization parameters
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // we memorize the error
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // mistakes?
        if (erreurs.size() != 0) {
            return;
        }
         // create a IArticlesDomain business layer access object
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // we memorize the error
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // memorize certain application urls
        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);

         // it's over
        return;
    }


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

         // check how the initialization of the servelet went
        if (erreurs.size() != 0) {
             // do we have the url of the error page?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // action is processed
        String action = request.getParameter("action");
        if (action == null) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // article info
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // purchase an item
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // basket display
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // remove an item from the basket
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // shopping cart validation
            doValidationPanier(request, response);
            return;
        }
         // unknown share
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // the error page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // end
        return;
    }


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

         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // validate this basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // error recovery
        ArrayList erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
            request.setAttribute(
                "actions",
                new Hashtable[] { hActionListe, hActionPanier });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // displays the list of items
        request.setAttribute("message", "Votre panier a été validé");
        doListe(request, response);
         // end
        return;
    }


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

         // remove a purchase from the basket
        try {
            Panier panier =
                (Panier) request.getSession().getAttribute("panier");
            String strIdAchat = request.getParameter("id");
            panier.enlever(Integer.parseInt(strIdAchat));
        } catch (NumberFormatException ignored) {
        } catch (NullPointerException ignored) {
        }
         // the basket is displayed
        doPanier(request, response);
    }


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

         // the basket is displayed
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // empty basket?
        if (panier == null || panier.getAchats().size() == 0) {
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
                .forward(request, response);
             // end
            return;
        }
         // there's something in the basket
        request.setAttribute("panier", panier);
        request.setAttribute(
            "actions",
            new Hashtable[] { hActionListe, hActionValidationPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_PANIER))
            .forward(request, response);
         // end
        return;
    }


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

         // purchase an item
         // we recover the quantity
        int qté = 0;
        try {
            qté = Integer.parseInt(request.getParameter("qte"));
            if (qté <= 0)
                throw new NumberFormatException();
        } catch (NumberFormatException ex) {
             // wrong qty
            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);
             // end
            return;
        }
         // retrieve the client session
        HttpSession session = request.getSession();
         // we create the purchase
        Article article = (Article) session.getAttribute("article");
        Achat achat = new Achat(article, qté);
         // the purchase is added to the customer's basket
        Panier panier = (Panier) session.getAttribute("panier");
        if (panier == null) {
            panier = new Panier();
            session.setAttribute("panier", panier);
        }
        panier.ajouter(achat);
         // we return to the list of items
        String url = config.getInitParameter(URL_MAIN) + "?action=liste";
        getServletContext().getRequestDispatcher(url).forward(
            request,
            response);
         // end
        return;
    }


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

         // displays the list of items
        request.setAttribute("infos", infos);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_DEBUG))
            .forward(request, response);
         // end
        return;
    }


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

         // idem get
        doGet(request, response);
    }


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

         // error list
        ArrayList erreurs = new ArrayList();
         // retrieve the requested id
        String strId = request.getParameter("id");
         // anything?
        if (strId == null) {
             // not normal
            erreurs.add("action incorrecte([infos,id=null]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // transform strId into an integer
        int id = 0;
        try {
            id = Integer.parseInt(strId);
        } catch (Exception ex) {
             // not normal
            erreurs.add("action incorrecte([infos,id=" + strId + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // the key item id is requested
        Article article = null;
        try {
            article=articlesDomain.getArticleById(id);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            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) {
             // not normal
            erreurs.add("Article de clé [" + id + "] inexistant");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // put the article in the session
        request.getSession().setAttribute("article", article);
         // the info page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        //    request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_INFOS))
            .forward(request, response);
         // end
        return;
    }

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

         // the error page is displayed
        request.setAttribute("erreurs", erreurs);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
            .forward(request, response);
         // end
        return;
    }


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

         // list of errors
        ArrayList erreurs = new ArrayList();
         // the list of items is requested
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // end
        return;
    }

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

Lasceremo al lettore il tempo necessario per leggere e comprendere questo codice. Speriamo che i commenti siano d'aiuto.

3.5.10.5. Test dell'applicazione

Diamo un'occhiata ad alcuni screenshot dei test. Innanzitutto, la home page dell'applicazione:

Image

L'URL richiesto era in realtà [http://localhost:8080/webarticles]. Il lettore noterà che nel file [web.xml] definiamo una home page 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 mostrato dall'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>

Inoltre, nel file [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>

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

    public void init() {
         // retrieve servlet initialization parameters
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // we memorize the error
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // mistakes?
        if (erreurs.size() != 0) {
            return;
        }
         // create a IArticlesDomain business layer access object
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // we memorize the error
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // memorize certain application urls
        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);

         // it's over
        return;
    }

Commenti: Il metodo [init]

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

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

    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
         // check how the initialization of the servelet went
        if (erreurs.size() != 0) {
             // do we have the url of the error page?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // action is processed
        String action = request.getParameter("action");
        if (action == null) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // article info
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // purchase an item
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // basket display
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // remove an item from the basket
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // shopping cart validation
            doValidationPanier(request, response);
            return;
        }
         // unknown share
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // the error page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // end
        return;
    }
  • Il metodo [doGet] inizia verificando se si sono verificati errori di inizializzazione dopo il metodo [init]. In tal caso, visualizza la vista [ERRORS] e il processo termina.
  • Altrimenti, recupera il parametro [action] dalla richiesta del client. Ricorda che l'applicazione è stata realizzata per rispondere a richieste che devono includere un parametro [action].
  • 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 {

         // error list
        ArrayList erreurs = new ArrayList();
         // the list of items is requested
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // end
        return;
    }
  • Ricordiamo che il metodo [init] ha memorizzato il servizio per l'accesso al modello dell'applicazione (livello di dominio) in un campo privato del servlet:
    // champs privés
    private IArticlesDomain articlesDomain = null;
  • Utilizzando questo servizio di accesso, possiamo richiedere l'elenco degli articoli:
        // la liste des erreurs
        ArrayList erreurs = new ArrayList();
        // on demande la liste des articles
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
            // on mémorise l'erreur
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
  • Se si verificano errori, viene inviata la vista [ERRORS]:
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
  • altrimenti, viene inviata la vista [LIST]:
1
2
3
4
5
6
7
         // displays the list of items
        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 liscio e abbiamo ottenuto con successo la vista [LIST]. Invitiamo il lettore a rivedere il codice della vista [LIST] per verificare che i parametri dinamici previsti da questa vista siano effettivamente forniti dal controller. Lo stesso tipo di verifica dovrebbe essere eseguito per ogni vista:

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

Ora descriveremo semplicemente il flusso delle schermate che un utente dell'applicazione incontra. Invitiamo il lettore a seguire ogni volta un ragionamento simile a quello precedente:

Dall'elenco degli elementi, l'utente può selezionare un elemento:

L'acquirente può acquistare l'articolo n. 3 qui. Inseriamo un errore di digitazione nella quantità:

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

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

L'articolo è effettivamente nel carrello. Rimuoviamolo:

L'acquisto è stato rimosso dal carrello e il carrello è stato ricaricato. Ora è vuoto.

Acquistiamo 100 pezzi dell'articolo n. 3 e 2 pezzi dell'articolo n. 4:

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

L'articolo n. 4, invece, è stato acquistato, come dimostra il suo nuovo livello di scorte pari a 39 (40-1):

3.6. Architettura MVC con Struts

3.6.1. Architettura generale delle applicazioni

Rivediamo l'architettura MVC dell'applicazione:

Nella versione precedente:

  • il controller 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 dall'[ActionServlet] generico di Struts
  • Le viste saranno gestite dalle stesse pagine JSP di prima, con alcune piccole differenze
  • Il modello sarà gestito dagli stessi tre archivi

Vedremo che la migrazione dell'applicazione precedente a Struts comporta le seguenti attività:

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

Rivediamo l'architettura MVC generica utilizzata da Struts:

M = modello
classi di business, classi di accesso ai dati e il database
V = viste
pagine JSP
C = controller
il servlet che elabora le richieste dei client, 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 estenderlo. Per i casi semplici, ciò non è necessario. Questo servlet generico recupera le informazioni di cui ha bisogno da un file chiamato, nella maggior parte dei casi, struts-config.xml.
  • Se la richiesta del client contiene parametri del modulo, il controller li inserisce in un oggetto Bean. Gli oggetti Bean creati nel tempo vengono memorizzati nella sessione o nella richiesta del client. Questo comportamento è configurabile. Non è necessario ricrearli se sono già stati creati.
  • Nel file di configurazione struts-config.xml, ogni URL da elaborare a livello di programmazione (e quindi non corrispondente a una vista JSP che potrebbe essere richiesta direttamente) è associato a determinate informazioni:
    • il nome della classe Action responsabile dell'elaborazione della richiesta. Anche in questo caso, l'oggetto Action istanziato può essere memorizzato nella sessione o nella richiesta.
    • Se l'URL richiesto è parametrizzato (come quando un modulo viene inviato al controller), viene specificato il nome del bean responsabile della memorizzazione dei dati del modulo.
  • Grazie alle informazioni fornite dal file di configurazione, quando riceve una richiesta URL da un client, il controller è in grado di stabilire se è necessario creare un bean 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 chiamato `validate` viene chiamato automaticamente dal controller. Il bean viene creato dallo sviluppatore. Lo sviluppatore inserisce quindi il codice che verifica la validità dei dati del modulo all'interno del metodo validate. Se i dati risultano non validi, il controller non procederà oltre. Passerà il controllo a una vista il cui nome trova nel proprio file di configurazione. L'interazione è quindi completata. Si noti che lo sviluppatore può scegliere di non far verificare la validità del modulo. Anche questo viene fatto nel file struts-config.xml. In questo caso, il controller non chiama il metodo validate del bean.
  • Se i dati del bean sono corretti, o se non è prevista alcuna convalida, o se non c'è alcun bean, il controller passa il controllo all'oggetto Action associato all'URL. Lo fa chiamando il metodo execute di quell'oggetto, passando ad esso il riferimento al bean che potrebbe aver costruito. È qui che lo sviluppatore fa ciò che deve essere fatto: potrebbe dover chiamare classi di business o 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 suo file di configurazione, il controller troverà l'URL associato al nome della vista che gli è stato chiesto di visualizzare. Quindi invia la vista. L'interazione con il client è completa.

Nella nostra applicazione, non useremo 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 riceve. Questo facilita il porting della nostra applicazione iniziale. L'architettura finale della nostra applicazione sarà quindi la seguente:

M=modello
classi di business, classi di accesso ai dati e il database
V = Viste
le pagine JSP
C = Controller
il servlet per l'elaborazione delle richieste dei client, oggetti [Action]

3.6.2. Il modello

È stato presentato in precedenza. Consiste negli 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, 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] troverete le stesse librerie della versione precedente, più quella richiesta da Struts:

Image

3.6.3.4. Configurazione dell'applicazione

L'applicazione viene configurata utilizzando 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 dice questo file?

  • La home page dell'applicazione è [views/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 Struts (config)
    • Il nome del file di configurazione 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 dice 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
info.do
per visualizzare le informazioni su un articolo specifico
purchase.do
per acquistare un articolo specifico
carrello.do
per visualizzare il carrello
remove-purchase.do
per rimuovere un acquisto dal carrello
confirmcart.do
per confermare il carrello
  • Le azioni sopra elencate corrispondono una a una alle azioni gestite dal servlet nella versione precedente. Per ciascuna di esse vengono fornite le seguenti informazioni:
  • il nome della classe responsabile della gestione di questa azione
  • le possibili risposte (= viste) dopo l'elaborazione dell'azione. Solo una di esse verrà selezionata dal controller.
  • 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 in [WEB-INF/classes]. In Eclipse, ciò si ottiene collocandolo in [WEB-INF/src]:

Image

3.6.4. Le viste JSP

Anche le viste JSP qui utilizzate sono quelle della versione precedente. Sono state apportate pochissime modifiche: gli URL del tipo [?action=XX?id=YY& ...] sono stati modificati in [/XX.do?id=YY&....]. Stiamo ripetendo le spiegazioni già fornite qui per evitare che l'utente debba tornare indietro. È importante comprendere che le informazioni passate alla vista dal controller sono esattamente le stesse in entrambe le versioni. Nulla è cambiato a questo riguardo.

3.6.4.1. entete.jsp

Per garantire la coerenza tra le diverse viste, queste condivideranno la stessa intestazione, che mostra il nome dell'applicazione insieme al menu:

Il menu è dinamico e definito dal controller. Il controller include nella richiesta inviata alla pagina JSP un attributo chiave "actions" con un valore associato di un array Hastable[]. Ogni elemento di questo array è un dizionario destinato a generare un'opzione di menu nell'intestazione. Ogni dizionario ha due chiavi:

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

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

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

Una volta eseguito, questo tag includerà il codice della pagina [entete.jsp] nella pagina JSP che lo contiene. Poiché l'URL della pagina è un URL relativo (senza / finale), la pagina [entete.jsp] verrà cercata nella stessa directory della pagina contenente 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. list.jsp

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

Viene visualizzata in seguito a una richiesta a /main.do o /validerpanier.do. I parametri di richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable[] - l'array delle opzioni di menu
listarticles
ArrayList di oggetti di tipo [Item]
messaggio
Oggetto String - messaggio da visualizzare in fondo alla pagina

Ogni link [Info] nella tabella HTML degli articoli ha un URL nel formato [/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 vista mostra le informazioni relative a un articolo e ne consente l'acquisto:

Image

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

azioni
object Hashtable[] - l'array delle opzioni di menu
item
oggetto di tipo [Article] - elemento da visualizzare
msg
Oggetto stringa - messaggio da visualizzare in caso di errore con la quantità
qte
Oggetto stringa - valore da visualizzare nel campo di immissione [Qty]

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

Image

Questa pagina contiene un modulo che viene inviato tramite il pulsante [Acquista]. L'URL di destinazione 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. cart.jsp

Questa vista mostra il contenuto del carrello:

Image

Viene visualizzata in seguito a una richiesta a /panier.do o /retirerachat.do?id=ID. I parametri di richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable[] - l'array delle opzioni di menu
carrello
oggetto di tipo [ShoppingCart] - il carrello da visualizzare

Ogni link [Rimuovi] nell'array del carrello HTML ha un URL nella forma [removeitem.do?id=ID], dove ID è il campo [id] dell'articolo da 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. emptycart.jsp

Questa vista mostra le informazioni che indicano che il carrello è vuoto:

Image

Viene visualizzata in seguito a una richiesta a /panier.do o /retirerachat.do?id=ID. I parametri di richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable[] - l'array delle opzioni di 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. errors.jsp

Questa vista 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, che viene gestita dalla vista [INFOS]. Gli elementi della richiesta del controller sono i seguenti:

azioni
Oggetto Hashtable[] - l'array delle opzioni di menu
errori
ArrayList di 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] reindirizza semplicemente 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]. Sappiamo che un servlet possiede un metodo [init] che consente l'inizializzazione dell'applicazione all'avvio. Se utilizziamo il controller generico di Struts [ActionServlet], non abbiamo accesso al suo metodo [init]. In questo caso, abbiamo delle attività da eseguire all'avvio dell'applicazione, principalmente l'istanziazione di un oggetto di accesso al modello. Pertanto, abbiamo bisogno di un metodo [init]. Estendiamo quindi la classe [ActionServlet] nella 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 {
   // private fields
  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);

     // getters - setters
  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{

     // init parent class
    super.init();
     // retrieve servlet initialization parameters
    config = getServletConfig();
    String param = null;
    for (int i = 0; i < parameters.length; i++) {
      param = config.getInitParameter(parameters[i]);
      if (param == null) {
         // we memorize the error
        erreurs.add("Paramètre [" + parameters[i]
            + "] absent dans le fichier [web.xml]");
      }
    }
     // mistakes?
    if (erreurs.size() != 0) {
      return;
    }
     // create a IArticlesDomain business layer access object
    try {
      articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
          new ClassPathResource((String) config
              .getInitParameter(SPRING_CONFIG_FILENAME))))
          .getBean("articlesDomain");
    } catch (Exception ex) {
       // we memorize the error
      erreurs.add("Erreur de configuration de l'accès aux données : "
          + ex.toString());
      return;
    }
     // memorize certain application urls
    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);

     // it's over
    return;
  }
}

Commenti:

  • Il valore della classe risiede nel suo metodo [init] e nei suoi campi privati
  • Il metodo [init] fa la stessa cosa del metodo [init] nel controller della versione precedente:
    • verifica la presenza di determinati parametri di configurazione
    • Istanzia un servizio per accedere al dominio dell'applicazione utilizzando Spring
    • imposta un elenco di errori per segnalare eventuali errori di inizializzazione
  • Prima di iniziare a lavorare, il metodo [init] chiama il metodo [init] della classe padre [ActionServlet]. Questo metodo elaborerà il file di configurazione Struts [struts-config.xml].
  • Sono definiti diversi campi privati con i relativi accessori:
    • errors: l'elenco degli errori rilevati da [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: dizionari. Ciascuno contiene le informazioni necessarie per visualizzare un'opzione nel menu principale mostrato dalla vista [entete.jsp]
    • acticlesDomain: il servizio che fornisce l'accesso al modello dell'applicazione
  • Il controller di un'applicazione Struts è accessibile alle classi [Action] responsabili della gestione delle varie azioni possibili. Queste classi avranno accesso ai campi privati precedenti poiché sono dotate di accessori pubblici.

Questo controller viene istanziato 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>

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

3.6.6. Azioni dell'applicazione Struts

3.6.6.1. Introduzione

Ogni azione Struts sarà implementata come una classe. Nella versione precedente, ogni azione era implementata come un metodo nel controller dell'applicazione. La scrittura della classe [Action] comporta solitamente:

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

3.6.6.2. main.do, list.do

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

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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();

     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();

     // list of errors
    ArrayList erreurs = new ArrayList();
     // the list of items is requested
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // we memorize the error
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
     // mistakes?
    if (erreurs.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // displays the list of items
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");
  }
}

Commenti:

  • Scrivere il codice per una classe [Action] consiste essenzialmente nello scrivere il codice per il suo metodo [execute]
  • Una certa quantità di informazioni è stata memorizzata nell'istanza del controller. Recuperiamo un riferimento ad essa utilizzando:
     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
  • Recuperiamo l'elenco degli errori di inizializzazione memorizzati dal controller. Se questo elenco non è vuoto, la vista [ERRORS] viene inviata al client:
1
2
3
4
5
6
7
8
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

La vista che verrà effettivamente inviata al client è definita nel file [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>

Questa è la vista [/vues/erreurs.jsp]. Il lettore è invitato a verificare cosa ci si aspetta da questa vista. Queste informazioni sono fornite qui dall'azione nell'oggetto [request] come attributi.

  • Ancora una volta, grazie al controller, l'azione può recuperare l'oggetto che fornisce l'accesso al modello dell'applicazione (livello di dominio):
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • Una volta fatto questo, possiamo richiedere l'elenco degli articoli:
    // la liste des erreurs
    ArrayList erreurs = new ArrayList();
    // on demande la liste des articles
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
      // on mémorise l'erreur
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • Se si verificano errori, viene inviata la vista [ERRORS]:
1
2
3
4
5
6
7
8
     // mistakes?
    if (erreurs.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • altrimenti, viene inviata la vista [LIST]:
1
2
3
4
5
6
     // displays the list of items
    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 client è 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>

Questa è la vista [/vues/liste.jsp]. Il lettore è invitato a verificare cosa ci si aspetta da questa vista. Queste informazioni sono fornite qui dall'azione nell'oggetto [request] come attributi.

3.6.6.3. infos.do

Questa azione viene utilizzata per fornire informazioni su uno degli elementi visualizzati nella vista [LIST]:

Questa azione è configurata come segue nel file [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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // list of errors
    ArrayList erreurs = new ArrayList();
     // retrieve the requested id
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // the key item id is requested
    Article article = null;
    try {
      article = articlesDomain.getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // not normal
      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) {
       // not normal
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // put the article in the session
    request.getSession().setAttribute("article", article);
     // the info page is displayed
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionListe() });
    return mapping.findForward("afficherInfosArticle");
  }
}

Commenti:

  • L'inizio del metodo [execute] è identico a quello discusso in precedenza. Lo stesso vale anche per le altre azioni.
  • Il metodo recupera il parametro [id], che dovrebbe normalmente essere presente nell'URL. L'URL dovrebbe infatti avere la forma [/infos.do?id=X]. Vengono eseguiti vari controlli per verificare la presenza e la validità del parametro [id]. Se si verifica un problema, viene visualizzata la vista [ERRORS].
  • Se [id] è valido, l'elemento corrispondente viene richiesto al livello [domain]. Se questo genera un'eccezione o se l'elemento non viene trovato, viene inviata nuovamente la vista [ERRORS].
  • Se tutto va bene, l'elemento recuperato viene memorizzato nella sessione. Questo è un punto discutibile. Qui, supponiamo che il cliente possa acquistare questo elemento. Se lo fa, lo recupereremo dalla sessione piuttosto che richiederlo nuovamente dal livello [domain].
  • Infine, viene visualizzata la vista [INFOS]. La vista che verrà effettivamente inviata al cliente è 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>

Questa è la vista [/vues/infos.jsp]. Il lettore è invitato a verificare cosa ci si aspetta da questa vista. Queste informazioni sono fornite qui dall'azione nell'oggetto [request] come attributi.

3.6.6.4. purchase.do

Questa azione viene utilizzata per acquistare l'articolo visualizzato dalla precedente vista [INFOS]:

Una volta acquistato l'articolo, viene nuovamente visualizzata la vista [LIST] (vista a destra). Se osserviamo il codice HTML della vista a sinistra sopra riportata, vediamo 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>

Possiamo vedere che il modulo viene inviato al controller con l'azione [achat.do].

Questa azione è configurata come segue nel file [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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // we return to the list of items
    return mapping.findForward("afficherListeArticles");
  }
}

Commenti:

  • L'inizio del metodo [execute] è identico a quelli studiati in precedenza.
  • Ricordiamo il formato 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, [qte]: quantità acquistata.
  • Vengono verificate la presenza e la validità del parametro [qte]. Se questo parametro risulta errato, all'utente viene restituita la vista [INFOS] insieme a un messaggio di errore:
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      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. La sessione potrebbe essere scaduta. In questo caso, viene visualizzata la vista [ERRORS]:
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      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, che viene anch'esso recuperato dalla sessione:
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • Infine, inviamo la vista [LIST]:
    // on revient à la liste des articles
    return mapping.findForward("afficherListeArticles");
  • La vista che verrà effettivamente inviata al client è 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>

Questa è la vista [/main.do]. Questa vista non è una vista ma un'azione. L'azione [/main.do] descritta sopra verrà quindi eseguita e visualizzerà l'elenco degli articoli.

3.6.6.5. cart.do

Questa azione viene utilizzata per visualizzare tutti gli acquisti del cliente. È disponibile tramite l'opzione di menu [Visualizza carrello]:

Il codice HTML associato al link [Visualizza carrello] è il seguente:

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

L'azione [panier.do] è configurata come segue nel file [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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // the basket is displayed
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // empty basket
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    } else {
       // there's something in the basket
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(), mainServlet.getHActionValidationPanier() });
      return mapping.findForward("afficherPanier");
    }
  }
}

Commenti:

  • L'inizio del metodo [execute] è identico a quelli discussi in precedenza.
  • Il carrello viene recuperato dalla sessione in cui si trova normalmente. La sessione potrebbe essere scaduta, nel qual caso non c'è alcun carrello. Non consideriamo questo come un errore, ma supponiamo semplicemente che il carrello sia vuoto.
  • Se il carrello è vuoto, viene visualizzata la vista [EMPTY CART]
  • altrimenti, viene visualizzata la vista [PANIER]

Le viste effettivamente inviate al client 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. checkout.do

Questa azione rimuove un articolo dal carrello:

Dopo l'azione [retirerachat.do], il carrello viene visualizzato nuovamente (vista in alto a destra). Se osserviamo il codice HTML del link [Conferma carrello] nella vista in alto a sinistra, vediamo 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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // we pick up the basket
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    }
     // retrieve the id of the item to be removed
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // we remove the purchase
    panier.enlever(id);
     // the basket is displayed again
    return mapping.findForward("afficherPanier");
  }
}

Commenti:

  • L'inizio del metodo [execute] è identico a quelli studiati in precedenza.
  • Il codice seguente verifica la presenza e la validità del parametro [id]. Se non è corretto, viene inviata la vista [ERRORS].
  • Altrimenti, l'acquisto viene rimosso dal carrello:
    // on enlève l'achat
    panier.enlever(id);
  • quindi il carrello viene visualizzato nuovamente:
    // on affiche de nouveau le panier
    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>

Possiamo vedere che, in termini di viste, sarà l'azione [/cart.do] a essere attivata. Questo è già stato descritto. Visualizzerà la vista [CARRELLO] o [CARRELLO VUOTO] a seconda dello stato del carrello.

3.6.6.7. confirmCart.do

Questa azione serve a confermare gli acquisti del cliente. In pratica, comporta una singola operazione: le quantità in magazzino degli articoli acquistati vengono ridotte nel database in base alle quantità acquistate. Questa azione proviene dal seguente menu:

 

Il codice HTML per il link [Conferma carrello] è il seguente:

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

Quando si clicca su questo link, le quantità in magazzino vengono aggiornate e l'elenco degli articoli viene visualizzato nuovamente.

Questa azione è configurata come segue nel file [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 {

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
         // validate basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            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");
        }
         // recover any errors
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
         // everything looks OK - the item list is displayed
    return mapping.findForward("afficherListeArticles");
  }
}

Commenti:

  • L'inizio del metodo [execute] è identico a quelli studiati in precedenza.
  • Recuperiamo il carrello dalla sessione. Se la sessione è scaduta, visualizziamo la vista [ERRORS]:
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • Elaboriamo gli acquisti nel carrello. Potrebbero verificarsi degli errori se le scorte non sono sufficienti per evadere gli acquisti. In questo caso, visualizziamo la vista [ERRORI]:
         // validate basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            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");
        }
         // recover any errors
         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 bene, visualizziamo nuovamente l'elenco degli elementi:
        // tout semble OK - on affiche la liste des articles
    return mapping.findForward("afficherListeArticles");

La vista effettivamente inviata al client è 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>

Possiamo vedere che, dal punto di vista della vista, sarà l'azione [/main.do] a essere attivata. Questo è già stato descritto. Verrà visualizzata la vista [LIST].

3.7. Architettura MVC con Spring

3.7.1. Architettura generale dell'applicazione

Rivediamo l'architettura MVC dell'applicazione:

Nella prima versione:

  • il controller 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 dall'[ActionServlet] generico di Struts
  • le viste erano gestite dalle stesse pagine JSP della versione [Struts]
  • Il modello era gestito dagli stessi tre archivi

Nella versione Spring:

  • il controller sarà gestito da un servlet fornito da Spring [DispatcherServlet]
  • Le viste saranno gestite dalle stesse pagine JSP di prima, con alcune piccole differenze
  • Il modello sarà gestito dagli stessi tre file

Scopriremo che la migrazione della nostra applicazione da Struts a Spring è semplice se siamo disposti a rinunciare all'uso di tutti gli elementi raccomandati per un'architettura Spring MVC standard. Le modifiche principali sono le seguenti:

  • le azioni che in precedenza venivano gestite in metodi specifici del servlet/controller, o da istanze di classi derivate dalla classe [Action] in Struts, sono ora gestite da istanze di classe che implementano l'interfaccia [Controller] di Spring
  • I file di configurazione necessari sono i seguenti:
    • [web.xml] poiché si tratta di un'applicazione web. Questo file contiene un listener che, all'avvio dell'applicazione, utilizzerà il file [applicationContext.xml]
    • [applicationContext.xml] per creare i bean richiesti dall'applicazione, in particolare il bean del servizio di accesso al modello
  • Le viste JSP saranno identiche a quelle in Struts. Dovremo crearne una nuova.

Ricordiamo l'architettura MVC di STRUTS utilizzata nella versione precedente:

M = Modello
classi di business, classi di accesso ai dati e il database
V = Viste
le pagine JSP
C = Controller
il servlet per l'elaborazione delle richieste dei client, oggetti [Action]

Con Spring, utilizziamo la stessa architettura:

M = modello
classi di business, classi di accesso ai dati e il database
V = viste
le pagine JSP
C = Controller
il servlet che elabora le richieste dei client, gli oggetti che implementano l'interfaccia [Controller]
  • Il controller è il cuore dell'applicazione. Tutte le richieste dei client passano attraverso di esso. Si tratta di un servlet generico fornito da SPRING. È di tipo [DispatcherServlet]. D'ora in poi, ci riferiremo a questo controller come al controller [Spring].
  • Il controller [Spring] indirizzerà la richiesta del client a una delle istanze [Controller]. Ci sarà un'istanza di questo tipo per ogni azione da elaborare. Questo è definito nell'URL richiesto, proprio come con Struts. Pertanto, sapremo che l'azione richiesta è l'azione [list] perché l'URL richiesto è [list.do]
  • Se C è il contesto dell'applicazione, il controller [Spring] utilizza un file [C-servlet.xml] che svolge lo stesso ruolo del file di configurazione struts-config.xml nella versione Struts. Per ogni azione da elaborare dall'applicazione, associamo il nome della classe di tipo Controller responsabile della gestione della richiesta.
  • Il controller passa il controllo all'oggetto di tipo Controller associato all'azione. Lo fa chiamando il metodo handleRequest di quell'oggetto e passando ad esso la richiesta del client. È qui che lo sviluppatore esegue le operazioni necessarie: potrebbe dover chiamare classi di logica di business o 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 suo file di configurazione, il controller troverà l'URL associato al nome della vista che gli è stato chiesto di visualizzare. Quindi invia la vista. L'interazione con il client è completata.

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, 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] troverete le stesse librerie della versione precedente, ad eccezione di quelle di Struts, che non sono più necessarie:

Image

3.7.3.4. Configurazione dell'applicazione

L'applicazione viene configurata utilizzando 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>
     <!-- application spring context loader -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- the servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- url mapping -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- entry document -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

Cosa dice questo file?

  • La home page 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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Alcuni elementi sono familiari, mentre altri lo sono meno. Durante l'inizializzazione dell'applicazione verranno istanziati tre bean:

  • articlesDao: servizio che fornisce l'accesso al livello [dao]
  • articlesDomain: servizio che fornisce l'accesso al modello
  • config: un bean in cui raccoglieremo le informazioni che tutti i client devono condividere. Questo bean svolgerà il ruolo tradizionalmente svolto dal 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>
     <!-- stock managers = controllers -->
    <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>

     <!-- application mapping-->
    <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>

     <!-- view manager -->
    <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>

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

Cosa dice 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
info.do
per visualizzare le informazioni su un articolo specifico
purchase.do
per acquistare un articolo specifico
carrello.do
per visualizzare il carrello
remove-purchase.do
per rimuovere un acquisto dal carrello
confirmcart.do
per confermare il carrello
  • Le azioni sopra elencate corrispondono una a una alle azioni gestite dal controller nelle versioni precedenti. Per ciascuna di esse è specificato il nome della classe responsabile della gestione. Prendiamo l'esempio dell'azione [/panier.do]:
  • deve essere gestita dal bean [VoirPanierController]. Questo nome è arbitrario. Si tratta semplicemente di una 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. Qui, il bean [config] definito da [applicationContext.xml] e istanziato all'avvio dell'applicazione viene fornito come parametro. Questo verrà fatto per tutte le azioni [Controller]. Pertanto, ciascuna di esse avrà, in un campo privato, l'oggetto [config] in cui troverà tutte le informazioni condivise tra tutti i client.
  • Come devono essere risolti i nomi delle viste:
    <!-- gestionnaire de vues -->
    <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à, dopo l'elaborazione, una chiave al controller Spring per indicare quale vista deve visualizzare. In base a questa chiave, possono esserci varie strategie per generare la vista associata alla chiave. La strategia utilizzata è quella definita dal bean [viewResolver]. In questo caso, questo bean è associato alla classe [org.springframework.web.servlet.view.InternalResourceViewResolver] con vari parametri di inizializzazione. Senza entrare nei dettagli, il bean [viewResolver] specifica qui che se la chiave della vista è "XX", la vista generata sarà [/views/XX.jsp]. Il tipo di viste inviate al client può essere modificato in vari modi:

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

Pertanto, è 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, ciò si ottiene collocandolo in [WEB-INF/src]:

Image

3.7.4. Le viste JSP

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

Image

Viene creata una sola nuova vista: redirpanier.jsp. Viene utilizzata esclusivamente per 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 invitano i lettori a consultare le definizioni delle varie viste nella versione Struts.

3.7.5. Elaborazione delle azioni

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

Image

Vediamo come funziona l'applicazione Spring utilizzando 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>
     <!-- application spring context loader -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- the servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- url mapping -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- entry document -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  • Se questa era la prima richiesta all'applicazione, sono state attivate diverse operazioni:
    • il listener [org.springframework.web.context.ContextLoaderListener] è stato caricato
    • ha analizzato 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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <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>
     <!-- stock managers = controllers -->
    <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>

     <!-- application mapping-->
    <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>

     <!-- view manager -->
    <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>

     <!-- message file -->
    <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 gestire la richiesta del client. La richiesta era: [http://localhost:8080/springwebarticles]. In questo caso, non stiamo richiedendo un URL dal 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"/>
  • Al client viene quindi richiesto di reindirizzarsi all'URL [http://localhost:8080/springwebarticles/main.do]. Il client esegue l'operazione.
  • 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>
     <!-- stock managers = controllers -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

     <!-- application mapping-->
    <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 indica che l'azione [/main.do] deve essere gestita dal bean [ListController].
  • La richiesta del client viene passata al metodo [handleRequest] del bean [ListController]. Questo metodo esegue il proprio compito e restituisce al controller la chiave della vista da visualizzare. In questo caso, se tutto va bene, questa chiave sarà [list].
  • Il controller Spring utilizza il bean [viewResolver] dal file di configurazione [springwebarticles-servlet.xml] per determinare la vista associata a questa 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 accennato al fatto che all'avvio dell'applicazione vengono istanziati i bean presenti nel file [applicationContext.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Conosciamo bene i bean [articlesDao, articlesDomain], ma non il bean [config]. Questo bean è 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 {

     // private fields
    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);

     // getters-setters
    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;
    }

     // init web application
    public void init() {
         // memorize certain application urls
        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);

         // it's over
        return;
    }
}

Questa classe svolge la stessa funzione del metodo [init] di un servlet di un'applicazione web. Inizializza l'applicazione. In questo caso, ciò avviene come segue:

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

Quando viene creato, il suo campo privato [articlesDomain] viene inizializzato

  • poi, grazie all'attributo [init-method="init"] del bean sopra, viene eseguito il metodo [init] della classe associata al bean. Qui, si inizializzano i tre dizionari [hActionListe, hActionPanier, hActionValidationPanier] utilizzati per generare i tre possibili link di menu offerti all'utente.
  • Vengono creati degli accessori pubblici per rendere questi campi privati accessibili 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 molto spesso in:

  • copiare e incollare la classe [Action] utilizzata nella versione Struts
  • 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:

    <!-- le mapping de l'application-->
    <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], di cui parleremo 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 {

     // web app configuration
    Config config;

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

     // query processing
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

         // the list of items is requested
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // send error view
            return new ModelAndView("erreurs");
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // send view
        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 mostrato sopra, il campo [config] di [ListController] viene inizializzato con il bean [config]. Di cosa si tratta? È il bean [config] definito in [applicationContext.xml], ovvero un'istanza di [istia.st.articles.web.spring.Config] descritta in precedenza.

  • Scrivere il codice per una classe [Controller] implica essenzialmente scrivere il codice per il suo metodo [handleRequest]
  • Richiediamo l'elenco degli articoli dal modello. Questo è accessibile tramite [config.getArticlesDomain()]. Se si verifica un'eccezione, viene visualizzata la vista [ERRORS]. Il risultato restituito da [handleRequest] deve essere di tipo [ModelAndView]. Questa classe può essere istanziata in vari modi. Qui, e questo sarà sempre il caso, creiamo un'istanza di [ModelAndView] passandole la chiave della vista da visualizzare. Ricordiamo che, in base alla configurazione del bean [viewResolver], la richiesta della vista con chiave XX comporterà l'invio della vista [/vues/XX.jsp].
        // on demande la liste des articles
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
            // on mémorise l'erreur
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
            // on affiche la page des erreurs
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
            // envoyer la vue erreurs
            return new ModelAndView("erreurs");
        }
  • Se non ci sono errori, viene inviata la vista [LIST]:
1
2
3
4
5
6
7
        // on affiche la liste des articles
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
        // envoyer la vue
        return new ModelAndView("liste");

3.7.7.3. infos.do

Questa azione viene utilizzata per fornire informazioni su uno degli elementi visualizzati nella vista [LIST]:

Questa azione è definita nel file [springwebarticles-servlet.xml] come segue:

    <!-- le mapping de l'application-->
    <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 {

     // web app configuration
    Config config;

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

     // query processing
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

     // error list
    ArrayList erreurs = new ArrayList();
     // retrieve the requested id
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // the id key item is requested
    Article article = null;
    try {
      article = config.getArticlesDomain().getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // not normal
      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) {
       // not normal
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // put the article in the session
    request.getSession().setAttribute("article", article);
     // the info page is displayed
    request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
        return new ModelAndView("infos");
    }
}

Commenti:

  • Il metodo [handleRequest] recupera il parametro [id], che normalmente dovrebbe essere presente nell'URL. L'URL dovrebbe infatti avere il formato [/infos.do?id=X]. Vengono eseguiti vari controlli per verificare la presenza e la validità del parametro [id]. Se si verifica un problema, viene visualizzata la vista [ERRORS].
  • Se [id] è valido, l'elemento corrispondente viene richiesto al livello [domain]. Se questo genera un'eccezione o se l'elemento non viene trovato, viene inviata nuovamente la vista [ERROR].
  • Se tutto procede correttamente, l'elemento recuperato viene memorizzato nella sessione. Questo è un punto discutibile. In questo caso, si presume che il cliente possa acquistare questo elemento. In tal caso, lo recupereremo dalla sessione anziché richiederlo nuovamente dal livello [domain].
  • Infine, viene visualizzata la vista [INFO].

3.7.7.4. purchase.do

Questa azione viene utilizzata per acquistare l'articolo visualizzato dalla precedente vista [INFOS]:

Image

Se osserviamo il codice HTML di questa vista, vediamo 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>

Possiamo vedere che il modulo viene inviato al controller con l'azione [achat.do].

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

    <!-- le mapping de l'application-->
    <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 {

   // web app configuration
  Config config;

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

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the session item
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if (article == null) {
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // we return to the list of items
    return new ModelAndView("index");
  }
}

Commenti:

  • Esaminiamo il formato 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>
  • La richiesta contiene due parametri: [id]: numero dell'articolo, [qte]: quantità acquistata.
  • Vengono verificate la presenza e la validità del parametro [qte]. Se questo parametro risulta errato, all'utente viene restituita la vista [INFOS] insieme a un messaggio di errore:
    // la liste des erreurs sur cette action
    ArrayList erreurs = new ArrayList();
    // on récupère la quantité achetée
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
      // qté erronée
      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. La sessione potrebbe essere scaduta. In questo caso, viene inviata la vista [ERRORS]:
    // on récupère la session du client
    HttpSession session = request.getSession();
    // on récupère l'article mis en session
    Article article = (Article) session.getAttribute("article");
    // session expirée ?
    if (article == null) {
      // on affiche la page des erreurs
      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
    // on crée le nouvel achat
    Achat achat = new Achat(article, qté);
    // on ajoute l'achat au panier du client
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • Infine, inviamo la vista [LIST]:
    // on revient à la liste des articles
    return new ModelAndView("index");
  • Sopra, inviamo la vista [/views/index.jsp]. Sappiamo che questa vista indica al browser del cliente di reindirizzarsi all'URL [/main.do]. È questo reindirizzamento che visualizzerà l'elenco degli articoli.

3.7.7.5. cart.do

Questa azione viene utilizzata per visualizzare tutti gli acquisti del cliente. È disponibile tramite il menu:

Image

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

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

La pagina restituita da questo link è la seguente:

Image

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

    <!-- le mapping de l'application-->
    <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 {

   // web app configuration
  Config config;

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

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the basket is displayed
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // empty basket
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("paniervide");
    } else {
       // there's something in the basket
      request.setAttribute("actions", new Hashtable[] {
          config.getHActionListe(), config.getHActionValidationPanier() });
      return new ModelAndView("panier");
    }
  }
}

Commenti:

  • Il carrello viene recuperato dalla sessione in cui è normalmente memorizzato. La sessione potrebbe essere scaduta, nel qual caso non è presente alcun carrello. Non consideriamo questa situazione come un errore, ma supponiamo semplicemente che il carrello sia vuoto.
  • Se il carrello è vuoto, viene visualizzata la vista [CARRELLO VUOTO]
  • altrimenti, viene visualizzata la vista [CARRELLO]

3.7.7.6. removePurchase.do

Questa azione viene utilizzata per rimuovere un acquisto dal carrello:

Image

Se osserviamo il codice HTML del link sopra riportato, vediamo 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 nel file [springwebarticles-servlet.xml]:

    <!-- le mapping de l'application-->
    <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 {

   // web app configuration
  Config config;

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

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // we pick up the basket
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // retrieve the id of the item to be removed
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // we remove the purchase
    panier.enlever(id);
     // the basket is displayed again
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");
  }
}

Commenti:

  • Il codice verifica la presenza e la validità del parametro [id]. Se non è corretto, viene inviata la vista [ERRORS].
  • In caso contrario, l'articolo viene rimosso dal carrello:
    // on enlève l'achat
    panier.enlever(id);
  • quindi il carrello viene ricaricato:
    // on affiche de nouveau le panier
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");

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

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

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

Possiamo vedere che il client verrà reindirizzato all'azione [/panier.do]. Questo è già stato descritto. Verrà visualizzata la vista [PANIER] o [PANIERVIDE] a seconda dello stato del carrello.

3.7.7.7. confirmcart.do

Questa azione serve a confermare gli acquisti del cliente. In pratica, si tratta di un'unica operazione: le quantità in magazzino degli articoli acquistati vengono decrementate nel database in base alle quantità acquistate. Questa azione è accessibile dal seguente menu:

Image

Il codice HTML per il link [Conferma carrello] è il seguente:

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

Quando si clicca su questo link, le quantità in magazzino vengono aggiornate e l'elenco degli articoli viene visualizzato nuovamente.

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

    <!-- le mapping de l'application-->
    <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 {

   // web app configuration
  Config config;

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

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the buyer has confirmed his basket
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // validate basket
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // not normal
      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");
    }
     // recover any errors
    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");
    }
     // everything looks OK - the item list is displayed
    return new ModelAndView("index");
  }
}

Commenti:

  • Recuperiamo il carrello dalla sessione. Se la sessione è scaduta, visualizziamo la vista [ERRORS]:
    // la liste des erreurs sur cette action
    ArrayList erreurs = new ArrayList();
    // l'acheteur a confirmé son panier
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
      // session expirée
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • Elaboriamo gli acquisti nel carrello. Potrebbero verificarsi degli errori se le scorte non sono sufficienti per evadere gli acquisti. In questo caso, visualizziamo la vista [ERRORS]:
    // on valide le panier
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
      // pas normal
      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");
    }
    // on récupère les éventuelles erreurs
    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 bene, visualizziamo nuovamente l'elenco degli articoli:
    // tout semble OK - on affiche la liste des articles
    return new ModelAndView("index");

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