3. Article 2 - Exemples d'architectures web à trois couches
Objectifs de l'article :
- architectures à 3 couches
- architecture web MVC basique
- architecture Struts MVC
- architecture Spring MVC
Outils utilisés :
- Spring : http://www.springframework.org/
- Ibatis SqlMap : http://www.ibatis.com/
- Junit : http://www.junit.org/index.htm
- Eclipse : http://www.eclipse.org/
- Struts : http://struts.apache.org/
- Firebird : http://firebird.sourceforge.net/ : SGBD, pilote JDBC. En fait, toute source JDBC fait l'affaire.
- IBExpert, personal edition : http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (mars 2005). IBExpert permet d'administrer graphiquement le SGBD Firebird.
- Tomcat : http://jakarta.apache.org/tomcat/
- Plugin Tomcat pour Eclipse : http://www.sysdeo.com/eclipse/tomcatPlugin.html. Voir également le document https://tahe.developpez.com/java/eclipse/
La compréhension de ce document nécessite divers pré-requis. Certains d'entre-eux peuvent être acquis dans des documents que j'ai écrits. Dans ce cas, je les cite. Il est bien évident que ce n'est qu'une suggestion et que le lecteur peut utiliser ses documents favoris.
- langage Java : [https://tahe.developpez.com/java/cours]
- programmation web en Java : [https://tahe.developpez.com/java/web/]
- programmation web avec Java, Eclipse, Tomcat : [https://tahe.developpez.com/java/eclipse/]
- programmation web avec Struts : [https://tahe.developpez.com/java/struts/]
- utilisation de l'aspect IoC de Spring : [https://tahe.developpez.com/java/springioc]
- bibliothèque JSTL de balises : [https://tahe.developpez.com/java/eclipse/] (en partie)
- documentation Ibatis SqlMap : [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
- Firebird : [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (mars 2005).
Les idées de ce document ont pour origine un livre lu au cours de l'été 2004, un magnifique travail de Rod Johnson : J2EE Development without EJB aux éditions Wrox.
3.1. L'application webarticles
Nous souhaitons donner ici quelques éléments d'une application web de commerce électronique. Celle-ci permettra à des clients du web
- de consulter une liste d'articles provenant d'une base de données
- d'en mettre certains dans un panier électronique
- de valider celui-ci. Cette validation aura pour seul effet de mettre à jour, dans la base, les stocks des articles achetés.
Les différentes vues présentées à l'utilisateur seront les suivantes :
- la vue [LISTE] qui présente une liste des articles en vente

- la vue [INFOS] qui donne des informations supplémentaires sur un produit :

- la vue [PANIER] qui donne le contenu du panier du client

- la vue [PANIERVIDE] pour le cas où le panier du client est vide

- la vue [ERREURS] qui signale toute erreur de l'application

3.2. Architecture générale de l'application
On souhaite construire une application ayant la structure à trois couches suivante :
- les trois couches sont rendues indépendantes grâce à l'utilisation d'interfaces Java
- l'intégration des différentes couches est réalisée par Spring
- chaque couche fait l'objet de paquetages séparés web (couche Interface Utilisateur), domain (couche métier) et dao (couche d'accès aux données).
Nous supposerons ici que les couches [domain] et [dao] sont acquises. Nous ne nous intéresserons qu'à la couche [web] que nous nous proposons de construire de plusieurs façons :
- à l'aide d'une technologie classique servlet contrôleur - pages JSP
- à l'aide de la technologie Struts MVC
- à l'aide de la technologie Spring MVC
Dans tous les cas, l'application respectera une architecture MVC (Modèle - Vue - Contrôleur). Si nous reprenons le schéma en couches ci-dessus, l'architecture MVC s'y intègre de la façon suivante :
Le traitement d'une demande d'un client se déroule selon les étapes suivantes :
- le client fait une demande au contrôleur. Ce contrôleur est une servlet qui voit passer toutes les demandes des clients. C'est la porte d'entrée de l'application. C'est le C de MVC.
- le contrôleur traite cette demande. Pour ce faire, il peut avoir besoin de l'aide de la couche métier, ce qu'on appelle le modèle M dans la structure MVC.
- le contrôleur reçoit une réponse de la couche métier. La demande du client a été traitée. Celle-ci peut appeler plusieurs réponses possibles. Un exemple classique est
- une page d'erreurs si la demande n'a pu être traitée correctement
- une page de confirmation sinon
- le contrôleur choisit la réponse (= vue) à envoyer au client. Celle-ci est le plus souvent une page contenant des éléments dynamiques. Le contrôleur fournit ceux-ci à la vue.
- la vue est envoyée au client. C'est le V de MVC.
3.3. Le modèle
Nous étudions ici le M de MVC. Le modèle est ici constitué des éléments suivants :
- les classes métier
- les classes d'accès aux données
- la base de données
3.3.1. La base de données
La base de données ne contient qu'une table appelée ARTICLES. Celle-ci a été générée avec les commandes SQL suivantes :
| 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
);
/* contraintes */
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<>'');
/* clé primaire */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
|
| clé primaire identifiant un article de façon unique |
| nom de l'article |
| son prix |
| son stock actuel |
| le stock au-dessous duquel une commande de réapprovisionnement doit être faite |
Dans les tests qui suivent, une base [Firebird] a été utilisée. [Firebird] est un SGBD " open source ". Le pilote JDBC [firebirdsql-full.jar] est placé dans le dossier [WEB-INF/lib] de l'application web.
3.3.2. Les paquetages du modèle
Le modèle M est ici fourni sous la forme de trois archives :
- istia.st.articles.dao : contient les classes d'accès aux données de la couche [dao]
- istia.st.articles.exception : contient une classe d'exception pour cette gestion d'articles
- istia.st.articles.domain : contient les classes métier de la couche [domain]
archive | contenu | rôle |
istia.st.articles.dao | - contient le paquetage [istia.st.articles.dao] qui lui-même contient les éléments suivants : - [IArticlesDao]: l'interface d'accès à la couche Dao C'est la seule interface que voit la couche [domain]. Elle n'en voit pas d'autre. - [Article] : classe définissant un article - [ArticlesDaoSqlMap] : classe d'implémentation de l'interface [IArticlesDao] avec l'outil SqlMap | couche d'accès aux données – se trouve entièrement dans la couche [dao] de l'architecture 3-tier de l'application web |
istia.st.articles.domain | - contient le paquetage [istia.st.articles.domain] qui lui-même contient les éléments suivants : - [IArticlesDomain]: l'interface d'accès à la couche [domain]. C'est la seule interface que voit la couche web. Elle n'en voit pas d'autre. - [AchatsArticles] : une classe implémentant [IArticlesDomain] - [Achat] : classe représentant l'achat d'un client - [Panier] : classe représentant l'ensemble des achats d'un client | représente le modèle des achats sur le web - se trouve entièrement dans la couche [domain] de l'architecture 3-tier de l'application web |
istia.st.articles.exception | - contient le paquetage [istia.st.articles.exception] qui lui-même contient les éléments suivants : - [UncheckedAccessArticlesException]: classe définissant une exception de type [RuntimeException]. Ce type d'exception est lancée par la couche [dao] dès qu'un problème d'accès aux données se produit. | |
3.3.3. Le paquetage [istia.st.articles.dao]
La classe définissant un article est la suivante :
| 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 attributs d'instance
setId(id);
setNom(nom);
setPrix(prix);
setStockActuel(stockActuel);
setStockMinimum(stockMinimum);
}
// getters - setters
public int getId() {
return id;
}
public void setId(int id) {
// id valide ?
if (id < 0)
throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
// nom valide ?
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) {
// prix valide ?
if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
this.prix = prix;
}
public int getStockActuel() {
return stockActuel;
}
public void setStockActuel(int stockActuel) {
// stock valide ?
if (stockActuel < 0)
throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
this.stockActuel = stockActuel;
}
public int getStockMinimum() {
return stockMinimum;
}
public void setStockMinimum(int stockMinimum) {
// stock valide ?
if (stockMinimum < 0)
throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
this.stockMinimum = stockMinimum;
}
public String toString() {
return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
+ stockMinimum + "]";
}
}
|
Cette classe offre :
- un constructeur permettant de fixer les 5 informations d'un article
- des accesseurs appelés souvent getters/setters servant à lire et écrire les 5 informations. Les noms de ces méthodes suivent la norme JavaBean. L'utilisation d'objets JavaBean dans la couche DAO pour faire l'interface avec les données du SGBD est classique.
- une vérification des données insérées dans l'article. En cas de données erronées, une exception est lancée.
- une méthode toString qui permet d'obtenir la valeur d'un article sous forme de chaîne de caractères. C'est souvent utile pour le débogage d'une application.
L'interface [IArticlesDao] est définie comme suit :
| 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);
}
|
Le rôle des différentes méthodes de l'interface est le suivant :
| rend tous les articles de la table ARTICLES dans une liste d'objets [Article] |
| vide la table ARTICLES |
| rend l'objet [Article] identifié par sa clé primaire |
| permet d'ajouter un article à la table ARTICLES |
| permet de modidier un article de la table [ARTICLES] |
| permet de supprimer un article de la table [ARTICLES] |
| permet de modifier le stock d'un article de la table [ARTICLES] |
L'interface met à disposition des programmes clients un certain nombre de méthodes définies uniquement par leurs signatures. Elle ne s'occupe pas de la façon dont ces méthodes seront réellement implémentées. Cela amène de la souplesse dans une application. Le programme client fait ses appels sur une interface et non pas sur une implémentation précise de celle-ci.
Le choix d'une implémentation précise se fera au moyen d'un fichier de configuration Spring. Nous nous proposons d'implémenter ici l'interface IArticlesDao en utilisant un produit open source appelé SqlMap. Il nous permettra d'enlever toute instruction SQL du code java.
La classe d'implémentation [ArticlesDaoSqlMap] est définie comme suit :
| 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) {}
}
|
Toutes les méthodes d'accès aux données ont été synchronisées afin d'éviter les problèmes d'accès concurrents à la source de données. A un moment donné, un seul thread a accès à une méthode donnée.
La classe [ArticlesDaoSqlMap] utilise l'outil [Ibatis SqlMap]. L'intérêt de cet outil est de permettre de sortir le code SQL d'accès aux données du code Java. Il est alors placé dans un fichier de configuration. Nous aurons l'occasion d'y revenir. Pour se construire, la classe [ArticlesDaoSqlMap] a besoin d'un fichier de configuration dont le nom est passé en paramètre au constructeur de la classe. Ce fichier de configuration définit les informations nécessaires pour :
- accéder au SGBD dans lequel se trouvent les articles
- gérer un pool de connexions
- gérer les transactions
Dans notre exemple, il s'appellera [sqlmap-config-firebird.xml] et définira l'accès à une base 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>
|
Le fichier de configuration [articles.xml] référencé ci-dessus permet de définir comment construire une instance de la classe [istia.st.articles.dao.Article] à partir d'une ligne de la table [ARTICLES] du SGBD. Il définit également les requêtes SQL qui permettront à la couche [dao] d'obtenir les données de la source de données Firebird.
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- un alias sur la classe istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- le mapping ORM : ligne table ARTICLES - instance classe 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>
<!-- la requête SQL pour obtenir tous les articles -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- la requête SQL pour supprimer tous les articles -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- la requête SQL pour insérer un article -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>
<!-- la requête SQL pour supprimer un article donné -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- la requête SQL pour modifier un article donné -->
<statement id="modifyArticle">
update ARTICLES set nom=#nom#,
prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
id=#id#
</statement>
<!-- la requête SQL pour obtenir un article donné -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- la requête SQL pour modifier le stock d'un article donné -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#mouvement#
where id=#id# and stockActuel+#mouvement#>=0
</statement>
</sqlMap>
|
Le code du paquetage [dao] sera trouvé en annexe.
3.3.4. Le paquetage [istia.st.articles.domain]
L'interface [IArticlesDomain] découple la couche [métier] de la couche [web]. Cette dernière accède à la couche [métier/domain] via cette interface sans se préoccuper de la classe qui l'implémente réellement. L'interface définit les actions suivantes pour l'accès à la couche métier :
| package istia.st.articles.domain;
// Imports
import java.util.ArrayList;
import java.util.List;
public abstract interface IArticlesDomain {
// Méthodes
void acheter(Panier panier);
List getAllArticles();
Article getArticleById(int idArticle);
ArrayList getErreurs();
}
|
| rend la liste liste d'objets [Article] à présenter au client |
Article getArticleById(int idArticle)
| rend l'objet [Article] identifié par [idArticle] |
void acheter(Panier panier)
| valide le panier du client en décrémentant les stocks des articles achetés de la quantité achetée - peut échouer si le stock est insuffisant |
| rend la liste des erreurs qui se sont produites - vide si pas d'erreurs |
Ici, l'interface [IArticlesDomain] sera implémentée par la classe [AchatsArticles] suivante :
| 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 {
// Champs
private IArticlesDao articlesDao;
private ArrayList erreurs;
// Constructeurs
public AchatsArticles(IArticlesDao articlesDao) { }
// Méthodes
public ArrayList getErreurs() {}
public List getAllArticles() {}
public Article getArticleById(int id) {}
public void acheter(Panier panier) { }
}
|
Cette classe implémente les quatre méthodes de l'interface [IArticlesDomain]. Elle a deux champs privés :
| l'objet d'accès aux données fourni par la couche d'accès aux données |
| la liste des erreurs éventuelles |
Pour construire une instance de la classe, il faut fournir l'objet permettant l'accès aux données du SGBD :
public AchatsArticles(IArticlesDao articlesDao)
| constructeur |
La classe [Achat] représente un achat du client :
| package istia.st.articles.domain;
public class Achat {
// Champs
private Article article;
private int qte;
// Constructeurs
public Achat(Article article, int qte) { }
// Méthodes
public double getTotal() {}
public Article getArticle() {}
public void setArticle(Article article) { }
public int getQte() {}
public void setQte() { }
public String toString() {}
}
|
La classe [Achat] est un JavaBean avec les champs et méthodes suivants :
| l'article acheté |
| la quantité achetée |
| rend le montant de l'achat |
| chaîne d'identité de l'objet |
La classe [Panier] représente l'ensemble des achats du client :
| package istia.st.articles.domain;
// Imports
import java.util.ArrayList;
public class Panier {
// Champs
private ArrayList achats;
// Constructeurs
public Panier() { }
// Méthodes
public ArrayList getAchats() {}
public void ajouter(Achat unAchat) { }
public void enlever(int idAchat) { }
public double getTotal() {}
public String toString() { }
}
|
La classe [Panier] est un JavaBean avec les champs et méthodes suivants :
| la liste des achats du client - liste d'objets de type [Achat] |
void ajouter(Achat unAchat)
| ajoute un achat à la liste des achats |
void enlever(int idArticle)
| enlève l'achat de l'article idArticle |
| rend le montant total des achats |
| rend la chaîne d'identité du panier |
| rend la liste des achats |
Le code du paquetage [domain] sera trouvé en annexe.
3.3.5. Le paquetage [istia.st.articles.exception]
Ce paquetage contient la classe définissant l'exception lancée par la couche [dao] lorsqu'elle rencontre un problème d'accès à la source de données :
| 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. Tests du modèle
Le modèle M a été testé sous Eclipse avec la configuration suivante :

