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

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

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

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

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

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:
- 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.
- Il controller elabora questa richiesta. Per farlo, potrebbe aver bisogno dell’assistenza del livello business, noto come modello M nella struttura MVC.
- 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
- 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.
- 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:
- classi di business
- classi di accesso ai dati
- 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);
|
| chiave primaria che identifica in modo univoco un articolo |
| nome dell'elemento |
| il suo prezzo |
| scorte attuali |
| 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:
- un costruttore per impostare le 5 informazioni relative a un elemento
- 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.
- la convalida dei dati inseriti per l'elemento. Se i dati non sono validi, viene generata un'eccezione.
- 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:
| restituisce tutti gli elementi della tabella ARTICLES in un elenco di oggetti [Article] |
| cancella la tabella ARTICLES |
| restituisce l'oggetto [Article] identificato dalla sua chiave primaria |
| consente di aggiungere un articolo alla tabella ARTICLES |
| consente di modificare una voce nella tabella [ARTICLES] |
| consente di eliminare un articolo dalla tabella [ARTICLES] |
| 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#>=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();
}
|
| 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 |
| 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:
| l'oggetto di accesso ai dati fornito dal livello di accesso ai dati |
| 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:
| l'articolo acquistato |
| la quantità acquistata |
| restituisce l'importo dell'acquisto |
| 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:
| 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 |
| restituisce l'importo totale degli acquisti |
| restituisce la rappresentazione stringa del carrello |
| 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:

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:
| // 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:
| // 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:
| // 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:
| Classi di business, classi di accesso ai dati e il database |
| Pagine JSP |
| 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.jsp | Le viste si trovano nella cartella [vues] dell'applicazione  |
| info.jsp |
| carrello.jsp |
| empty-cart.jsp |
| 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:
| significato | azione del controller | risposte possibili |
| il client desidera l'elenco di elementi | - richiede l'elenco degli articoli al azienda | - [LIST] - [ERRORI] |
| Il client richiede Informazioni su uno degli elementi visualizzati nella vista [LIST] | - richiede l'elemento dal livello business | - [INFO] - [ERRORI] |
| 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 |
| il cliente desidera rimuovere un acquisto dal proprio carrello | - recupera il carrello dalla sessione e modificalo | - [CARRELLO] - [SVUOTA CARRELLO] - [ERRORI] |
| 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:
- modifiche agli URL delle varie viste
- modifiche alle classi che implementano le interfacce [IArticlesDao] e [IArticlesDomain]
- 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:
- 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.
- 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#>=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
| |
 |  |
3.5.9. Viste JSP
Le viste JSP utilizzano la libreria di tag JSTL.
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:
| Oggetto Hashtable[] - l'array delle opzioni di menu |
| ArrayList di oggetti di tipo [Item] |
| 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:

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:
| Oggetto Hashtable[] - l'array delle opzioni di menu |
| oggetto di tipo [Article] - elemento da visualizzare |
| Oggetto stringa - messaggio da visualizzare in caso di errore con la quantità |
| 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à:

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:

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:
| Oggetto Hashtable[] - l'array delle opzioni di menu |
| 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:

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:
| Oggetto Hashtable[] - l'array delle opzioni di menu |
Codice:
| <%@ 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:

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:
| Oggetto Hashtable[] - l'array delle opzioni di menu |
| 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:
| <%@ 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:
| /**
* @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 |
| GET /main?action=list | - richiedi l'elenco degli elementi dalla classe di business - visualizzarlo | [LIST] o [ERRORS] |
| GET /main?action=info&id=ID | - recupera l'elemento con id=ID dalla classe di business - visualizzarlo | [INFO] o [ERRORS] |
| 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] |
| GET /main?action=removePurchase&id=ID | - rimuove l'articolo con id=ID dalla lista della spesa nella sessione del cliente | [CART] |
| GET /main?action=cart | - Visualizza la sessione del cliente | [CART] o [EMPTY_CART] |
| 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
| 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:

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]:
| // 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:
| classi di business, classi di accesso ai dati e il database |
| pagine JSP |
| 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:
| classi di business, classi di accesso ai dati e il database |
| le pagine JSP |
| 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:

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]:

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:

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

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:
| per visualizzare l'elenco degli articoli |
| per visualizzare l'elenco degli articoli |
| per visualizzare le informazioni su un articolo specifico |
| per acquistare un articolo specifico |
| per visualizzare il carrello |
| per rimuovere un acquisto dal carrello |
| 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]:

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:
| Oggetto Hashtable[] - l'array delle opzioni di menu |
| ArrayList di oggetti di tipo [Item] |
| 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:

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:
| object Hashtable[] - l'array delle opzioni di menu |
| oggetto di tipo [Article] - elemento da visualizzare |
| Oggetto stringa - messaggio da visualizzare in caso di errore con la quantità |
| 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à:

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:

Viene visualizzata in seguito a una richiesta a /panier.do o /retirerachat.do?id=ID. I parametri di richiesta del controller sono i seguenti:
| Oggetto Hashtable[] - l'array delle opzioni di menu |
| 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:

Viene visualizzata in seguito a una richiesta a /panier.do o /retirerachat.do?id=ID. I parametri di richiesta del controller sono i seguenti:
| Oggetto Hashtable[] - l'array delle opzioni di menu |
Codice:
| <%@ 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:

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:
| Oggetto Hashtable[] - l'array delle opzioni di menu |
| 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:
| <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:

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:
| // 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]:
| // 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]:
| // 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:
| <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:
| <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:
| classi di business, classi di accesso ai dati e il database |
| le pagine JSP |
| il servlet per l'elaborazione delle richieste dei client, oggetti [Action] |
Con Spring, utilizziamo la stessa architettura:
| classi di business, classi di accesso ai dati e il database |
| le pagine JSP |
| 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:

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]:

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:

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]:

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:
| per visualizzare l'elenco degli articoli |
| per visualizzare l'elenco degli articoli |
| per visualizzare le informazioni su un articolo specifico |
| per acquistare un articolo specifico |
| per visualizzare il carrello |
| per rimuovere un acquisto dal carrello |
| 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]:

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

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]:

Vediamo come funziona l'applicazione Spring utilizzando un esempio:
- L'utente richiede l'URL [http://localhost:8080/springwebarticles/main.do]

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:

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]:
| // 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]:

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:
| // 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:

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:

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:

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:

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].