Commentaires :
- dans [WEB-INF/lib] on trouve :
- les archives nécessaires à l'outil [ibatis SqlMap] chargé d'accéder au SGBD Firebird : ibatis-*.jar
- celle nécessaire à l'outil [spring] : spring.jar
- le pilote JDBC du SGBD [Firebird] : firebirdsql-full.jar
- les archives nécessaires aux logs : log4-*.jar, commons-logging.jar
- les trois archives du modèle testé : istia.st.articles.*.jar
- l'archive nécessaire à l'outil de test [junit]
- dans [WEB-INF/src] on trouve les fichiers de configuration qui seront automatiquement recopiés dans [WEB-INF/classes] par Eclipse :
- les fichiers de configuration de l'outil [sqlmap] : sqlmap-config-firebird.xml, articles.xml
- celui de l'outil [spring] : spring-config-test-dao.xml, spring-config-test-domain.xml
- le fichier de configuration de l'outil [log4j] : log4j.properties
- dans le paquetage [istia.st.articles.tests], on trouve les classes de tests du modèle
3.3.6.1. Tests de la couche [dao]
La classe de test JUnit de la couche [dao] est la suivante. Sa lecture permet de comprendre comment sont utilisées les méthodes de l'interface [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 de la classe ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {
// 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");
}
public void testGetAllArticles() {
// affiche les articles
listArticles();
}
public void testClearAllArticles() {
// vide la table des articles
articlesDao.clearAllArticles();
// lit la table ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
}
public void testAjouteArticle() {
// suppression du contenu de ARTICLES
articlesDao.clearAllArticles();
// lit la table ARTICLES
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));
// lit la table ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
//l'affiche
listArticles();
}
public void testSupprimeArticle() {
// suppression du contenu de ARTICLES
articlesDao.clearAllArticles();
// lit la table ARTICLES
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));
// lit la table ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// suppression
articlesDao.supprimeArticle(4);
// lit la table ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(1, articles.size());
// affiche la table
listArticles();
}
public void testModifieArticle() {
// suppression du contenu de ARTICLES
articlesDao.clearAllArticles();
// lit la table ARTICLES
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));
// lit la table ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
// modification
articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
// getById
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getPrix(), 44, 1e-6);
// affiche la table
listArticles();
}
public void testGetArticleById() {
// suppression du contenu de ARTICLES
articlesDao.clearAllArticles();
// lit la table ARTICLES
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));
// lit la table ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
private void listArticles() {
// lit la table ARTICLES
List articles = articlesDao.getAllArticles();
// on affiche les articles lus
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
public void testChangerStockArticle() throws InterruptedException {
// suppression du contenu de 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);
// création de 100 threads de mise à jour du stock de l'article 3
Thread[] taches = new Thread[100];
for (int i = 0; i < taches.length; i++) {
taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
taches[i].start();
}
// on attend la fin des threads
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// récupérer l'article 3 et vérifier son 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);
// affiche la table
listArticles();
}
}
|
Commentaires :
- la classe de test mémorise, grâce à sa méthode setUp, une instance de la classe à tester :
| // 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'objet à tester est fourni par [Spring]. Ci-dessus, on demande le bean spring nommé [articlesDao]. Ce bean est défini dans le fichier de configuration de Spring [spring-config-test-dao.xml] :
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la classe d'accès aux données -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
</beans>
|
On voit ci-dessus, que le bean [articlesDao] est une instance de la classe [istia.st.articles.dao.ArticlesDaoSqlMap]. Cette classe a un constructeur qui attend comme paramètre, le nom du fichier de configuration de l'outil [SqlMap]. Ce nom lui est ici fourni. C'est [sqlmap-config-firebird.xml]. Ce dernier a déjà été décrit. Il donne toutes les informations nécessaires pour accéder aux données du SGBD.
La méthode [testChangerStockArticle] crée 100 threads chargés de décrémenter le stock d'un article donné. Il s'agit ici de tester les accès concurrents au SGBD. Parce que la méthode [changerStockArticle] de la classe [istia.st.articles.dao.ArticlesDaoSqlMap] a été synchronisée, ce test passe. Si on enlève la synchronisation, il ne passe plus. La classe chargée de mettre à jour le stock est la suivante :
| 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() {
// suivi
System.out.println(name + " lancé");
// modification stock article 3
articlesDao.changerStockArticle(3, -1);
// suivi
System.out.println(name + " terminé");
}
}
|
- la classe ci-dessus décrémente de 1 le stock de l'article n° 3
3.3.6.2. Tests de la couche [domain]
La classe de test JUnit de la couche [domain] est la suivante :
| 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 de la classe ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {
// 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");
}
// récupération d'un article particulier
public void testGetArticleById() {
// suppression du contenu de ARTICLES
articlesDao.clearAllArticles();
// lit la table ARTICLES
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));
// lit la table ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDomain.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
// affichage écran
private void listArticles() {
// lit la table ARTICLES
List articles = articlesDomain.getAllArticles();
// on affiche les articles lus
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
// achats d'articles
public void testAchatPanier(){
// suppression du contenu de ARTICLES
articlesDao.clearAllArticles();
// lit la table ARTICLES
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);
// lit la table ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// création d'un panier avec deux achats
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// vérifications
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// validation panier
articlesDomain.acheter(panier);
// vérifications
assertEquals(0,articlesDomain.getErreurs().size());
assertEquals(0,panier.getAchats().size());
// rechercher article n° 3
article3=articlesDomain.getArticleById(3);
assertEquals(20,article3.getStockActuel());
// rechercher article n° 4
article4=articlesDomain.getArticleById(4);
assertEquals(30,article4.getStockActuel());
// nouveau panier
panier.ajouter(new Achat(article3,100));
// validation panier
articlesDomain.acheter(panier);
// vérifications - on a trop acheté
// on doit avoir une erreur
assertEquals(1,articlesDomain.getErreurs().size());
// rechercher article n° 3
article3=articlesDomain.getArticleById(3);
// son stock n'a pas du changer
assertEquals(20,article3.getStockActuel());
}
// retirer des achats
public void testRetirerAchats(){
// suppression du contenu de ARTICLES
articlesDao.clearAllArticles();
// lit la table ARTICLES
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);
// lit la table ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// création d'un panier avec deux achats
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// vérifications
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// ajouter un article déjà acheté
panier.ajouter(new Achat(article3,10));
// vérifications
// le total doit être passé à 1000
assertEquals(1000.0,panier.getTotal(),1e-6);
// toujours 2 articles dans le panier
assertEquals(2,panier.getAchats().size());
// qté article 3 a du passer à 20
Achat achat=(Achat)panier.getAchats().get(0);
assertEquals(20,achat.getQte());
// on retire l'article 3 du panier
panier.enlever(3);
// vérifications
// le total doit être passé à 400
assertEquals(400.0,panier.getTotal(),1e-6);
// 1 seul article dans le panier
assertEquals(1,panier.getAchats().size());
// ce doit être l'article n° 4
assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
}
}
|
Commentaires :
- la classe de test mémorise, grâce à sa méthode setUp, une instance de la classe à tester ainsi qu'une instance de la classe d'accès aux données. Ce dernier point est litigieux. La classe de test ne devrait théoriquement pas avoir besoin d'avoir accès à la couche [dao] qu'elle n'est même pas censée connaître. Ici, nous sommes passés outre cette " éthique " qui, pour être respectée, nous aurait obliger à créer de nouvelles méthodes dans notre interface [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'objet à tester est fourni par [Spring]. Ci-dessus, on demande le bean spring nommé [articlesDomain]. Ce bean est défini dans le fichier de configuration de Spring [spring-config-test-domain.xml] :
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la classe d'accès aux données -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- la classe métier -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
On voit ci-dessus, que le bean [articlesDomain] est une instance de la classe [istia.st.articles.domain.AchatsArticles]. Cette classe a un constructeur qui attend comme paramètre, un objet d'accès à la couche [dao] de type [IArticlesDao]. Ici, le fichier de configuration indique que cet objet est le bean nommé [articlesDao]. Ceci oblige Spring à instancier ce bean. L'instanciation du bean [articlesDao] a été expliqué précédemment. Donc au final, deux beans ont été instanciés :
- [articlesDao] de type [istia.st.articles.dao.ArticlesDaoSqlMap]
- [articlesDomain] de type [ istia.st.articles.domain.AchatsArticles]
Ces deux instanciations sont provoquées par le premier appel à Spring :
| // récupère une instance d'accès au domaine
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
|
On récupère alors le bean [articlesDomain]. Lors du second appel à 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] se contente de rendre une référence au bean [articlesDao] qui a déjà été créé lors de l'appel précédent. C'est le principe du singleton. Si on demande un bean à Spring, il l'instancie s'il n'existe pas déjà sinon il rend une référence au bean existant.
3.4. Application web MVC à trois couches
Dans la suite, nous voulons construire l'application web à trois couches suivante :
L'application aura une architecture MVC. Le modèle M a été écrit et testé. C'est celui décrit précédemment. Il nous est fourni dans trois archives [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Il nous faut écrire le contrôleur C et les vues V.
Nous considérons tout d'abord, une méthode classique, celle où :
- le contrôleur C est assuré par une servlet unique
- les vues V sont assurées par des pages JSP
3.5. Architecture MVC à base d'une servlet contrôleur et de pages JSP
L'architecture MVC de l'application sera la suivante :
| les classes métier, les classes d'accès aux données et la base de données |
| les pages JSP |
| la servlet de traitement des requêtes clientes |
3.5.1. Le modèle
Il a été présenté précédemment. Il est constitué des archives Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.5.2. Les vues
Les vues correspondent à celles qui ont été présentées en début de document :
| liste.jsp | Les vues sont rassemblées dans le dossier [vues] de l'application  |
| infos.jsp |
| panier.jsp |
| paniervide.jsp |
| erreurs.jsp |
3.5.3. Le contrôleur
Le contrôleur sera formé d'une servlet unique appelée [WebArticles]. Il traitera les différentes demandes des clients. Celles-ci seront formalisées par la présence d'un paramètre [action] dans la requête HTTP du client :
| signification | action du contrôleur | réponses possibles |
| le client veut la liste des articles | - demande la liste des articles à la couche métier | - [LISTE] - [ERREURS] |
| le client demande des nformations sur l'un des articles affichés dans la vue [LISTE] | - demande l'article à la couche métier | - [INFOS] - [ERREURS] |
| le client achète un article | - demande l'article à la couche métier et l'intègre dans le panier du client | - [INFOS] si erreur de qté - [LISTE] si pas d'erreur |
| le client veut supprimer un achat de son panier | - récupère le panier dans la session et le modifie | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| le client veut visualiser son panier | - récupère le panier dans la session | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| le client a terminé ses achats et passe à la phase paiement | - met à jour dans la base les stocks des articles achetés - vide le panier du client des articles dont l'achat a été validé | - [LISTE] - [ERREURS] |
3.5.4. Configuration de l'application
Nous chercherons à configurer l'application de façon à la rendre la plus souple possible vis à vis de changements tels que :
- le changement des url des différentes vues
- le changement des classes implémentant les interfaces [IArticlesDao] et [IArticlesDomain]
- le changement du SGBD, de la base, de la table des articles
3.5.5. Les changements d'url
Les noms des url des vues seront placés dans le fichier [web.xml] de configuration de l'application avec quelques autres paramètres :
| <?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>
|
On trouve dans [web.xml]
- les url des différentes vues de l'application
- le nom [springConfigFileName] du fichier de configuration de Spring qui va permettre la création des objets singleton d'accès aux couches métier et Dao
- la vue [/vues/index.jsp] qui sera affichée lorsque l'url demandée par le client sera /<context>où <context> est le contexte de l'application
3.5.6. Le changement des classes d'implémentation des interfaces
Dans l'esprit des architectures à trois couches, les couches doivent être étanches les unes par rapport aux autres. Cette étanchéité est obtenue de la façon suivante :
- les couches communiquent entre-elles par des interfaces et non par des classes concrètes
- le code d'une couche n'instancie jamais elle-même la classe d'une autre couche afin de l'utiliser. Elle demande simplement à un outil externe, ici [Spring], une instance d'implémentation de l'interface de la couche qu'elle veut utiliser. Pour cela, nous savons qu'elle n'a pas besoin de connaître le nom de la classe d'implémentation mais seulement le nom du bean Spring dont elle veut une référence.
Dans notre application, le fichier de configuration Spring pourrait être le suivant :
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la classe d'accès aux données -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- la classe métier -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Pour avoir accès à la couche [métier], une classe de la couche [Interface utilisateur, UI] pourra demander le bean [articlesDomain]. Spring instanciera alors un objet de type [istia.st.articles.domain.AchatsArticles]. Pour cette instanciation, il a besoin d'un bean de type [articlesDao], c'est à dire d'un objet de type [istia.st.articles.dao.ArticlesDaoSqlMap]. Spring instanciera alors un tel objet. Cette instanciation se fera à partir des informations contenues dans le fichier [sqlmap-config-firebird.xml], fichier de configuration d'un accès aux données par SqlMap. A la fin de l'opération, la classe [UI] qui a demandé le bean [articlesDomain] a toute la chaîne qui la relie aux données du SGBD :
3.5.7. Les changement liés au SGBD ou à la base des données
L'indépendance de l'application web vis à vis des changements liés au SGBD ou à la base est assurée ici par les fichiers de configuration de SqlMap. Il y en a deux :
- le fichier [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>
|
Ce fichier désigne ici une base de données Firebird. Il suffit de changer le nom du pilote JDBC pour travailler avec un autre SGBD.
- le fichier [articles.xml] qui rassemble les différentes instructions SQL nécessaires à l'application :
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- un alias sur la classe istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- le mapping ORM : ligne table ARTICLES - instance classe 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>
<!-- la requête SQL pour obtenir tous les articles -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- la requête SQL pour supprimer tous les articles -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- la requête SQL pour insérer un article -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>
<!-- la requête SQL pour supprimer un article donné -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- la requête SQL pour modifier un article donné -->
<statement id="modifyArticle">
update ARTICLES set nom=#nom#,
prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
id=#id#
</statement>
<!-- la requête SQL pour obtenir un article donné -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- la requête SQL pour modifier le stock d'un article donné -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#mouvement#
where id=#id# and stockActuel+#mouvement#>=0
</statement>
</sqlMap>
|
Si les noms de la table des articles ou des colonnes venaient à changer, on serait amené à réécrire les requêtes dans ce fichier de configuration sans avoir à changer le code Java. Ce serait le cas également si une requête venait à être remplacée par une procédure stockée pour des raisons de performances.
3.5.8. L'architecture globale de l'application [webarticles]
Une application web Java est un puzzle avec de nombreux éléments. Lui donner une architecture MVC augmente en général le nombre de ceux-ci. La structure de l'application [webarticles] sous [eclipse] est la suivante :
structure générale - on voit ci-dessous les
archives Java utilisées par le projet
Eclipse.
spring : pour Spring
ibatis : pour SqlMap
log4j, commons-logging : pour les logs
de Spring et Sqlmap
firebird : pour le SGBD firebird
mysql : pour le SGBD MySQL
jstl, standard : pour la bibliothèque
de balises JSTL
|
le dossier des sources java : contient le code Java
ainsi que les fichiers de configuration
spring et sqlmap. Eclipse recopie
automatiquement ces fichiers
dans [WEB-INF/classes].
C'est là que l'application les trouvera.
|
 |  |
le dossier [WEB-INF] de l'application : contient le
descripteur [web.xml] de l'application ainsi que le
fichiers de définition de la bibliothèques JSTL
| |
 |  |
3.5.9. Les vues JSP
Les vues JSP utilisent la bibliothèque de balises JSTL.
3.5.9.1. entete.jsp
Afin de donner une certaine homogénéité aux différentes vues, celles-ci partageront un même entête, celui qui affiche le nom de l'application avec le menu :
Le menu est dynamique et fixé par le contrôleur. Celui-ci met dans la requête transmise à la page JSP, un attribut de clé "actions" ayant pour valeur associée, un tableau de type Hastable[]. Chaque élément de ce tableau est un dictionnaire destiné à générer une option du menu de l'entête. Chaque dictionnaire a deux clés :
- href : l'url associée à l'option de menu
- lien : le texte du menu
Les autres vues de l'application utiliseront l'entête défini par [entete.jsp] à l'aide de la balise JSP suivante :
<jsp:include page="entete.jsp"/>
A l'exécution, cette balise aura pour effet d'inclure dans le code de la page JSP qui la contient, celui de la page [entete.jsp]. L'url de la page étant une url relative (absence de /), la page [entete.jsp] sera cherchée dans le même dossier que la page possédant la balise <jsp:include>.
Code :
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>webarticles</title>
</head>
<body>
<table>
<tr>
<td><h2>Magasin virtuel</h2></td>
<c:forEach items="${actions}" var="action">
<td>|</td>
<td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
</c:forEach>
</tr>
</table>
<hr>
|
3.5.9.2. liste.jsp
Cette vue affiche la liste des articles disponibles à la vente :
Elle est affichée à la suite d'une requête /main?action=liste ou /main?action=validationpanier. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
| ArrayList d'objets de type [Article] |
| objet String - message à afficher en bas de page |
Chaque lien [Infos] du tableau HTML des articles a une url de la forme [?action=infos&id=ID] où ID est le champ id de l'article affiché.
Code :
| <%@ 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
Cette vue affiche des informations sur un article et permet également son achat :

Elle est affichée à la suite d'une requête /main?action=infos&id=ID ou d'une requête /main?action=achat&id=ID lorsque la quantité achetée est erronée. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
| objet de type [Article] - article à afficher |
| objet String - message à afficher en cas d'erreur sur la quantité |
| objet String - valeur à afficher dans le champ de saisie [Qte] |
Les champs [msg] et [qte] sont utilisés en cas d'erreur de saisie sur la quantité :

Cette page contient un formulaire qui est posté par le bouton [Acheter]. L'url cible du POST est [?action=achat&id=ID] où ID est l'id de l'article acheté.
Code :
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Article d'id [<c:out value="${article.id}"/>]</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
</tr>
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><c:out value="${article.stockActuel}"/></td>
<td><c:out value="${article.stockMinimum}"/></td>
</tr>
</table>
<p>
<form method="post" action="?action=achat&id=<c:out value="${article.id}"/>"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
<td><c:out value="${msg}"/></td>
</tr>
</table>
</form>
</body>
</html>
|
3.5.9.4. panier.jsp
Cette vue affiche le contenu du panier :

Elle est affichée à la suite d'une requête /main?action=panier ou /main?action=retirerachat&id=ID. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
| objet de type [Panier] - le panier à afficher |
Chaque lien [Retirer] du tableau HTML des achats du panier a une url de la forme [?action=retirerachat&id=ID] où ID est le champ [id] de l'article qu'on veut retirer du panier.
Code :
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<table border="1">
<tr>
<td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
</tr>
<c:forEach var="achat" items="${panier.achats}">
<tr>
<td><c:out value="${achat.article.nom}"/></td>
<td><c:out value="${achat.qte}"/></td>
<td><c:out value="${achat.article.prix}"/></td>
<td><c:out value="${achat.total}"/></td>
<td><a href="<c:out value="?action=retirerachat&id=${achat.article.id}"/>">Retirer</a></td>
</tr>
</c:forEach>
</table>
<p>
Total de la commande : <c:out value="${panier.total}"/> euros
</body>
</html>
|
3.5.9.5. paniervide.jsp
Cette vue affiche l'information indiquant que le panier est vide :

Elle est affichée à la suite d'une requête /main?action=panier ou /main?action=retirerachat&id=ID. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
Code :
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<p>
Votre panier est vide.
</body>
</html>
|
3.5.9.6. erreurs.jsp
Cette vue est affichée en cas d'erreurs :

Elle est affichée à la suite de toute requête menant à une erreur sauf pour l'action d'achat avec une quantité erronée traitée par la vue [INFOS]. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
| ArrayList d'objets String représentant les messages d'erreurs à afficher |
Code :
| <%@ 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
Cette page est définie comme page d'accueil de l'application dans le fichier [web.xml] de l'application :
| <?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 vue [index.jsp] se contente de rediriger le client vers le point d'entrée de l'application :
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
|
3.5.10. Le contrôleur
Il reste à écrire le coeur de notre application web, le contrôleur. Son rôle consiste à :
- récupérer la requête du client,
- traiter l'action demandée par celui-ci à l'aide des classes métier,
- envoyer en réponse la vue appropriée.
3.5.10.1. Initialisation du contrôleur
Lorsque la classe du contrôleur est chargée par le serveur de servlets, sa méthode [init] est exécutée. Ce sera la seule fois. Une fois chargée en mémoire, le contrôleur y restera et traitera les requêtes des différents clients. Chaque client fait l'objet d'un thread d'exécution et les méthodes du contrôleur sont ainsi exécutées simultanément par différents threads. On rappelle que, pour cette raison, le contrôleur ne doit pas avoir de champs que ses méthodes pourraient modifier. Ses champs doivent être en lecture seule. Ils sont initialisés par la méthode [init] dont c'est le rôle principal. Cette méthode a en effet la particularité d'être exécutée une unique fois par un seul thread. Il n'y a donc pas de problèmes d'accès concurrents aux champs du contrôleur dans cette méthode. La méthode [init] a pour but d'initialiser les objets nécessaires à l'application web et qui seront partagés en lecture seule par tous les threads clients. Ces objets partagés peuvent être placés en deux endroits :
- les champs privés du contrôleur
- le contexte d'exécution de l'application (ServletContext)
La méthode [init] de l'application [webarticles] fera les actions suivantes :
- vérifiera la présence, dans le fichier [web.xml], des paramètres nécessaires au bon fonctionnement de l'application. Ceux-ci ont été décrits au paragraphe 3.5.5.
- positionnera un champ privé [ArrayList erreurs] avec la liste des erreurs éventuelles. Cette liste sera vide s'il n'y a pas d'erreurs mais elle existera néanmoins.
- s'il y a eu des erreurs, la méthode [init] s'arrête là. Sinon, elle crée un objet de type [IArticlesDomain] qui sera l'objet métier que le contrôleur utilisera pour ses besoins. Comme il a été expliqué en 3.5.6, le contrôleur demandera au framework Spring le bean dont il a besoin. Cette opération d'instanciation peut amener différentes erreurs. Si tel est le cas, elles seront, là encore, mémorisées dans le champ [erreurs] du contrôleur.
3.5.10.2. Méthodes doGet, doPost
Ces deux méthodes traitent les requêtes les requêtes HTTP GET et POST des clients. On traitera celles-ci indifféremment. La méthode [doPost] pourra ainsi renvoyer à la méthode [doGet] ou vice-versa. La requête client sera traitée de la façon suivante :
- le champ [erreurs] sera vérifié. S'il est non vide, cela signifie qu'il y a eu des erreurs lors de l'initialisation de l'application et que celle-ci ne peut pas fonctionner. On enverra alors, en réponse, la vue [ERREURS].
- le paramètre [action] de la requête sera récupéré et vérifié. S'il ne correspond pas à une action connue, la vue [ERREURS] est envoyée avec un message d'erreur approprié.
- si le paramètre [action] est valide, la requête du client est passée à une procédure spécifique à l'action pour traitement. La procédure traitant l'action [uneAction] aura pour signature :
| /**
* @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. Traitement des différentes actions
Les méthodes traitant les différentes actions possibles de l'application sont les suivantes :
méthode | demande | traitement | réponses possibles |
| GET /main?action=liste | - demander la liste des articles à la classe métier - l'afficher | [LISTE] ou [ERREURS] |
| GET /main?action=infos&id=ID | - demander l'article d'id=ID à la classe métier - l'afficher | [INFOS] ou [ERREURS] |
| POST /main?action=achat&id=ID - la qté achetée fait partie des paramètres postés | - demander l'article d'id=ID à la classe métier - l'inclure dans le panier dans la session client | [LISTE] ou [INFOS] ou [ERREURS] |
| GET /main?action=retirerachat&id=ID | - retirer l'article d'id=ID de la liste des achats du panier de la session client | [PANIER] |
| GET /main?action=panier | - faire afficher le panier de la session client | [PANIER] ou [PANIERVIDE] |
| GET /main?action=validationpanier | - décrémenter dans la base les stocks de tous les articles présents dans le panier de session du client | [LISTE] ou [ERREURS] |
3.5.10.4. Le code
| 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 {
// champs privés
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() {
// on récupère les paramètres d'initialisation de la servlet
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// on mémorise l'erreur
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// des erreurs ?
if (erreurs.size() != 0) {
return;
}
// on crée un objet IArticlesDomain d'accès à la couche métier
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// on mémorise l'erreur
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// on mémorise certaines url de l'application
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);
// c'est fini
return;
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// on vérifie comment s'est passée l'initialisation de la servelet
if (erreurs.size() != 0) {
// a-t-on l'url de la page d'erreurs ?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// on affiche la page des erreurs
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fin
return;
}
// on traite l'action
String action = request.getParameter("action");
if (action == null) {
// liste des articles
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// liste des articles
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// infos sur un article
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// achat d'un article
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// affichage du panier
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// suppression d'un article du panier
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// validation du panier
doValidationPanier(request, response);
return;
}
// action inconnue
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// on affiche la page des erreurs
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// fin
return;
}
private void doValidationPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// l'acheteur a confirmé son panier
Panier panier = (Panier) request.getSession().getAttribute("panier");
// on valide ce panier
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// pas normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// on récupère les erreurs
ArrayList erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionPanier });
afficheErreurs(request, response, erreurs);
return;
}
// on affiche la liste des articles
request.setAttribute("message", "Votre panier a été validé");
doListe(request, response);
// fin
return;
}
private void doRetirerAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// on retire un achat du panier
try {
Panier panier =
(Panier) request.getSession().getAttribute("panier");
String strIdAchat = request.getParameter("id");
panier.enlever(Integer.parseInt(strIdAchat));
} catch (NumberFormatException ignored) {
} catch (NullPointerException ignored) {
}
// on affiche le panier
doPanier(request, response);
}
private void doPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// on affiche le panier
Panier panier = (Panier) request.getSession().getAttribute("panier");
// panier vide ?
if (panier == null || panier.getAchats().size() == 0) {
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
.forward(request, response);
// fin
return;
}
// il y a qq chose dans le panier
request.setAttribute("panier", panier);
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionValidationPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER))
.forward(request, response);
// fin
return;
}
private void doAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// achat d'un article
// on récupère la quantité
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"));
String url =
config.getInitParameter(URL_MAIN)
+ "?action=infos&id="
+ request.getParameter("id");
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// fin
return;
}
// on récupère la session du client
HttpSession session = request.getSession();
// on crée l'achat
Article article = (Article) session.getAttribute("article");
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);
// on revient à la liste des articles
String url = config.getInitParameter(URL_MAIN) + "?action=liste";
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// fin
return;
}
private void afficheDebugInfos(
HttpServletRequest request,
HttpServletResponse response,
ArrayList infos)
throws ServletException, IOException {
// on affiche la liste des articles
request.setAttribute("infos", infos);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_DEBUG))
.forward(request, response);
// fin
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 {
// la liste des erreurs
ArrayList erreurs = new ArrayList();
// on récupère l'id demandé
String strId = request.getParameter("id");
// qq chose ?
if (strId == null) {
// pas normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// on transforme strId en entier
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// pas normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// on demande l'article de clé id
Article article = null;
try {
article=articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// pas 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) {
// pas normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// on met l'article dans la session
request.getSession().setAttribute("article", article);
// on affiche la page d'infos
request.setAttribute("actions", new Hashtable[] { hActionListe });
// request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_INFOS))
.forward(request, response);
// fin
return;
}
private void afficheErreurs(
HttpServletRequest request,
HttpServletResponse response,
ArrayList erreurs)
throws ServletException, IOException {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreurs);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fin
return;
}
private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// 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());
}
// des erreurs ?
if (erreurs.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fin
return;
}
// on affiche la liste des articles
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// fin
return;
}
/**
* suivi console pour débogage
* @param message : le message à afficher
*/
private void affiche(String message) {
System.out.println(message);
}
}
|
Nous laissons le lecteur prendre son temps pour lire et comprendre ce code. Nous espérons que les commentaires l'y aideront.
3.5.10.5. Tests de l'application
Montrons quelques copies d'écran de tests. Tout d'abord, la page d'accueil de l'application :

L'url demandée était en réalité [http://localhost:8080/webarticles]. Le lecteur verra que dans le fichier [web.xml], nous définissons une page d'accueil pour l'application :
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
La vue [index.jsp] est définie comme suit :
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
Il y a donc eu redirection vers l'url [http://localhost:8080/webarticles/main?action=liste], ce que nous montre l'url du navigateur sur la copie d'écran. L'url [/main?action=liste] a donc été demandée. Toujours dans [web.xml], l'url /main est associée à la servlet [webarticles] :
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
Toujours dans [web.xml], la servlet [webarticles] est associée à la servlet [ istia.st.articles.web.WebArticles] :
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
La servlet [ istia.st.articles.web.WebArticles] est donc chargée par le conteneur de servlets Tomcat si elle ne l'était pas encore et sa méthode [init] exécutée :
| public void init() {
// on récupère les paramètres d'initialisation de la servlet
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// on mémorise l'erreur
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// des erreurs ?
if (erreurs.size() != 0) {
return;
}
// on crée un objet IArticlesDomain d'accès à la couche métier
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// on mémorise l'erreur
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// on mémorise certaines url de l'application
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);
// c'est fini
return;
}
|
Commentaires : la méthode [init]
- vérifie la présence de certains paramètres de configuration
- instancie un service d'accès au domaine de l'application grâce à Spring
- positionne une liste d'erreurs pour indiquer d'éventuelles erreurs d'initialisation
- un certain nombre de champs privés :
- erreurs : la liste des erreurs détectées par [init]
- [hActionListe, hActionPanier, hActionValidationPanier] : dictionnaires. Chacun d'eux détient les informations nécessaires pour afficher une option du menu principal affiché par la vue [entete.jsp]
- acticlesDomain : le service d'accès au modèle de l'application
La méthode [init] n'est exécutée qu'une fois, au chargement initial de la servlet. Ensuite, l'une des méthodes [doGet, doPost] est exécutée selon le type [GET, POST] de la requête du client. Ici, les deux méthodes font la même chose et le code a été placé dans [doGet] :
| public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// on vérifie comment s'est passée l'initialisation de la servelet
if (erreurs.size() != 0) {
// a-t-on l'url de la page d'erreurs ?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// on affiche la page des erreurs
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fin
return;
}
// on traite l'action
String action = request.getParameter("action");
if (action == null) {
// liste des articles
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// liste des articles
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// infos sur un article
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// achat d'un article
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// affichage du panier
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// suppression d'un article du panier
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// validation du panier
doValidationPanier(request, response);
return;
}
// action inconnue
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// on affiche la page des erreurs
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// fin
return;
}
|
- la méthode [doGet] commence par vérifier s'il y a eu des erreurs d'initialisation à l'issue de la méthode [init]. Si oui, elle fait afficher la vue [ERREURS] et c'est fini.
- Sinon, elle récupère le paramètre [action] dans la requête du client. Rappelons que l'application a été construite pour répondre à des requêtes où se trouve obligatoirement un paramètre [action].
- Elle fait exécuter la méthode liée à l'action. Ici de sera la méthode [doListe].
La méthode [doListe] est la suivante :
| private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// 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());
}
// des erreurs ?
if (erreurs.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fin
return;
}
// on affiche la liste des articles
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// fin
return;
}
|
- rappelons que la méthode [init] a mémorisé le service d'accès au modèle de l'application (couche domain) dans un champ privé de la servlet :
// champs privés
private IArticlesDomain articlesDomain = null;
- à partir de ce service d'accès, on peut demander la liste des articles :
| // 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());
}
|
- s'il se produit des erreurs, la vue [ERREURS] est envoyée :
| // des erreurs ?
if (erreurs.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fin
return;
}
|
- sinon c'est la vue [LISTE] qui est envoyée :
| // on affiche la liste des articles
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
|
Dans notre exemple, tout s'est bien passé et nous avons bien obtenu la vue [LISTE]. Le lecteur est invité à relire le code de la vue [LISTE] afin de découvrir que les paramètres dynamiques attendus par cette vue sont bien fournis ci-dessus par le contrôleur. Le même type de vérification est à faire pour chaque vue :
- retrouver les paramètres dynamiques de la vue
- s'assurer que le contrôleur met bien ceux-ci dans les attributs de la requête transmise à la vue
Nous donnons maintenant simplement le cheminement des écrans rencontrés par un utilisateur de l'application. Le lecteur est invité à refaire à chaque fois un raisonnement analogue au précédent :
A partir de la liste des articles, l'utilisateur peut choisir un article :
L'acheteur peut acheter ici l'article n° 3. Faisons une erreur de saisie sur la quantité :
L'erreur a été signalée. Maintenant, acheton quelques articles :
L'achat a été enregistré et la liste des articles réaffichée. Vérifions le panier :
L'achat est bien dans le panier. Retirons-le :
L'achat a été retiré du panier et ce dernier réaffiché. Ici, il est vide.
Achetons 100 articles n° 3 et 2 articles n° 4 :
L'achat de l'article n° 3 s'est révélé impossible car on voulait en acheter 100 et il n'y en avait que 30 en stock. Cet achat est resté dans le panier :
L'article n° 4 a lui été acheté comme le montre son nouveau stock égal à 39 (40-1) :
3.6. Architecture MVC avec Struts
3.6.1. Architecture générale de l'application
Revenons sur l'architecture MVC de l'application :
Dans la version précédente :
- le contrôleur était assuré par une servlet
- les vues étaient assurées par des pages JSP
- le modèle était assuré par un ensemble de trois archives .jar
Dans la version Struts :
- le contrôleur sera assuré par une servlet dérivée de la servlet générique [ActionServlet] de Struts
- les vues seront assurées par les mêmes pages JSP que précédemment à quelques détails près
- le modèle sera assuré par les mêmes trois archives
Nous allons découvrir que le passage à Struts de l'application précédente consiste en les tâches suivantes :
- les actions qui étaient traitées dans des méthodes particulières de la servlet/contrôleur sont maintenant traitées par des instances de classes dérivées de la classe [Action] de Struts
- écrire les fichiers de configuration [web.xml] et [struts-config.xml]
- amener quelques modifications aux pages JSP
Rappelons l'architecture générique MVC utilisée par STRUTS :
| les classes métier, les classes d'accès aux données et la base de données |
| les pages JSP |
| la servlet de traitement des requêtes clientes, les objets [Action] et les beans [ActionForm] associés aux formulaires. |
- le contrôleur est le coeur de l'application. Toutes les demandes du client transitent par lui. C'est une servlet générique fournie par STRUTS. On peut dans certains cas être amené à la dériver. Pour les cas simples, ce n'est pas nécessaire. Cette servlet générique prend les informations dont elle a besoin dans un fichier le plus souvent appelé struts-config.xml.
- si la requête du client contient des paramètres de formulaire, ceux-ci sont mis par le contrôleur dans un objet Bean. Les objets bean ainsi créés au fil du temps sont stockés dans la session ou la requête du client. Ce point est configurable. Ils n'ont pas à être recréés s'ils l'ont déjà été.
- dans le fichier de configuration struts-config.xml, à chaque URL devant être traitée par programme (ne correspondant donc pas à une vue JSP qu'on pourrait demander directement) on associe certaines informations :
- le nom de la classe de type Action chargée de traiter la requête. Là encore, l'objet Action instancié peut être conservé dans la session ou la requête.
- si l'URL demandée est paramétrée (cas de l'envoi d'un formulaire au contrôleur), le nom du bean chargé de mémoriser les informations du formulaire est indiqué.
- muni de ces informations fournies par son fichier de configuration, à la réception d'une demande d'URL par un client, le contrôleur est capable de déterminer s'il y a un bean à créer et lequel. Une fois instancié, le bean peut vérifier que les données qu'il a stockées et qui proviennent du formulaire, sont valides ou non. Une méthode du bean appelée validate est appelée automatiquement par le contrôleur. Le bean est construit par le développeur. Celui-ci met donc dans la méthode validate le code vérifiant la validité des données du formulaire. Si les données se révèlent invalides, le contrôleur n'ira pas plus loin. Il passera la main à une vue dont il trouvera le nom dans son fichier de configuration. L'échange est alors terminé. On notera que le développeur peut demander à ce que la validité du formulaire ne soit pas vérifiée. Il fait cela également dans le fichier struts-config.xml. Dans ce cas, le contrôleur n'appelle pas la méthode validate du bean.
- si les données du bean sont correctes, ou s'il n'y a pas de vérification ou s'il n'y a pas de bean, le contrôleur passe la main à l'objet de type Action associé à l'URL. Il le fait en demandant l'exécution de la méthode execute de cet objet à laquelle il transmet la référence du bean qu'il a éventuellement construit. C'est ici que le développeur fait ce qu'il a à faire : il devra éventuellement faire appel à des classes métier ou à des classes d'accès aux données. A la fin du traitement, l'objet Action rend au contrôleur le nom de la vue qu'il doit envoyer en réponse au client.
- dans son fichier de configuration, le contrôleur trouvera l'URL associée au nom de la vue qu'on lui a demandé d'afficher. Il envoie alors cette dernière. L'échange avec le client est terminé.
Dans notre application, nous n'utiliserons pas d'objets [Bean] comme objets tampon entre le client et les classes [Action]. L'objet [Action] ira directement chercher les paramètres de la requête du client dans l'objet [HttpServletRequest] qu'elle recevra. Cela facilite le portage de notre application initiale. L'architecture finale de notre application sera donc la suivante :
| les classes métier, les classes d'accès aux données et la base de données |
| les pages JSP |
| la servlet de traitement des requêtes clientes, les objets [Action] |
3.6.2. Le modèle
Il a été présenté précédemment. Il est constitué des archives Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.6.3. Configuration de l'application
3.6.3.1. Architecture générale
L'architecture générale du projet Eclipse est la suivante :

3.6.3.2. Configuration de l'accès aux données
Etant donné, que l'interface d'accès aux données ne bouge pas, les fichiers de configuration associés sont les mêmes que dans la version précédente. Ils sont définis dans [WEB-INF/src] :

Sur la copie d'écran ci-dessus, les fichiers [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] sont ceux de la version précédente.
3.6.3.3. Le répertoire des archives
Dans [WEB-INF/lib], on trouve les mêmes archives que dans la version précédente, plus celle nécessaire à Struts :

3.6.3.4. Configuration de l'application
L'application est configurée à l'aide de deux fichiers : [web.xml, struts-config.xml] dans le dossier [WEB-INF] :

Le fichier [web.xml] est le suivant :
| <?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>
|
Que dit ce fichier ?
- la page d'accueil de l'application est [vues/index.jsp] (welcome-file)
- les demandes d'url de la forme *.do seront redirigées vers la servlet [strutswebarticles] (servlet-mapping)
- la servlet [strutswebarticles] est une instance de la classe [ istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
- cette servlet admet deux paramètres d'initialisation
- le nom du fichier de configuration de Struts (config)
- le nom du fichier de configuration de Spring (springConfigFileName)
Le fichier [struts-config.xml] est le suivant :
| <?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>
|
Que dit ce fichier de configuration ?
- que notre contrôleur traitera les url suivantes :
| pour afficher la liste des articles |
| pour afficher la liste des articles |
| pour afficher des informations sur un article donné |
| pour acheter un article donné |
| pour afficher le panier des achats |
| pour retirer un achat du panier |
| pour valider un panier d'achats |
- les actions précédentes correspondent une à une aux actions traitées par la servlet dans la version précédente. Pour chacune d'elles sont précisées les informations suivantes :
- le nom de la classe chargée de traiter cette action
- les réponses (= vues) possibles après le traitement de l'action. Une seule d'entre-elles sera choisie par le contrôleur.
- le nom d'un fichier de messages pour l'application (message-resources). Ici, le fichier existera mais sera vide. Il ne sera pas utilisé. Il doit être placé dans le [ClassPath] de l'application. Ici il sera placé dans [WEB-INF/classes]. Sous Eclipse, on obtient ce résultat en le plaçant dans [WEB-INF/src] :

3.6.4. Les vues JSP
Les vues JSP utilisées sont là aussi celles de la version précédente. Très peu de choses sont modifiées : il s'agit des url de la forme [?action=XX?id=YY& ...] qui deviennent [/XX.do?id=YY&....]. Nous reprenons ici des explications déjà données afin d'éviter à l'utilisateur de revenir en arrière. Il est important de comprendre que les informations transmises à la vue par le contrôleur sont exactement les mêmes dans les deux versions. Rien n'a été changé sur ce point.
3.6.4.1. entete.jsp
Afin de donner une certaine homogénéité aux différentes vues, celles-ci partageront un même entête, celui qui affiche le nom de l'application avec le menu :
Le menu est dynamique et fixé par le contrôleur. Celui-ci met dans la requête transmise à la page JSP, un attribut de clé "actions" ayant pour valeur associée, un tableau de type Hastable[]. Chaque élément de ce tableau est un dictionnaire destiné à générer une option du menu de l'entête. Chaque dictionnaire a deux clés :
- href : l'url associée à l'option de menu
- lien : le texte du menu
Les autres vues de l'application utiliseront l'entête défini par [entete.jsp] à l'aide de la balise JSP suivante :
<jsp:include page="entete.jsp"/>
A l'exécution, cette balise aura pour effet d'inclure dans le code de la page JSP qui la contient, celui de la page [entete.jsp]. L'url de la page étant une url relative (absence de /), la page [entete.jsp] sera cherchée dans le même dossier que la page possédant la balise <jsp:include>.
Code :
| <%@ 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>
|
Commentaires : pas de changements vis à vis de la version précédente
3.6.4.2. liste.jsp
Cette vue affiche la liste des articles disponibles à la vente :
Elle est affichée à la suite d'une requête /main.do ou /validerpanier.do. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
| ArrayList d'objets de type [Article] |
| objet String - message à afficher en bas de page |
Chaque lien [Infos] du tableau HTML des articles a une url de la forme [/infos.do?id=ID] où ID est le champ id de l'article affiché.
Code :
| <%@ 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>
|
Commentaires : un changement (mis en relief ci-dessus)
3.6.4.3. infos.jsp
Cette vue affiche des informations sur un article et permet également son achat :

Elle est affichée à la suite d'une requête /infos.do?id=ID ou d'une requête /achat.do?id=ID lorsque la quantité achetée est erronée. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
| objet de type [Article] - article à afficher |
| objet String - message à afficher en cas d'erreur sur la quantité |
| objet String - valeur à afficher dans le champ de saisie [Qte] |
Les champs [msg] et [qte] sont utilisés en cas d'erreur de saisie sur la quantité :

Cette page contient un formulaire qui est posté par le bouton [Acheter]. L'url cible du POST est [/achat.do?id=ID] où ID est l'id de l'article acheté.
Code :
| <%@ 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>
|
Commentaires : un changement (mis en relief ci-dessus)
3.6.4.4. panier.jsp
Cette vue affiche le contenu du panier :

Elle est affichée à la suite d'une requête /panier.do ou /retirerachat.do?id=ID. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
| objet de type [Panier] - le panier à afficher |
Chaque lien [Retirer] du tableau HTML des achats du panier a une url de la forme [retirerachat.do?id=ID] où ID est le champ [id] de l'article qu'on veut retirer du panier.
Code :
| <%@ 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>
|
Commentaires : un changement (mis en relief ci-dessus)
3.6.4.5. paniervide.jsp
Cette vue affiche l'information indiquant que le panier est vide :

Elle est affichée à la suite d'une requête /panier.do ou /retirerachat.do?id=ID. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
Code :
| <%@ 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>
|
Commentaires : pas de changements.
3.6.4.6. erreurs.jsp
Cette vue est affichée en cas d'erreurs :

Elle est affichée à la suite de toute requête menant à une erreur sauf pour l'action d'achat avec une quantité erronée traitée par la vue [INFOS]. Les éléments de la requête du contrôleur sont les suivants :
| objet Hashtable[] - le tableau des options du menu |
| ArrayList d'objets String représentant les messages d'erreurs à afficher |
Code :
| <%@ 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>
|
Commentaires : pas de changements.
3.6.4.7. index.jsp
Cette page est définie comme page d'accueil de l'application dans le fichier [web.xml] de l'application :
| <?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 vue [index.jsp] se contente de rediriger le client vers le point d'entrée de l'application :
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
Commentaires : un changement (mis en relief ci-dessus)
3.6.5. Le contrôleur Struts
Struts dispose d'un contrôleur générique appelé [ActionServlet]. On sait qu'une servlet dispose d'une méthode [init] qui permet d'initialiser l'application lorsque celle-ci démarre. Si nous utilisons le contrôleur générique de Struts [ActionServlet], nous n'avons pas accès à sa méthode [init]. Ici, nous avons des choses à faire au démarrage de l'application, essentiellement instancier un objet d'accès au modèle. Aussi avons-nous besoin d'une méthode [init]. Nous dérivons donc la classe [Actionservlet] dans la classe [MainServlet] suivante :
| 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 {
// champs privés
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 classe parent
super.init();
// on récupère les paramètres d'initialisation de la servlet
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// on mémorise l'erreur
erreurs.add("Paramètre [" + parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// des erreurs ?
if (erreurs.size() != 0) {
return;
}
// on crée un objet IArticlesDomain d'accès à la couche métier
try {
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource((String) config
.getInitParameter(SPRING_CONFIG_FILENAME))))
.getBean("articlesDomain");
} catch (Exception ex) {
// on mémorise l'erreur
erreurs.add("Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// on mémorise certaines url de l'application
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);
// c'est fini
return;
}
}
|
Commentaires :
- l'intérêt de la classe est dans sa méthode [init] et ses champs privés
- la méthode [init] fait la même chose que la méthode [init] du contrôleur de la version précédente :
- elle vérifie la présence de certains paramètres de configuration
- elle instancie un service d'accès au domaine de l'application grâce à Spring
- elle positionne une liste d'erreurs pour indiquer d'éventuelles erreurs d'initialisation
- avant de commencer à travailler, la méthode [init] appelle la méthode [init] de la classe parent [ActionServlet]. C'est elle qui va exploiter le fichier de configuration de Struts [struts-config.xml].
- un certain nombre de champs privés avec leurs accesseurs sont définis :
- erreurs : la liste des erreurs détectées par [init]
- [hActionListe, hActionPanier, hActionValidationPanier] : dictionnaires. Chacun d'eux détient les informations nécessaires pour afficher une option du menu principal affiché par la vue [entete.jsp]
- acticlesDomain : le service d'accès au modèle de l'application
- le contrôleur d'une application Struts est accessible aux classes [Action] chargées de traiter les différentes actions possibles. Ces classes auront accès aux champs privés précédents car ceux-ci sont pourvus d'accesseurs publics.
L'instanciation de ce contrôleur est assurée par le fichier [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>
|
Toute URL se terminant par .do, sera traitée par une instance de la classe [istia.st.articles.web.struts.MainServlet]
3.6.6. Les actions de l'application Struts
3.6.6.1. Introduction
Chaque action Struts va faire l'objet d'une classe. Dans la version précédente, chaque action faisait l'objet d'une méthode dans le contrôleur de l'application. L'écriture de la classe [Action] consiste le plus souvent :
- à faire un copier/coller de la méthode qui avait été utilisée dans la version précédente
- adapter le code aux conventions Struts
3.6.6.2. main.do, liste.do
Ces deux actions sont identiques et définies dans [struts-config.xml] par :
| <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>
|
Lorsque l'une de ces actions [main.do, liste.do] est exécutée dans un navigateur, on obtient le résultat suivant :

Le code de la classe [ListeArticlesAction] est le suivant :
| 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 {
// la servlet de contrôle
MainServlet mainServlet = (MainServlet) this.getServlet();
// erreurs d'initialisation ?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// l'objet d'accès au domaine
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// 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());
}
// des erreurs ?
if (erreurs.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// on affiche la liste des articles
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
}
}
|
Commentaires :
- écrire le code d'une classe [Action] consiste essentiellement à écrire le code de sa méthode [execute]
- un certain nombre d'informations ont été mémorisées dans l'instance du contrôleur. On récupère une référence sur celle-ci par :
// la servlet de contrôle
MainServlet mainServlet = (MainServlet) this.getServlet();
- on récupère la liste des erreurs d'initialisation mémorisées par le contrôleur. Si cette liste est non vide, la vue [ERREURS] est envoyée au client :
| // erreurs d'initialisation ?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
|
La vue qui sera réellement envoyée au client nous est fournie par [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>
Il s'agit de la vue [/vues/erreurs.jsp]. Le lecteur est invité à vérifier ce qui est attendu par cette vue. Ces informations sont ici fournies par l'action dans l'objet [request] en tant qu'attributs.
- toujours grâce au contrôleur, l'action peut récupérer l'objet d'accès au modèle de l'application (couche domain) :
// l'objet d'accès au domaine
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
- ceci fait, on peut demander la liste des articles :
| // 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());
}
|
- s'il se produit des erreurs, la vue [ERREURS] est envoyée :
| // des erreurs ?
if (erreurs.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- sinon c'est la vue [LISTE] qui est envoyée :
| // on affiche la liste des articles
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
|
La vue qui sera réellement envoyée au client nous est fournie par [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>
Il s'agit de la vue [/vues/liste.jsp]. Le lecteur est invité à vérifier ce qui est attendu par cette vue. Ces informations sont ici fournies par l'action dans l'objet [request] en tant qu'attributs.
3.6.6.3. infos.do
Cette action sert à fournir de l'information sur l'un des articles affichés dans la vue [LISTE] :
Cette action est configurée de la façon suivante dans [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>
Le code de la classe [InfosArticleAction] est le suivant :
| 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 {
// la servlet de contrôle
MainServlet mainServlet = (MainServlet) this.getServlet();
// erreurs d'initialisation ?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// l'objet d'accès au domaine
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// la liste des erreurs
ArrayList erreurs = new ArrayList();
// on récupère l'id demandé
String strId = request.getParameter("id");
// qq chose ?
if (strId == null) {
// pas normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// on transforme strId en entier
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// pas normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// on demande l'article de clé id
Article article = null;
try {
article = articlesDomain.getArticleById(id);
} 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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
if (article == null) {
// pas normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// on met l'article dans la session
request.getSession().setAttribute("article", article);
// on affiche la page d'infos
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
}
|
Commentaires :
- le début de la méthode [execute] est identique à celle étudiée précédemment. Ce sera également le cas pour les autres actions.
- la méthode récupère le paramètre [id] qui doit normalement se trouver dans l'url. Celle-ci doit être en-effet de la forme [/infos.do?id=X]. Différents tests sont faits pour tester la présence et la validité du paramètre [id]. En cas de problème, la vue [ERREURS] est envoyée.
- si [id] est valide, l'article correspondant est demandé à la couche [domain]. Si celle-ci lance une exception ou si l'article n'est pas trouvé, là encore la vue [ERREURS] est envoyée.
- si tout va bien, l'article obtenu est mis dans la session. C'est un point discutable. Ici, on part sur le principe que le client va peut-être acheter cet article. S'il le fait, on ira le chercher dans la session, plutôt que de le demander de nouveau à la couche [domain].
- enfin, la vue [INFOS] est affichée. La vue qui sera réellement envoyée au client nous est fournie par [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 s'agit de la vue [/vues/infos.jsp]. Le lecteur est invité à vérifier ce qui est attendu par cette vue. Ces informations sont ici fournies par l'action dans l'objet [request] en tant qu'attributs.
3.6.6.4. achat.do
Cette action sert à acheter l'article affiché par la vue [INFOS] précédente :
Une fois l'article acheté, la vue [LISTE] est réaffichée (vue de droite). Si on regarde le code HTML de la vue de gauche ci-dessus, on trouve que la balise <form> est définie de la façon suivante :
| <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>
|
On voit donc que le formulaire est posté au contrôleur avec l'action [achat.do].
Cette action est configurée de la façon suivante dans [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>
Le code de la classe [AchatArticleAction] est le suivant :
| 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 {
// la servlet de contrôle
MainServlet mainServlet = (MainServlet) this.getServlet();
// erreurs d'initialisation ?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// 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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
// 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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// 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);
// on revient à la liste des articles
return mapping.findForward("afficherListeArticles");
}
}
|
Commentaires :
- le début de la méthode [execute] est identique à celles étudiées précédemment.
- rappelons la forme du formulaire envoyé au contrôleur :
| <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>
|
- Il y a deux paramètres dans la requête : [id] : n° de l'article acheté, [qte] : quantité achetée.
- La présence et la validité du paramètre [qte] sont testées. Si ce paramètre est reconnu incorrect, la vue [INFOS] est renvoyée à l'utilisateur accompagnée d'un message d'erreur :
| // 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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
|
- l'article acheté est récupéré dans la session. Celle-ci a pu expirer. Dans ce cas, on envoie la vue [ERREURS] :
| // 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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- si la session n'a pas expiré, l'article est mis dans le panier, lui aussi récupéré dans la session :
| // 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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// 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);
|
- enfin, on envoie la vue [LISTE] :
// on revient à la liste des articles
return mapping.findForward("afficherListeArticles");
- La vue qui sera réellement envoyée au client nous est fournie par [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 s'agit de la vue [/main.do]. Cette vue n'est pas une vue mais une action. L'action [/main.do] décrite précédemment va donc s'exécuter et afficher la liste des articles.
3.6.6.5. panier.do
Cette action sert à afficher tous les achats du client. Elle est disponible via l'option de menu [Voir le panier] :
Le code HTML associé au lien [Voir le panier] est le suivant :
<a href="panier.do">Voir le panier</a>
L'action [panier.do] est configurée de la façon suivante dans [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>
Le code de la classe [VoirPanierAction] est le suivant :
| 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 {
// la servlet de contrôle
MainServlet mainServlet = (MainServlet) this.getServlet();
// erreurs d'initialisation ?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// on affiche le panier
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// panier vide
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
} else {
// il y a qq chose dans le panier
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(), mainServlet.getHActionValidationPanier() });
return mapping.findForward("afficherPanier");
}
}
}
|
Commentaires :
- le début de la méthode [execute] est identique à celles étudiées précédemment.
- le panier est pris dans la session où il se trouve normalement. La session a pu expirer et alors on n'a pas de panier. On ne fait pas de ce cas un cas d'erreur mais on considère simplement que le panier est vide.
- si le panier est vide, la vue [PANIERVIDE] est affichée
- sinon, c'est la vue [PANIER]
Les vues réellement envoyées au client sont définies par l'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>
3.6.6.6. retirerachat.do
Cette action sert à retirer un achat du panier :
Après l'action [retirerachat.do], le panier est réaffiché (vue de droite ci-dessus). Si on regarde le code HTML du lien [Valider le panier] de la vue de gauche ci-dessus, on a la chose suivante :
<a href="retirerachat.do?id=3">Retirer</a>
L'action [retirerachat.do] reçoit donc en paramètre, l'id de l'article à retirer du panier. Cette action est configurée de la façon suivante dans [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>
Le code de la classe [RetirerAchatAction] est le suivant :
| 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 {
// la servlet de contrôle
MainServlet mainServlet = (MainServlet) this.getServlet();
// erreurs d'initialisation ?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// la liste des erreurs sur cette action
ArrayList erreurs = new ArrayList();
// on récupère le 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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
}
// on récupère l'id de l'article à retirer
String strId = request.getParameter("id");
// qq chose ?
if (strId == null) {
// pas normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// on transforme strId en entier
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// pas normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// on enlève l'achat
panier.enlever(id);
// on affiche de nouveau le panier
return mapping.findForward("afficherPanier");
}
}
|
Commentaires :
- le début de la méthode [execute] est identique à celles étudiées précédemment.
- le code qui suit vise à vérifier la présence et la validité du paramètre [id]. S'il s'avère incorrect, la vue [ERREURS] est envoyée.
- sinon, l'achat est enlevé du panier :
// on enlève l'achat
panier.enlever(id);
- puis le panier est réaffiché :
// on affiche de nouveau le panier
return mapping.findForward("afficherPanier");
La vue réellement envoyée au client est définie par l'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>
On voit qu'en fait de vue, c'est l'action [/panier.do] qui va être déclenchée. Celle-ci a déjà été décrite. Elle va afficher la vue [PANIER] ou [PANIERVIDE] selon l'état du panier.
3.6.6.7. validerpanier.do
Cette action sert à valider les achats faits par le client. Concrètement, cela se traduit par une unique action : les stocks des articles achetés sont décrémentés des quantités achetées, dans la base de données. Cette action vient du menu suivant :
Le code HTML du lien [Valider le panier] est le suivant :
<a href="validerpanier.do">Valider le panier</a>
Lorsque ce lien est activé, les stocks sont décrémentés et la liste des articles de nouveau affichée.
Cette action est configurée de la façon suivante dans [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>
Le code de la classe [ValiderPanierAction] est le suivant :
| 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 {
// la servlet de contrôle
MainServlet mainServlet = (MainServlet) this.getServlet();
// erreurs d'initialisation ?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// on affiche la page des erreurs
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// l'objet d'accès au domaine
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// 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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// on valide le panier
try {
articlesDomain.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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// on récupère les éventuelles erreurs
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
// tout semble OK - on affiche la liste des articles
return mapping.findForward("afficherListeArticles");
}
}
|
Commentaires :
- le début de la méthode [execute] est identique à celles étudiées précédemment.
- on récupère le panier dans la session. Si celle-ci a expiré, on envoie la vue [ERREURS] :
| // 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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- on valide les achats du panier. Il peut se produire des erreurs si les stocks sont insuffisants pour satisfaire les achats. Dans ce cas, on envoie la vue [ERREURS] :
| // on valide le panier
try {
articlesDomain.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[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// on récupère les éventuelles erreurs
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
|
- si tout s'est bien passé, on réaffiche la liste des articles :
// tout semble OK - on affiche la liste des articles
return mapping.findForward("afficherListeArticles");
La vue réellement envoyée au client est définie par l'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>
On voit qu'en fait de vue, c'est l'action [/main.do] qui va être déclenchée. Celle-ci a déjà été décrite. Elle va afficher la vue [LISTE].
3.7. Architecture MVC avec Spring
3.7.1. Architecture générale de l'application
Revenons sur l'architecture MVC de l'application :
Dans la première version :
- le contrôleur était assuré par une servlet
- les vues étaient assurées par des pages JSP
- le modèle était assuré par un ensemble de trois archives .jar
Dans la version Struts :
- le contrôleur était assuré par une servlet dérivée de la servlet générique [ActionServlet] de Struts
- les vues étaient assurées par les mêmes pages JSP que dans la version [Struts]
- le modèle était assuré par les mêmes trois archives
Dans la version Spring :
- le contrôleur sera assuré par une servlet fournie par Spring [DispatcherServlet]
- les vues seront assurées par les mêmes pages JSP que précédemment à quelques détails près
- le modèle sera assuré par les mêmes trois archives
Nous allons découvrir que le passage de Struts à Spring de notre application est simple si on accepte de ne pas utiliser tous les éléments préconisés pour une architecture Spring MVC orthodoxe. Les transformations principales sont les suivantes :
- les actions qui étaient traitées dans des méthodes particulières de la servlet/contrôleur, ou par des instances de classes dérivées de la classe [Action] dans Struts, sont maintenant traitées par des instances de classe implémentant l'interface Spring [Controller]
- les fichiers de configuration nécessaires deviennent les suivants :
- [web.xml] parce qu'on a affaire à une application web. Ce fichier contient un Listener qui, au moment de l'initialisation de l'application va exploiter le fichier [applicationContext.xml]
- [applicationContext.xml] qui va permettre de créer les beans dont a besoin l'application, notamment celui du service d'accès au modèle
- les vues JSP seront identiques à celles de Struts. On sera amené à en créer une nouvelle.
Rappelons l'architecture MVC STRUTS utilisée dans la version précédente :
| les classes métier, les classes d'accès aux données et la base de données |
| les pages JSP |
| la servlet de traitement des requêtes clientes, les objets [Action] |
Avec Spring, nous prenons une architecture identique :
| les classes métier, les classes d'accès aux données et la base de données |
| les pages JSP |
| la servlet de traitement des requêtes clientes, les objets implémentant l'interface [Controller] |
- le contrôleur est le coeur de l'application. Toutes les demandes du client transitent par lui. C'est une servlet générique fournie par SPRING. Elle est de type [DispatcherServlet]. Nous appellerons désormais ce contrôleur, le contrôleur [Spring].
- le contrôleur [Spring] va orienter la requête du client vers l'une des instances de type [Controller]. Il y aura une telle instance par action à traiter. Celle-ci est définie dans l'url demandée comme avec Struts. Ainsi on saura que l'action demandée est l'action [liste] parce que l'url demandée est [liste.do]
- si C est le contexte de l'application, le contrôleur [Spring] exploite un fichier [C-servlet.xml] qui joue le rôle du fichier de configuration struts-config.xml, dans la version Struts. A chaque action devant être traitée par l'application, on associe le nom de la classe de type Controller chargée de traiter la requête.
- le contrôleur passe la main à l'objet de type Controller associé à l'action. Il le fait en demandant l'exécution de la méthode handleRequest de cet objet à laquelle il transmet la requête du client. C'est ici que le développeur fait ce qu'il a à faire : il devra éventuellement faire appel à des classes métier ou à des classes d'accès aux données. A la fin du traitement, l'objet Controller rend au contrôleur le nom de la vue qu'il doit envoyer en réponse au client.
- dans son fichier de configuration, le contrôleur trouvera l'URL associée au nom de la vue qu'on lui a demandé d'afficher. Il envoie alors cette dernière. L'échange avec le client est terminé.
3.7.2. Le modèle
C'est le même que dans les deux versions précédentes. Il est constitué des archives Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.7.3. Configuration de l'application
3.7.3.1. Architecture générale
L'architecture générale du projet Eclipse est la suivante :

3.7.3.2. Configuration de l'accès aux données
Etant donné, que l'interface d'accès aux données ne bouge pas, les fichiers de configuration associés sont les mêmes que dans la version précédente. Ils sont définis dans [WEB-INF/src] :

Sur la copie d'écran ci-dessus, les fichiers [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] sont ceux des versions précédentes.
3.7.3.3. Le répertoire des archives
Dans [WEB-INF/lib], on trouve les mêmes archives que dans la version précédente, sauf celles de Struts qui n'est plus nécessaire :

3.7.3.4. Configuration de l'application
L'application est configurée à l'aide de trois fichiers : [web.xml, applicationContext.xml, springwebarticles-servlet.xml] dans le dossier [WEB-INF] :

Le fichier [web.xml] est le suivant :
| <?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>
<!-- le chargeur du contexte spring de l'application -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- la servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- le mapping des url -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- le document d'entrée -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Que dit ce fichier ?
- la page d'accueil de l'application est [/vues/index.jsp] (welcome-file)
- les demandes d'url de la forme *.do seront redirigées vers la servlet [springwebarticles] (servlet-mapping)
- la servlet [springwebarticles] est une instance de la classe [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class) fournie par Spring.
- le listener [org.springframework.web.context.ContextLoaderListener] sera lancé au démarrage de l'application. Son rôle principal va être d'instancier les beans Spring définis dans le fichier [applicationContext.xml]
Le fichier [applicationContext.xml] est le suivant :
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la classe d'accès aux données -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- la classe métier -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- 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>
</beans>
|
On retrouve des choses connues et d'autres moins. Trois beans seront instanciés lors de l'initialisation de l'application :
- articlesDao : service d'accès à la couche [dao]
- articlesDomain : service d'accès au modèle
- config : un bean dans lequel nous allons rassembler les informations que doivent partager tous les clients. Ce bean jouera le rôle que joue classiquement le contexte de l'application mais avec des informations typées plutôt que non typées.
Le dernier fichier [springwebarticles.xml] définit les actions acceptées par l'application d'une façon très proche à celle utilisée par le fichier Struts [struts-config.xml]. Son contenu est le suivant :
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- les gestionnaires d'actions = contrôleurs -->
<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>
<!-- 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>
<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>
<!-- 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>
<!-- le fichier des messages -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
Que dit ce fichier de configuration ?
- que notre contrôleur traitera les url suivantes :
| pour afficher la liste des articles |
| pour afficher la liste des articles |
| pour afficher des informations sur un article donné |
| pour acheter un article donné |
| pour afficher le panier des achats |
| pour retirer un achat du panier |
| pour valider un panier d'achats |
- les actions précédentes correspondent une à une aux actions traitées par le contrôleur dans les versions précédentes. Pour chacune d'elles est précisé le nom de la classe chargée de la traiter. Prenons l'exemple de l'action [/panier.do] :
- elle doit être traitée par le bean [VoirPanierController]. Ce nom est libre. C'est une simple clé.
<prop key="/panier.do">VoirPanierController</prop>
- la clé [VoirPanierController] est le nom d'un bean défini dans le même fichier de configuration :
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
- le bean [VoirPanierController] définit :
- la classe à instancier [ istia.st.articles.web.spring.VoirPanierController] pour traiter l'action
- comment l'instancier. Ici le bean [config] défini par [applicationContext.xml] et instancié au démarrage de l'application est fourni en paramètre. Cela sera fait pour toutes les actions [Controller]. Ainsi chacune d'elles disposera dans un champ privé, de l'objet [config] dans lequel elle trouvera toutes les informations partagées entre tous les clients.
- la façon dont doivent être résolus les noms des vues :
<!-- 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>
Comme avec Struts, l'instance [Controller] qui traite une action va rendre, après traitement, une clé au contrôleur Spring pour lui indiquer quelle vue il doit afficher. A partir de cette clé, il peut exister diverses stratégies pour générer la vue associée à la clé. La stratégie retenue est celle définie par le bean [viewResolver]. Ici, ce bean est associé à la classe [org.springframework.web.servlet.view.InternalResourceViewResolver] avec différents paramètres d'initialisation. Sans entrer dans les détails, le bean [viewResolver] dit ici que si la clé de la vue est "XX", alors la vue générée sera [/vues/XX.jsp]. Le type de vues envoyées au client peut être changée de diverses manières :
- en changeant de classe d'implémentation pour le bean [viewResolver]
- en changeant les paramètres d'initialisation de la classe d'implémentation
Ainsi, on peut passer d'une vue HTML à une vue XML simplement en changeant la valeur du bean [viewResolver]
- le nom d'un fichier de messages pour l'application (messageSource). Ici, le fichier existera mais sera vide. Il ne sera pas utilisé. Il doit être placé dans le [ClassPath] de l'application. Ici il sera placé dans [WEB-INF/classes]. Sous Eclipse, on obtient ce résultat en le plaçant dans [WEB-INF/src] :

3.7.4. Les vues JSP
Les vues JSP utilisées seront celles utilisées par Struts. Aucune n'est modifiée :

Une unique nouvelle vue est créée : redirpanier.jsp. Elle sert uniquement à rediriger le client vers l'action [/panier.do]. Son code est le suivant :
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
Le lecteur est invité à relire la définition des différentes vues dans la version Struts.
3.7.5. Le traitement des actions
Les classes nécessaires au traitement des différentes actions ont été rassemblées dans le paquetage [istia.st.articles.web.spring] :

Rappelons la mécanique de l'application Spring sur un exemple :
- l'utilisateur demande l'url [http://localhost:8080/springwebarticles/main.do]

Que s'est-il passé ?
- le fichier [web.xml] de l'application [springwebarticles] a été consulté :
| <?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>
<!-- le chargeur du contexte spring de l'application -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- la servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- le mapping des url -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- le document d'entrée -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
- si c'était la première requête à l'application, un certain nombre de choses se sont déclenchées :
- le listener [ org.springframework.web.context.ContextLoaderListener] a été chargé
- il a exploité le fichier de configuration [applicationContext.xml] :
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la classe d'accès aux données -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- la classe métier -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- 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>
</beans>
|
- les beans ci-dessus ont été créés dans le contexte de l'application
- le fichier [springwebarticles-servlet.xml] a ensuite été exploité :
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- les gestionnaires d'actions = contrôleurs -->
<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>
<!-- 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>
<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>
<!-- 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>
<!-- le fichier des messages -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
- les beans [Controller] définis par ce fichier ont eux-aussi été créés
- tout est maintenant en place pour traiter la requête du client. Celle-ci était : [http://localhost:8080/springwebarticles]. Ici, on ne demande pas une URL du contexte mais le contexte lui-même. C'est alors la section [welcome-file] du fichier [web.xml] qui est utilisée.
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
- la vue [index.jsp] est la suivante :
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
- le client est donc invité à se rediriger vers l'url [http://localhost:8080/springwebarticles/main.do]. Il le fait.
- le contrôleur Spring reçoit donc une nouvelle requête. Il exploite son fichier [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>
<!-- les gestionnaires d'actions = contrôleurs -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
....
</bean>
<!-- 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>
<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>
|
- ce fichier lui dit que l'action [/main.do] doit être traitée par le bean [ListController].
- la requête du client est transmise à la méthode [handleRequest] du bean [ListController]. Celle-ci fait son travail et rend au contrôleur la clé de la vue à afficher. Ici, si tout va bien, cette clé sera [liste].
- le contrôleur Spring utilise le bean [viewResolver] du fichier de configuration [springwebarticles-servlet.xml] pour déterminer la vue associée à cette clé. Ici, ce sera la vue [/vues/liste.jsp]
- la vue [/vues/liste.jsp] est envoyée au client
3.7.6. Initialisation de l'application Spring
Nous avons dit qu'au démarrage de l'application, les beans du fichier [applicationContext.xml] étaient instanciés :
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- la classe d'accès aux données -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- la classe métier -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- 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>
</beans>
|
Nous connaissons les beans [articlesDao, articlesDomain] mais pas le bean [config]. Celui-ci est défini par la classe Java suivante :
| package istia.st.articles.web.spring;
import java.util.Hashtable;
import istia.st.articles.domain.IArticlesDomain;
/**
* @author ST - ISTIA
*/
public class Config {
// champs privés
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 application web
public void init() {
// on mémorise certaines url de l'application
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);
// c'est fini
return;
}
}
|
Cette classe fait ce que fait la méthode [init] d'une servlet d'application web. Elle initialise l'application. Ici, celle-ci se passe de la façon suivante :
- parce que le bean [config] est défini de la façon suivante dans [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>
lorsqu'il est créé, son champ privé [articlesDomain] a été initialisé
- ensuite, à cause de l'attribut [init-method="init"] du bean ci-dessus, la méthode [init] de la classe associée au bean est exécutée. Ici, elle initialise les trois dictionnaires [hActionListe, hActionPanier, hActionValidationPanier] utilisés pour générer les trois liens possibles du menu offert à l'utilisateur.
- des accesseurs publics sont créés pour rendre accessibles ces champs privés aux instances de type [Controller] qui vont traiter les actions.
3.7.7. Les actions [Controller] de l'application Spring
3.7.7.1. Introduction
Chaque action Spring va faire l'objet d'une classe de type [Controller]. Dans la version Struts, chaque action faisait l'objet d'une classe de type [Action]. L'écriture de la classe [Controller] consiste le plus souvent :
- à faire un copier/coller de la classe [Action] qui avait été utilisée dans la version Struts
- à adapter le code aux conventions Spring
3.7.7.2. main.do, liste.do
Ces deux actions sont identiques et définies dans [springwebarticles-servlet.xml] par :
| <!-- 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>
|
Elles sont associées à la classe [ istia.st.articles.web.spring.ListController] que nous allons prochainement détailler. Lorsque l'une de ces actions est exécutée dans un navigateur, on obtient le résultat suivant :

Le code de la classe [istia.st.articles.web.spring.ListController] est le suivant :
| 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 {
// la configuration de l'appli web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// traitement de la reqête
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 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");
}
// 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");
}
}
|
Commentaires :
- la classe a un champ privé [config]. Ce champ a été initialisé par Spring lorsque le bean [ListController] a été instancié :
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
On voit ci-dessus, que le champ [config] de [ListController] est initialisé avec le bean [config]. De quoi s'agit-il ? Il s'agit du bean [config] définit dans [applicationContext.xml], c.a.d. une instance de [istia.st.articles.web.spring.Config] décrite plus haut.
- écrire le code d'une classe [Controller] consiste essentiellement à écrire le code de sa méthode [handleRequest]
- on demande la liste des articles au modèle. Celui-ci est accessible via [config.getArticlesDomain()]. Si une exception se produit, la vue [ERREURS] est envoyée. Le résultat rendu par [handleRequest] doit être de type [ModelAndView]. On peut instancier cette classe de différentes façons. Ici, et ce sera toujours le cas, on construit une instance de [ModelAndView] en lui passant la clé de la vue à afficher. On rappelle que par configuration du bean [viewResolver], demander l'affichage de la vue de clé XX aboutira à l'envoi de la vue [/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");
}
|
- s'il n'y a pas d'erreurs, c'est la vue [LISTE] qui est envoyée :
| // 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
Cette action sert à fournir de l'information sur l'un des articles affichés dans la vue [LISTE] :
Cette action est définie dans [springwebarticles-servlet.xml] par :
| <!-- 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>
|
Le code de la classe [InfosController] est le suivant :
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class InfosController implements Controller {
// la configuration de l'appli web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// traitement de la reqête
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// la liste des erreurs
ArrayList erreurs = new ArrayList();
// on récupère l'id demandé
String strId = request.getParameter("id");
// qq chose ?
if (strId == null) {
// pas normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// on transforme strId en entier
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// pas normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// on demande l'article de clé id
Article article = null;
try {
article = config.getArticlesDomain().getArticleById(id);
} 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");
}
if (article == null) {
// pas normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// on met l'article dans la session
request.getSession().setAttribute("article", article);
// on affiche la page d'infos
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("infos");
}
}
|
Commentaires :
- la méthode [handleRequest] récupère le paramètre [id] qui doit normalement se trouver dans l'url. Celle-ci doit être en-effet de la forme [/infos.do?id=X]. Différents tests sont faits pour tester la présence et la validité du paramètre [id]. En cas de problème, la vue [ERREURS] est envoyée.
- si [id] est valide, l'article correspondant est demandé à la couche [domain]. Si celle-ci lance une exception ou si l'article n'est pas trouvé, là encore la vue [ERREURS] est envoyée.
- si tout va bien, l'article obtenu est mis dans la session. C'est un point discutable. Ici, on part sur le principe que le client va peut-être acheter cet article. S'il le fait, on ira le chercher dans la session, plutôt que de le demander de nouveau à la couche [domain].
- enfin, la vue [INFOS] est affichée.
3.7.7.4. achat.do
Cette action sert à acheter l'article affiché par la vue [INFOS] précédente :

Si on regarde le code HTML de cette vue, on trouve que la balise <form> est définie de la façon suivante :
<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>
On voit donc que le formulaire est posté au contrôleur avec l'action [achat.do].
Cette action est configurée de la façon suivante dans [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>
Le code de la classe [AchatController] est le suivant :
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class AchatController implements Controller {
// la configuration de l'appli web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// traitement de la reqête
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 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");
}
// 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");
}
// 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);
// on revient à la liste des articles
return new ModelAndView("index");
}
}
|
Commentaires :
- rappelons la forme du formulaire envoyé au contrôleur :
<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>
- Il y a deux paramètres dans la requête : [id] : n° de l'article acheté, [qte] : quantité achetée.
- La présence et la validité du paramètre [qte] sont testées. Si ce paramètre est reconnu incorrect, la vue [INFOS] est renvoyée à l'utilisateur accompagnée d'un message d'erreur :
| // 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'article acheté est récupéré dans la session. Celle-ci a pu expirer. Dans ce cas, on envoie la vue [ERREURS] :
| // 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");
}
|
- si la session n'a pas expiré, l'article est mis dans le panier, lui aussi récupéré dans la session :
| // 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);
|
- enfin, on envoie la vue [LISTE] :
// on revient à la liste des articles
return new ModelAndView("index");
- Ci-dessus, on envoie la vue [/vues/index.jsp]. On sait que celle-ci demande au navigateur client de se rediriger vers l'url [/main.do]. C'est cette redirection qui va afficher la liste des articles.
3.7.7.5. panier.do
Cette action sert à afficher tous les achats du client. Elle est disponible via le menu :

Le code HTML associé au lien ci-dessus est le suivant :
<a href="panier.do">Voir le panier</a>
La page renvoyée par ce lien est la suivante :

Cette action est configurée de la façon suivante dans [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>
|
Le code de la classe [VoirPanierController] est le suivant :
| 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 {
// la configuration de l'appli web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// traitement de la reqête
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// on affiche le panier
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// panier vide
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("paniervide");
} else {
// il y a qq chose dans le panier
request.setAttribute("actions", new Hashtable[] {
config.getHActionListe(), config.getHActionValidationPanier() });
return new ModelAndView("panier");
}
}
}
|
Commentaires :
- le panier est pris dans la session où il se trouve normalement. La session a pu expirer et alors on n'a pas de panier. On ne fait pas de ce cas un cas d'erreur mais on considère simplement que le panier est vide.
- si le panier est vide, la vue [PANIERVIDE] est affichée
- sinon, c'est la vue [PANIER]
3.7.7.6. retirerachat.do
Cette action sert à retirer un achat du panier :

Si on regarde le code HTML du lien ci-dessus, on a la chose suivante :
<a href="retirerachat.do?id=3">Retirer</a>
L'action [retirerachat.do] reçoit donc en paramètre, l'id de l'article à retirer du panier. Cette action est configurée de la façon suivante dans [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>
|
Le code de la classe [RetirerAchatController] est le suivant :
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class RetirerAchatController implements Controller {
// la configuration de l'appli web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// traitement de la reqête
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// la liste des erreurs sur cette action
ArrayList erreurs = new ArrayList();
// on récupère le 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");
}
// on récupère l'id de l'article à retirer
String strId = request.getParameter("id");
// qq chose ?
if (strId == null) {
// pas normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// on transforme strId en entier
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// pas normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// on enlève l'achat
panier.enlever(id);
// on affiche de nouveau le panier
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
}
}
|
Commentaires :
- le code vérifie la présence et la validité du paramètre [id]. S'il s'avère incorrect, la vue [ERREURS] est envoyée.
- sinon, l'achat est enlevé du panier :
// on enlève l'achat
panier.enlever(id);
- puis le panier est réaffiché :
// on affiche de nouveau le panier
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
Rappelons le code de la vue [/vues/redirpanier.jsp] :
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
On voit que le client va être redirigé vers l'action [/panier.do]. Celle-ci a déjà été décrite. Elle va afficher la vue [PANIER] ou [PANIERVIDE] selon l'état du panier.
3.7.7.7. validerpanier.do
Cette action sert à valider les achats faits par le client. Concrètement, cela se traduit par une unique action : les stocks des articles achetés sont décrémentés des quantités achetées, dans la base de données. Cette action vient du menu suivant :

Le code HTML du lien [Valider le panier] est le suivant :
<a href="validerpanier.do">Valider le panier</a>
Lorsque ce lien est activé, les stocks sont décrémentés et la liste des articles de nouveau affichée.
Cette action est configurée de la façon suivante dans [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>
|
Le code de la classe [ValiderPanierController] est le suivant :
| 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 {
// la configuration de l'appli web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// traitement de la reqête
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 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");
}
// 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");
}
// tout semble OK - on affiche la liste des articles
return new ModelAndView("index");
}
}
|
Commentaires :
- on récupère le panier dans la session. Si celle-ci a expiré, on envoie la vue [ERREURS] :
| // 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");
}
|
- on valide les achats du panier. Il peut se produire des erreurs si les stocks sont insuffisants pour satisfaire les achats. Dans ce cas, on envoie la vue [ERREURS] :
| // 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");
}
|
- si tout s'est bien passé, on réaffiche la liste des articles :
// tout semble OK - on affiche la liste des articles
return new ModelAndView("index");
On sait que la vue [/vues/index.jsp] redirige le client vers l'action [/main.do]. Cette action va afficher la vue [LISTE].