3. Article 2 - Examples of three-tier web architectures
Objectives of this article:
- 3-tier architectures
- basic MVC web architecture
- Struts MVC architecture
- Spring MVC architecture
Tools used:
- Spring: http://www.springframework.org/
- Ibatis SqlMap: http://www.ibatis.com/
- JUnit: http://www.junit.org/index.htm
- Eclipse: http://www.eclipse.org/
- Struts: http://struts.apache.org/
- Firebird: http://firebird.sourceforge.net/: DBMS, JDBC driver. In fact, any JDBC source will do.
- IBExpert, Personal Edition: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (March 2005). IBExpert allows you to graphically administer the Firebird DBMS.
- Tomcat: http://jakarta.apache.org/tomcat/
- Tomcat plugin for Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. See also the document https://tahe.developpez.com/java/eclipse/
Understanding this document requires various prerequisites. Some of these can be found in documents I have written. In such cases, I cite them. Obviously, this is only a suggestion, and readers are free to use their preferred resources.
- Java language: [https://tahe.developpez.com/java/cours]
- Web programming in Java: [https://tahe.developpez.com/java/web/]
- Web programming with Java, Eclipse, and Tomcat: [https://tahe.developpez.com/java/eclipse/]
- Web programming with Struts: [https://tahe.developpez.com/java/struts/]
- Using Spring's IoC aspect: [https://tahe.developpez.com/java/springioc]
- JSTL tag library: [https://tahe.developpez.com/java/eclipse/] (in part)
- Ibatis SqlMap documentation: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
- Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (March 2005).
The ideas in this document originated from a book I read in the summer of 2004, a magnificent work by Rod Johnson: J2EE Development without EJB, published by Wrox.
3.1. The webarticles application
Here we would like to present some components of an e-commerce web application. This application will allow web clients
- to view a list of items from a database
- add some of them to an electronic shopping cart
- to confirm the cart. This confirmation will simply update the inventory levels of the purchased items in the database.
The different views presented to the user will be as follows:
- the [LIST] view, which displays a list of items for sale

- the [INFO] view, which provides additional information about a product:

- the [CART] view, which displays the contents of the customer's cart

- the [EMPTY CART] view, in case the customer's cart is empty

- the [ERRORS] view, which reports any application errors

3.2. General Application Architecture
We want to build an application with the following three-tier architecture:
- The three layers are made independent through the use of Java interfaces
- The integration of the different layers is handled by Spring
- Each layer is contained in separate packages: web (user interface layer), domain (business layer), and DAO (data access layer).
We will assume here that the [domain] and [DAO] layers are already in place. We will focus only on the [web] layer, which we propose to build in several ways:
- using classic servlet controller technology—JSP pages
- using Struts MVC technology
- using Spring MVC technology
In all cases, the application will follow an MVC (Model-View-Controller) architecture. If we refer back to the layered diagram above, the MVC architecture fits into it as follows:
Processing a client request follows these steps:
- The client sends a request to the controller. This controller is a servlet that handles all client requests. It is the application’s entry point. It is the C in MVC.
- The controller processes this request. To do so, it may need assistance from the business layer, known as the M model in the MVC structure.
- The controller receives a response from the business layer. The client’s request has been processed. This may trigger several possible responses. A classic example is
- an error page if the request could not be processed correctly
- a confirmation page otherwise
- The controller chooses the response (= view) to send to the client. This is most often a page containing dynamic elements. The controller provides these to the view.
- The view is sent to the client. This is the V in MVC.
3.3. The Model
Here we examine the M in MVC. The model consists of the following elements:
- business classes
- data access classes
- the database
3.3.1. The database
The database contains only one table named ARTICLES. This table was generated using the following SQL commands:
| CREATE TABLE ARTICLES (
ID INTEGER NOT NULL,
NOM VARCHAR(30) NOT NULL,
PRIX NUMERIC(15,2) NOT NULL,
STOCKACTUEL INTEGER NOT NULL,
STOCKMINIMUM INTEGER NOT NULL
);
/* constraints */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
|
| primary key uniquely identifying an item |
| item name |
| its price |
| current stock |
| the stock level below which a reorder must be placed |
In the following tests, a [Firebird] database was used. [Firebird] is an "open source" DBMS. The JDBC driver [firebirdsql-full.jar] is placed in the [WEB-INF/lib] folder of the web application.
3.3.2. The model packages
The M model is provided here in the form of three archives:
- istia.st.articles.dao: contains the data access classes for the [DAO] layer
- istia.st.articles.exception: contains an exception class for this article management
- istia.st.articles.domain: contains the business classes of the [domain] layer
archive | content | role |
istia.st.articles.dao | - contains the [istia.st.articles.dao] package, which itself contains the following elements: - [IArticlesDao]: the interface for accessing the Dao layer. This is the only interface visible to the [domain] layer. It sees no others. - [Article]: class defining an article - [ArticlesDaoSqlMap]: implementation class for the [IArticlesDao] interface using the SqlMap tool | data access layer – is located entirely within the [dao] layer of the web application’s 3-tier architecture |
istia.st.articles.domain | - contains the package [istia.st.articles.domain], which itself contains the following elements: - [IArticlesDomain]: the interface for accessing the [domain] layer. This is the only interface visible to the web layer. It sees no others. - [ArticlePurchases]: a class implementing [IArticlesDomain] - [Purchase]: a class representing a customer's purchase - [ShoppingCart]: a class representing a customer’s total purchases | represents the web purchase model - is located entirely within the [domain] layer of the web application's 3-tier architecture |
istia.st.articles.exception | - contains the package [istia.st.articles.exception], which itself contains the following elements: - [UncheckedAccessArticlesException]: class defining a [RuntimeException] exception. This type of exception is thrown by the [dao] layer as soon as a data access problem occurs. | |
3.3.3. The [istia.st.articles.dao] package
The class defining an article is as follows:
| package istia.st.articles.dao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
/**
* @author ST - ISTIA
*
*/
public class Article {
private int id;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
/**
* constructeur par défaut
*/
public Article() {
}
public Article(int id, String nom, double prix, int stockActuel,
int stockMinimum) {
// init instance attributes
setId(id);
setNom(nom);
setPrix(prix);
setStockActuel(stockActuel);
setStockMinimum(stockMinimum);
}
// getters - setters
public int getId() {
return id;
}
public void setId(int id) {
// valid id?
if (id < 0)
throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
// valid name?
if(nom==null || nom.trim().equals("")){
throw new UncheckedAccessArticlesException("Le nom est [null] ou vide");
}
this.nom = nom;
}
public double getPrix() {
return prix;
}
public void setPrix(double prix) {
// valid price?
if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
this.prix = prix;
}
public int getStockActuel() {
return stockActuel;
}
public void setStockActuel(int stockActuel) {
// valid stock?
if (stockActuel < 0)
throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
this.stockActuel = stockActuel;
}
public int getStockMinimum() {
return stockMinimum;
}
public void setStockMinimum(int stockMinimum) {
// valid stock?
if (stockMinimum < 0)
throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
this.stockMinimum = stockMinimum;
}
public String toString() {
return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
+ stockMinimum + "]";
}
}
|
This class provides:
- a constructor for setting the 5 pieces of information for an item
- accessors, often called getters/setters, used to read and write the 5 pieces of information. The names of these methods follow the JavaBean standard. The use of JavaBean objects in the DAO layer to interface with DBMS data is standard practice.
- validation of the data entered for the item. If the data is invalid, an exception is thrown.
- A toString method that returns the value of an item as a string. This is often useful for debugging an application.
The [IArticlesDao] interface is defined as follows:
| 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);
}
|
The roles of the various methods in the interface are as follows:
| returns all items from the ARTICLES table in a list of [Article] objects |
| clears the ARTICLES table |
| returns the [Article] object identified by its primary key |
| allows you to add an article to the ARTICLES table |
| allows you to modify an item in the [ARTICLES] table |
| allows you to delete an item from the [ARTICLES] table |
| allows you to modify the stock of an item in the [ARTICLES] table |
The interface provides client programs with a number of methods defined solely by their signatures. It does not concern itself with how these methods will actually be implemented. This brings flexibility to an application. The client program makes calls to an interface rather than to a specific implementation of that interface.
The choice of a specific implementation will be made via a Spring configuration file. Here, we propose to implement the IArticlesDao interface using an open-source product called SqlMap. This will allow us to remove all SQL statements from the Java code.
The implementation class [ArticlesDaoSqlMap] is defined as follows:
| 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) {}
}
|
All data access methods have been synchronized to prevent concurrent access issues to the data source. At any given time, only one thread has access to a given method.
The [ArticlesDaoSqlMap] class uses the [Ibatis SqlMap] tool. The advantage of this tool is that it allows the SQL code for data access to be separated from the Java code. It is then placed in a configuration file. We will have the opportunity to revisit this later. To be instantiated, the [ArticlesDaoSqlMap] class requires a configuration file whose name is passed as a parameter to the class constructor. This configuration file defines the information necessary to:
- access the DBMS containing the articles
- manage a connection pool
- manage transactions
In our example, it will be named [sqlmap-config-firebird.xml] and will define access to a Firebird database:
| <?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>
|
The configuration file [articles.xml] referenced above defines how to construct an instance of the class [istia.st.articles.dao.Article] from a row in the [ARTICLES] table of the DBMS. It also defines the SQL queries that will allow the [dao] layer to retrieve data from the Firebird data source.
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- an alias to the istia.st.articles.dao.Article class -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- mapping ORM : row table ARTICLES - instance class Article -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- query SQL to obtain all items -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- query SQL to delete all items -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- the SQL query to insert an article -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>
<!-- the SQL query to delete a given item -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- query SQL to modify a given item -->
<statement id="modifyArticle">
update ARTICLES set nom=#nom#,
prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
id=#id#
</statement>
<!-- query SQL to obtain a given item -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- query SQL to modify the stock of a given item -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#mouvement#
where id=#id# and stockActuel+#mouvement#>=0
</statement>
</sqlMap>
|
The code for the [dao] package can be found in the appendix.
3.3.4. The [istia.st.articles.domain] package
The [IArticlesDomain] interface decouples the [business] layer from the [web] layer. The latter accesses the [business/domain] layer via this interface without concerning itself with the class that actually implements it. The interface defines the following actions for accessing the business layer:
| package istia.st.articles.domain;
// Imports
import java.util.ArrayList;
import java.util.List;
public abstract interface IArticlesDomain {
// Methods
void acheter(Panier panier);
List getAllArticles();
Article getArticleById(int idArticle);
ArrayList getErreurs();
}
|
| returns the list of [Article] objects to be displayed to the client |
Article getArticleById(int idArticle)
| returns the [Article] object identified by [idArticle] |
| processes the customer's cart by decrementing the stock of purchased items by the quantity purchased—may fail if stock is insufficient |
| returns the list of errors that occurred - empty if no errors |
Here, the [IArticlesDomain] interface will be implemented by the following [PurchaseItems] class:
| package istia.st.articles.domain;
// Imports
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.List;
public class AchatsArticles implements IArticlesDomain {
// Fields
private IArticlesDao articlesDao;
private ArrayList erreurs;
// Manufacturers
public AchatsArticles(IArticlesDao articlesDao) { }
// Methods
public ArrayList getErreurs() {}
public List getAllArticles() {}
public Article getArticleById(int id) {}
public void acheter(Panier panier) { }
}
|
This class implements the four methods of the [IArticlesDomain] interface. It has two private fields:
| the data access object provided by the data access layer |
| the list of any errors |
To create an instance of the class, you must provide the object that allows access to the DBMS data:
public PurchasesItems(IArticlesDao articlesDao)
| constructor |
The [Purchase] class represents a customer purchase:
| package istia.st.articles.domain;
public class Achat {
// Fields
private Article article;
private int qte;
// Manufacturers
public Achat(Article article, int qte) { }
// Methods
public double getTotal() {}
public Article getArticle() {}
public void setArticle(Article article) { }
public int getQte() {}
public void setQte() { }
public String toString() {}
}
|
The [Purchase] class is a JavaBean with the following fields and methods:
| the purchased item |
| the quantity purchased |
| returns the purchase amount |
| object's string representation |
The [Cart] class represents the customer's total purchases:
| package istia.st.articles.domain;
// Imports
import java.util.ArrayList;
public class Panier {
// Fields
private ArrayList achats;
// Manufacturers
public Panier() { }
// Methods
public ArrayList getAchats() {}
public void ajouter(Achat unAchat) { }
public void enlever(int idAchat) { }
public double getTotal() {}
public String toString() { }
}
|
The [Cart] class is a JavaBean with the following fields and methods:
| the customer's list of purchases - a list of objects of type [Purchase] |
void add(Purchase purchase)
| adds a purchase to the list of purchases |
| removes the purchase for item idArticle |
| returns the total amount of purchases |
| returns the string representation of the shopping cart |
| returns the list of purchases |
The code for the [domain] package can be found in the appendix.
3.3.5. The [istia.st.articles.exception] package
This package contains the class defining the exception thrown by the [dao] layer when it encounters a problem accessing the data source:
| 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. Model Testing
Model M was tested in Eclipse with the following configuration:

Comments:
- In [WEB-INF/lib] you will find:
- the archives required by the [ibatis SqlMap] tool responsible for accessing the Firebird DBMS: ibatis-*.jar
- the file required for the [Spring] tool: spring.jar
- the JDBC driver for the [Firebird] DBMS: firebirdsql-full.jar
- the archives required for logging: log4-*.jar, commons-logging.jar
- the three archives for the tested model: istia.st.articles.*.jar
- the archive required for the [junit] testing tool
- In [WEB-INF/src] are the configuration files that will be automatically copied to [WEB-INF/classes] by Eclipse:
- the configuration files for the [sqlmap] tool: sqlmap-config-firebird.xml, articles.xml
- the [spring] tool’s configuration files: spring-config-test-dao.xml, spring-config-test-domain.xml
- the configuration file for the [log4j] tool: log4j.properties
- In the [istia.st.articles.tests] package, you will find the model test classes
3.3.6.1. Tests for the [dao] layer
The JUnit test class for the [dao] layer is as follows. Reading it helps understand how the methods of the [IArticlesDao] interface are used:
| package istia.st.articles.tests.dao;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.dao.Article;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// test the ArticlesDaoSqlMap class
public class JunitModeleDaoArticles extends TestCase {
// an instance of the class under test
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// retrieves a data access instance
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
public void testGetAllArticles() {
// displays articles
listArticles();
}
public void testClearAllArticles() {
// empties item table
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
}
public void testAjouteArticle() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
//the poster
listArticles();
}
public void testSupprimeArticle() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// delete
articlesDao.supprimeArticle(4);
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(1, articles.size());
// displays the table
listArticles();
}
public void testModifieArticle() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
// modification
articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
// getById
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getPrix(), 44, 1e-6);
// displays the table
listArticles();
}
public void testGetArticleById() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
private void listArticles() {
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
// display read articles
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
public void testChangerStockArticle() throws InterruptedException {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// insertion
int nbArticles = articlesDao.ajouteArticle(new Article(3, "article3",
30, 101, 3));
assertEquals(nbArticles, 1);
nbArticles = articlesDao.ajouteArticle(new Article(4, "article4", 40,
40, 4));
assertEquals(nbArticles, 1);
// creation of 100 threads to update the stock of item 3
Thread[] taches = new Thread[100];
for (int i = 0; i < taches.length; i++) {
taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
taches[i].start();
}
// we wait for the end of threads
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// retrieve item 3 and check stock
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
assertEquals(1, unArticle.getStockActuel());
// modification stock article 4
boolean erreur = false;
int nbLignes = articlesDao.changerStockArticle(4, -100);
assertEquals(0, nbLignes);
// displays the table
listArticles();
}
}
|
Comments:
- The test class stores, using its setUp method, an instance of the class under test:
| // une instance de la classe testée
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// récupère une instance d'accès aux données
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
|
- The object to be tested is provided by [Spring]. Above, we request the Spring bean named [articlesDao]. This bean is defined in the Spring configuration file [spring-config-test-dao.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
</beans>
|
As shown above, the [articlesDao] bean is an instance of the [istia.st.articles.dao.ArticlesDaoSqlMap] class. This class has a constructor that takes the name of the [SqlMap] tool's configuration file as a parameter. That name is provided here. It is [sqlmap-config-firebird.xml]. This file has already been described. It provides all the necessary information to access the DBMS data.
The [testChangerStockArticle] method creates 100 threads responsible for decrementing the stock of a given item. The purpose here is to test concurrent access to the DBMS. Because the [changerStockArticle] method of the [istia.st.articles.dao.ArticlesDaoSqlMap] class has been synchronized, this test passes. If we remove the synchronization, it no longer passes. The class responsible for updating the stock is as follows:
| package istia.st.articles.tests;
import istia.st.articles.dao.IArticlesDao;
public class ThreadMajStock extends Thread {
/**
* nom du thread
*/
private String name;
/**
* objet d'accès aux données
*/
private IArticlesDao articlesDao;
/**
*
* @param name
* le nom du thread afin de l'identifier
* @param articlesDao
* l'objet d'accès aux données du sgbd
*/
public ThreadMajStock(String name, IArticlesDao articlesDao) {
this.name = name;
this.articlesDao = articlesDao;
}
/**
* décrémente le stock de l'article 3 d'une unité fait un suivi écran des
* opérations
*/
public void run() {
// follow-up
System.out.println(name + " lancé");
// modification stock article 3
articlesDao.changerStockArticle(3, -1);
// follow-up
System.out.println(name + " terminé");
}
}
|
- The class above decrements the stock of item #3 by 1
3.3.6.2. [domain] layer tests
The JUnit test class for the [domain] layer is as follows:
| package istia.st.articles.tests.domain;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.Article;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// test the ArticlesDaoSqlMap class
public class JunitModeleDomainArticles extends TestCase {
// an instance of the domain access class
private IArticlesDomain articlesDomain;
// an instance of the data access class
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// retrieves a domain access instance
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// retrieves a data access instance
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
// retrieve a specific item
public void testGetArticleById() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDomain.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
// screen display
private void listArticles() {
// reads the ARTICLES table
List articles = articlesDomain.getAllArticles();
// display read articles
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
// article purchases
public void testAchatPanier(){
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// reads the ARTICLES table
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// create a basket with two purchases
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// checks
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// shopping cart validation
articlesDomain.acheter(panier);
// checks
assertEquals(0,articlesDomain.getErreurs().size());
assertEquals(0,panier.getAchats().size());
// search article n° 3
article3=articlesDomain.getArticleById(3);
assertEquals(20,article3.getStockActuel());
// search article n° 4
article4=articlesDomain.getArticleById(4);
assertEquals(30,article4.getStockActuel());
// new basket
panier.ajouter(new Achat(article3,100));
// shopping cart validation
articlesDomain.acheter(panier);
// checks - we bought too much
// we must have an error
assertEquals(1,articlesDomain.getErreurs().size());
// search article n° 3
article3=articlesDomain.getArticleById(3);
// its stock must not have changed
assertEquals(20,article3.getStockActuel());
}
// withdraw purchases
public void testRetirerAchats(){
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// reads the ARTICLES table
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// create a basket with two purchases
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// checks
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// add a previously purchased item
panier.ajouter(new Achat(article3,10));
// checks
// the total must be increased to 1000
assertEquals(1000.0,panier.getTotal(),1e-6);
// always 2 items in the basket
assertEquals(2,panier.getAchats().size());
// qty item 3 increased to 20
Achat achat=(Achat)panier.getAchats().get(0);
assertEquals(20,achat.getQte());
// article 3 is removed from the basket
panier.enlever(3);
// checks
// the total must be increased to 400
assertEquals(400.0,panier.getTotal(),1e-6);
// 1 item only in basket
assertEquals(1,panier.getAchats().size());
// this must be article no. 4
assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
}
}
|
Comments:
- The test class uses its setUp method to store an instance of the class under test as well as an instance of the data access class. This last point is controversial. Theoretically, the test class should not need access to the [DAO] layer, which it is not even supposed to know about. Here, we have disregarded this "ethic," which, if followed, would have required us to create new methods in our [IArticlesDomain] interface.
| // 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");
}
|
- The object to be tested is provided by [Spring]. Above, we request the Spring bean named [articlesDomain]. This bean is defined in the Spring configuration file [spring-config-test-domain.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
As shown above, the [articlesDomain] bean is an instance of the [istia.st.articles.domain.AchatsArticles] class. This class has a constructor that expects, as a parameter, an object providing access to the [dao] layer of type [IArticlesDao]. Here, the configuration file specifies that this object is the bean named [articlesDao]. This forces Spring to instantiate this bean. The instantiation of the [articlesDao] bean was explained earlier. So, ultimately, two beans have been instantiated:
- [articlesDao] of type [istia.st.articles.dao.ArticlesDaoSqlMap]
- [articlesDomain] of type [istia.st.articles.domain.AchatsArticles]
These two instantiations are triggered by the first call to Spring:
| // récupère une instance d'accès au domaine
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
|
The [articlesDomain] bean is then retrieved. During the second call to 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] simply returns a reference to the [articlesDao] bean that was already created during the previous call. This is the singleton principle. If you request a bean from Spring, it instantiates it if it does not already exist; otherwise, it returns a reference to the existing bean.
3.4. Three-tier MVC web application
Next, we want to build the following three-tier web application:
The application will have an MVC architecture. The M model has been written and tested. It is the one described previously. It is provided to us in three archives [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. We need to write the C controller and the V views.
First, let’s consider a classic approach, where:
- the controller C is handled by a single servlet
- the views V are handled by JSP pages
3.5. MVC architecture based on a controller servlet and JSP pages
The application’s MVC architecture will be as follows:
| Business classes, data access classes, and the database |
| JSP pages |
| the servlet that processes client requests |
3.5.1. The model
It was presented earlier. It consists of the Java archives [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.5.2. The views
The views correspond to those presented at the beginning of this document:
| list.jsp | The views are located in the [vues] folder of the application  |
| info.jsp |
| cart.jsp |
| empty-cart.jsp |
| errors.jsp |
3.5.3. The Controller
The controller will consist of a single servlet named [WebArticles]. It will handle various client requests. These requests will be identified by the presence of an [action] parameter in the client's HTTP request:
| meaning | controller action | possible responses |
| the client wants the list of items | - requests the list of items from the business | - [LIST] - [ERRORS] |
| The client requests Information about one of the items displayed in the view [LIST] | - requests the item from the business layer | - [INFO] - [ERRORS] |
| The customer purchases an item | - requests the item from the business layer and adds it to the customer's cart | - [INFO] if quantity error - [LIST] if no error |
| the customer wants to remove an purchase from their cart | - retrieve the cart from the session and modify it | - [CART] - [EMPTY CART] - [ERRORS] |
| the customer wants to view their cart | - retrieves the cart from the session | - [SHOPPING CART] - [EMPTY CART] - [ERRORS] |
| The customer has finished shopping and proceeds to checkout | - updates the database with the stock levels of the purchased items - removes from the customer's cart the items for which have been confirmed | - [LIST] - [ERRORS] |
3.5.4. Application configuration
We will aim to configure the application to make it as flexible as possible with regard to changes such as:
- changes to the URLs of the various views
- changes to the classes implementing the [IArticlesDao] and [IArticlesDomain] interfaces
- changes to the DBMS, the database, or the articles table
3.5.5. URL changes
The view URL names will be placed in the application's [web.xml] configuration file along with a few other parameters:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
<init-param>
<param-name>urlMain</param-name>
<param-value>/main</param-value>
</init-param>
<init-param>
<param-name>urlErreurs</param-name>
<param-value>/vues/erreurs.jsp</param-value>
</init-param>
<init-param>
<param-name>urlListe</param-name>
<param-value>/vues/liste.jsp</param-value>
</init-param>
<init-param>
<param-name>urlInfos</param-name>
<param-value>/vues/infos.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPanier</param-name>
<param-value>/vues/panier.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPanierVide</param-name>
<param-value>/vues/paniervide.jsp</param-value>
</init-param>
<init-param>
<param-name>urlDebug</param-name>
<param-value>/vues/debug.jsp</param-value>
</init-param>
</servlet>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
</web-app>
|
In [web.xml]
- the URLs of the application's various views
- the name [springConfigFileName] of the Spring configuration file that will enable the creation of singleton objects for accessing the business and DAO layers
- the view [/vues/index.jsp] that will be displayed when the URL requested by the client is /<context>, where <context> is the application context
3.5.6. Changing the classes that implement the interfaces
In the spirit of three-tier architectures, the layers must be isolated from one another. This isolation is achieved as follows:
- layers communicate with each other via interfaces, not concrete classes
- The code of one layer never instantiates the class of another layer itself in order to use it. It simply requests an instance of the interface implementation from an external tool—in this case, [Spring]—for the layer it wishes to use. To do this, we know that it does not need to know the name of the implementation class, but only the name of the Spring bean for which it wants a reference.
In our application, the Spring configuration file could look like this:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
To access the [business] layer, a class in the [User Interface, UI] layer can request the [articlesDomain] bean. Spring will then instantiate an object of type [istia.st.articles.domain.AchatsArticles]. For this instantiation, it requires a bean of type [articlesDao], i.e., an object of type [istia.st.articles.dao.ArticlesDaoSqlMap]. Spring will then instantiate such an object. This instantiation will be based on the information contained in the [sqlmap-config-firebird.xml] file, the configuration file for data access via SqlMap. At the end of the operation, the [UI] class that requested the [articlesDomain] bean has the entire chain connecting it to the DBMS data:
The web application's independence from changes related to the DBMS or the database is ensured here by SqlMap's configuration files. There are two of them:
- the [sql-map-config-firebird.xml] file
| <?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>
|
This file refers to a Firebird database. Simply change the name of the JDBC driver to work with a different DBMS.
- The [articles.xml] file, which contains the various SQL statements required by the 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">
<!-- an alias to the istia.st.articles.dao.Article class -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- mapping ORM : row table ARTICLES - instance class Article -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- query SQL to obtain all items -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- query SQL to delete all items -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- the SQL query to insert an article -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>
<!-- the SQL query to delete a given item -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- query SQL to modify a given item -->
<statement id="modifyArticle">
update ARTICLES set nom=#nom#,
prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
id=#id#
</statement>
<!-- the SQL query to obtain a given item -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- query SQL to modify the stock of a given item -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#mouvement#
where id=#id# and stockActuel+#mouvement#>=0
</statement>
</sqlMap>
|
If the names of the product table or columns were to change, we would need to rewrite the queries in this configuration file without having to change the Java code. This would also be the case if a query were to be replaced by a stored procedure for performance reasons.
3.5.8. The overall architecture of the [webarticles] application
A Java web application is a puzzle with many pieces. Giving it an MVC architecture generally increases the number of these pieces. The structure of the [webarticles] application under [Eclipse] is as follows:
general structure - below are the
Java archives used by the
Eclipse.
spring: for Spring
ibatis: for SqlMap
log4j, commons-logging: for
from Spring and SqlMap
firebird: for the Firebird DBMS
mysql: for the MySQL DBMS
jstl, standard: for the
JSTL tag library
|
the Java source folder: contains the Java code
as well as the configuration files
Eclipse automatically copies
these files
to [WEB-INF/classes].
This is where the application will find them.
|
 |  |
the application's [WEB-INF] folder: contains the
application's [web.xml] descriptor as well as the
JSTL library definition files
| |
 |  |
3.5.9. JSP views
JSP views use the JSTL tag library.
To ensure consistency across the different views, they will share the same header, which displays the application name along with the menu:
The menu is dynamic and set by the controller. The controller includes an "actions" key attribute in the request sent to the JSP page, with an associated value of a Hastable[] array. Each element of this array is a dictionary intended to generate a menu option in the header. Each dictionary has two keys:
- href: the URL associated with the menu option
- link: the menu text
The other views of the application will use the header defined by [entete.jsp] using the following JSP tag:
<jsp:include page="entete.jsp"/>
At runtime, this tag will include the code from the [entete.jsp] page into the JSP page containing it. Since the page URL is a relative URL (no trailing /), the [entete.jsp] page will be searched for in the same directory as the page containing the <jsp:include> tag.
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. list.jsp
This view displays the list of items available for sale:
It is displayed following a request to /main?action=list or /main?action=cartvalidation. The controller request parameters are as follows:
| Hashtable[] object - the array of menu options |
| ArrayList of objects of type [Item] |
| String object - message to display at the bottom of the page |
Each [Info] link in the HTML table of articles has a URL in the form [?action=infos&id=ID], where ID is the id field of the displayed article.
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
This view displays information about an item and also allows it to be purchased:

It is displayed following a request /main?action=infos&id=ID or a request /main?action=achat&id=ID when the quantity purchased is incorrect. The controller request parameters are as follows:
| Hashtable[] object - the array of menu options |
| object of type [Article] - item to display |
| String object - message to display in case of an error with the quantity |
| String object - value to display in the [Qty] input field |
The [msg] and [qte] fields are used in case of an input error regarding the quantity:

This page contains a form that is submitted via the [Buy] button. The POST target URL is [?action=purchase&id=ID], where ID is the ID of the purchased item.
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. cart.jsp
This view displays the contents of the shopping cart:

It is displayed following a request to /main?action=cart or /main?action=remove&id=ID. The controller request parameters are as follows:
| Hashtable[] object - the array of menu options |
| object of type [Cart] - the cart to display |
Each [Remove] link in the HTML shopping cart table has a URL in the form [?action=removeitem&id=ID], where ID is the [id] field of the item to be removed from the cart.
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. emptycart.jsp
This view displays information indicating that the cart is empty:

It is displayed following a request to /main?action=cart or /main?action=remove&id=ID. The controller request parameters are as follows:
| Hashtable[] object - the array of menu options |
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. errors.jsp
This view is displayed in case of errors:

It is displayed following any request that results in an error, except for the purchase action with an incorrect quantity, which is handled by the [INFOS] view. The elements of the controller request are as follows:
| Hashtable[] object - the array of menu options |
| ArrayList of String objects representing the error messages to display |
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
This page is defined as the application's home page in the application's [web.xml] file:
| <?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>
|
The [index.jsp] view simply redirects the client to the application's entry point:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
|
3.5.10. The Controller
We still need to write the core of our web application, the controller. Its role is to:
- retrieve the client's request,
- process the action requested by the client using business classes,
- send the appropriate view in response.
3.5.10.1. Initializing the Controller
When the controller class is loaded by the servlet server, its [init] method is executed. This will happen only once. Once loaded into memory, the controller will remain there and process requests from various clients. Each client is handled by a separate execution thread, so the controller’s methods are executed simultaneously by different threads. Note that, for this reason, the controller must not have any fields that its methods could modify. Its fields must be read-only. They are initialized by the [init] method, which is its primary role. This method has the unique characteristic of being executed only once by a single thread. Therefore, there are no issues with concurrent access to the controller’s fields within this method. The purpose of the [init] method is to initialize the objects required by the web application, which will be shared in read-only mode by all client threads. These shared objects can be placed in two locations:
- the controller’s private fields
- the application's execution context (ServletContext)
The [init] method of the [webarticles] application will perform the following actions:
- check the [web.xml] file for the parameters necessary for the application to function properly. These were described in section 3.5.5.
- initialize a private field [ArrayList errors] containing a list of any errors. This list will be empty if there are no errors, but it will exist regardless.
- If errors occurred, the [init] method stops there. Otherwise, it creates an object of type [IArticlesDomain], which will be the business object that the controller uses for its needs. As explained in 3.5.6, the controller will request the bean it needs from the Spring framework. This instantiation operation may result in various errors. If so, they will, once again, be stored in the controller’s [errors] field.
3.5.10.2. doGet, doPost Methods
These two methods handle HTTP GET and POST requests from clients. They will be handled interchangeably. The [doPost] method may thus redirect to the [doGet] method or vice versa. The client request will be processed as follows:
- The [errors] field will be checked. If it is not empty, this means that errors occurred during application initialization and that the application cannot run. The [ERRORS] view will then be sent in response.
- The [action] parameter of the request will be retrieved and checked. If it does not correspond to a known action, the [ERRORS] view is sent with an appropriate error message.
- If the [action] parameter is valid, the client's request is passed to a procedure specific to the action for processing. The procedure handling the action [uneAction] will have the following 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. Handling Different Actions
The methods that handle the application's various possible actions are as follows:
method | request | processing | possible responses |
| GET /main?action=list | - request the list of items from the business class - display it | [LIST] or [ERRORS] |
| GET /main?action=info&id=ID | - retrieve the item with id=ID from the business class - display it | [INFO] or [ERRORS] |
| POST /main?action=purchase&id=ID - The quantity purchased is included in the posted parameters | - request the item with id=ID from the business class - Add it to the cart in the customer session | [LIST] or [INFO] or [ERRORS] |
| GET /main?action=removePurchase&id=ID | - remove the item with id=ID from the shopping list in the the customer's session | [CART] |
| GET /main?action=cart | - Display the client session | [CART] or [EMPTY_CART] |
| GET /main?action=cartvalidation | - Decrement the stock levels of all items in the customer's [LIST] or [ERRORS] | [LIST] or [ERRORS] |
3.5.10.4. The 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 {
// private fields
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String URL_MAIN = "urlMain";
private final String URL_ERREURS = "urlErreurs";
private final String URL_LISTE = "urlListe";
private final String URL_INFOS = "urlInfos";
private final String URL_PANIER = "urlPanier";
private final String URL_PANIER_VIDE = "urlPanierVide";
private final String URL_DEBUG = "urlDebug";
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters =
{
URL_MAIN,
URL_ERREURS,
URL_LISTE,
URL_INFOS,
URL_PANIER,
URL_PANIER_VIDE,
URL_DEBUG,
SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste";
private final String ACTION_PANIER = "panier";
private final String ACTION_ACHAT = "achat";
private final String ACTION_INFOS = "infos";
private final String ACTION_RETIRER_ACHAT = "retirerachat";
private final String ACTION_VALIDATION_PANIER = "validationpanier";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
public void init() {
// retrieve servlet initialization parameters
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// we memorize the error
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// mistakes?
if (erreurs.size() != 0) {
return;
}
// create a IArticlesDomain business layer access object
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// we memorize the error
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// memorize certain application urls
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// it's over
return;
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// check how the initialization of the servelet went
if (erreurs.size() != 0) {
// do we have the url of the error page?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
// action is processed
String action = request.getParameter("action");
if (action == null) {
// list of items
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// list of items
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// article info
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// purchase an item
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// basket display
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// remove an item from the basket
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// shopping cart validation
doValidationPanier(request, response);
return;
}
// unknown share
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// the error page is displayed
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// end
return;
}
private void doValidationPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// the buyer has confirmed his basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
// validate this basket
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// error recovery
ArrayList erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionPanier });
afficheErreurs(request, response, erreurs);
return;
}
// displays the list of items
request.setAttribute("message", "Votre panier a été validé");
doListe(request, response);
// end
return;
}
private void doRetirerAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// remove a purchase from the basket
try {
Panier panier =
(Panier) request.getSession().getAttribute("panier");
String strIdAchat = request.getParameter("id");
panier.enlever(Integer.parseInt(strIdAchat));
} catch (NumberFormatException ignored) {
} catch (NullPointerException ignored) {
}
// the basket is displayed
doPanier(request, response);
}
private void doPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// the basket is displayed
Panier panier = (Panier) request.getSession().getAttribute("panier");
// empty basket?
if (panier == null || panier.getAchats().size() == 0) {
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
.forward(request, response);
// end
return;
}
// there's something in the basket
request.setAttribute("panier", panier);
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionValidationPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER))
.forward(request, response);
// end
return;
}
private void doAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// purchase an item
// we recover the quantity
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// wrong qty
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
String url =
config.getInitParameter(URL_MAIN)
+ "?action=infos&id="
+ request.getParameter("id");
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// end
return;
}
// retrieve the client session
HttpSession session = request.getSession();
// we create the purchase
Article article = (Article) session.getAttribute("article");
Achat achat = new Achat(article, qté);
// the purchase is added to the customer's basket
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// we return to the list of items
String url = config.getInitParameter(URL_MAIN) + "?action=liste";
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// end
return;
}
private void afficheDebugInfos(
HttpServletRequest request,
HttpServletResponse response,
ArrayList infos)
throws ServletException, IOException {
// displays the list of items
request.setAttribute("infos", infos);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_DEBUG))
.forward(request, response);
// end
return;
}
public void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// idem get
doGet(request, response);
}
private void doInfos(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// error list
ArrayList erreurs = new ArrayList();
// retrieve the requested id
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// the key item id is requested
Article article = null;
try {
article=articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
if (article == null) {
// not normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// put the article in the session
request.getSession().setAttribute("article", article);
// the info page is displayed
request.setAttribute("actions", new Hashtable[] { hActionListe });
// request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_INFOS))
.forward(request, response);
// end
return;
}
private void afficheErreurs(
HttpServletRequest request,
HttpServletResponse response,
ArrayList erreurs)
throws ServletException, IOException {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// list of errors
ArrayList erreurs = new ArrayList();
// the list of items is requested
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// we memorize the error
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
// displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// end
return;
}
/**
* suivi console pour débogage
* @param message : le message à afficher
*/
private void affiche(String message) {
System.out.println(message);
}
}
|
We’ll let the reader take their time to read and understand this code. We hope the comments will help.
3.5.10.5. Application Testing
Let’s look at a few test screenshots. First, the application’s home page:

The requested URL was actually [http://localhost:8080/webarticles]. The reader will see that in the [web.xml] file, we define a home page for the application:
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
The [index.jsp] view is defined as follows:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
There was therefore a redirection to the URL [http://localhost:8080/webarticles/main?action=liste], as shown by the browser URL in the screenshot. The URL [/main?action=liste] was thus requested. Still in [web.xml], the URL /main is associated with the [webarticles] servlet:
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
Also in [web.xml], the [webarticles] servlet is associated with the [istia.st.articles.web.WebArticles] servlet:
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
The [istia.st.articles.web.WebArticles] servlet is therefore loaded by the Tomcat servlet container if it wasn’t already, and its [init] method is executed:
| public void init() {
// retrieve servlet initialization parameters
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// we memorize the error
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// mistakes?
if (erreurs.size() != 0) {
return;
}
// create a IArticlesDomain business layer access object
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// we memorize the error
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// memorize certain application urls
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// it's over
return;
}
|
Comments: The [init] method
- checks for the presence of certain configuration parameters
- instantiates a service to access the application domain using Spring
- sets an error list to indicate any initialization errors
- a number of private fields:
- errors: the list of errors detected by [init]
- [hActionListe, hActionPanier, hActionValidationPanier]: dictionaries. Each contains the information needed to display an option in the main menu shown by the [entete.jsp] view
- acticlesDomain: the service for accessing the application model
The [init] method is executed only once, upon the initial loading of the servlet. After that, one of the [doGet, doPost] methods is executed depending on the [GET, POST] type of the client request. Here, both methods do the same thing, and the code has been placed in [doGet]:
| public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// check how the initialization of the servelet went
if (erreurs.size() != 0) {
// do we have the url of the error page?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
// action is processed
String action = request.getParameter("action");
if (action == null) {
// list of items
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// list of items
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// article info
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// purchase an item
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// basket display
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// remove an item from the basket
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// shopping cart validation
doValidationPanier(request, response);
return;
}
// unknown share
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// the error page is displayed
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// end
return;
}
|
- The [doGet] method begins by checking whether any initialization errors occurred after the [init] method. If so, it displays the [ERRORS] view and the process ends.
- Otherwise, it retrieves the [action] parameter from the client’s request. Remember that the application was built to respond to requests that must include an [action] parameter.
- It executes the method associated with the action. Here, that will be the [doListe] method.
The [doListe] method is as follows:
| private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// error list
ArrayList erreurs = new ArrayList();
// the list of items is requested
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// we memorize the error
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
// displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// end
return;
}
|
- Recall that the [init] method has stored the service for accessing the application model (domain layer) in a private field of the servlet:
// champs privés
private IArticlesDomain articlesDomain = null;
- Using this access service, we can request the list of 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());
}
|
- If errors occur, the [ERRORS] view is sent:
| // mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
|
- otherwise, the [LIST] view is sent:
| // displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
|
In our example, everything went smoothly and we successfully obtained the [LIST] view. The reader is invited to review the code for the [LIST] view to verify that the dynamic parameters expected by this view are indeed provided above by the controller. The same type of verification should be performed for each view:
- find the view’s dynamic parameters
- ensure that the controller places these in the request attributes passed to the view
We will now simply outline the flow of screens encountered by an application user. The reader is invited to follow a line of reasoning similar to the previous one each time:
From the list of items, the user can select an item:
The buyer can purchase item #3 here. Let’s make a typo in the quantity:
The error has been flagged. Now, let’s purchase a few items:
The purchase has been recorded and the list of items has been re-displayed. Let’s check the shopping cart:
The purchase is indeed in the shopping cart. Let’s remove it:
The purchase has been removed from the shopping cart, and the cart has been reloaded. It is now empty.
Let’s buy 100 of item #3 and 2 of item #4:
The purchase of item #3 was not possible because we wanted to buy 100, but there were only 30 in stock. This purchase remained in the cart:
Item #4, however, was purchased, as shown by its new stock level of 39 (40-1):
3.6. MVC Architecture with Struts
3.6.1. General Application Architecture
Let’s revisit the application’s MVC architecture:
In the previous version:
- the controller was handled by a servlet
- the views were handled by JSP pages
- the model was handled by a set of three .jar files
In the Struts version:
- the controller will be handled by a servlet derived from Struts' generic [ActionServlet]
- The views will be handled by the same JSP pages as before, with a few minor differences
- The model will be handled by the same three archives
We will see that migrating the previous application to Struts involves the following tasks:
- actions that were previously handled in specific methods of the servlet/controller are now handled by instances of classes derived from Struts' [Action] class
- Writing the configuration files [web.xml] and [struts-config.xml]
- Making a few changes to the JSP pages
Let’s review the generic MVC architecture used by Struts:
| business classes, data access classes, and the database |
| JSP pages |
| the servlet that processes client requests, the [Action] objects, and the [ActionForm] beans associated with forms. |
- The controller is the heart of the application. All client requests pass through it. It is a generic servlet provided by STRUTS. In some cases, you may need to extend it. For simple cases, this is not necessary. This generic servlet retrieves the information it needs from a file most often called struts-config.xml.
- If the client request contains form parameters, the controller places them into a Bean object. The Bean objects created over time are stored in the session or the client request. This behavior is configurable. They do not need to be recreated if they have already been created.
- In the struts-config.xml configuration file, each URL to be processed programmatically (and thus not corresponding to a JSP view that could be requested directly) is associated with certain information:
- the name of the Action class responsible for processing the request. Here again, the instantiated Action object can be stored in the session or request.
- If the requested URL is parameterized (as when a form is submitted to the controller), the name of the bean responsible for storing the form data is specified.
- Armed with this information provided by its configuration file, upon receiving a URL request from a client, the controller is able to determine whether a bean needs to be created and which one. Once instantiated, the bean can verify whether the data it has stored—which comes from the form—is valid or not. A method of the bean called `validate` is automatically called by the controller. The bean is built by the developer. The developer therefore places the code that verifies the validity of the form data within the validate method. If the data is found to be invalid, the controller will not proceed further. It will pass control to a view whose name it finds in its configuration file. The interaction is then complete. Note that the developer can choose not to have the form’s validity checked. This is also done in the struts-config.xml file. In this case, the controller does not call the bean’s validate method.
- If the bean’s data is correct, or if there is no validation, or if there is no bean, the controller passes control to the Action object associated with the URL. It does this by calling the execute method of that object, passing it the reference to the bean it may have constructed. This is where the developer does what needs to be done: they may need to call business classes or data access classes. At the end of processing, the Action object returns to the controller the name of the view it must send in response to the client.
- In its configuration file, the controller will find the URL associated with the name of the view it was asked to display. It then sends the view. The interaction with the client is complete.
In our application, we will not use [Bean] objects as buffer objects between the client and the [Action] classes. The [Action] object will directly retrieve the parameters of the client’s request from the [HttpServletRequest] object it receives. This facilitates the porting of our initial application. The final architecture of our application will therefore be as follows:
| business classes, data access classes, and the database |
| the JSP pages |
| the servlet for processing client requests, [Action] objects |
3.6.2. The model
It was presented earlier. It consists of the Java archives [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.6.3. Application configuration
3.6.3.1. General architecture
The general architecture of the Eclipse project is as follows:

3.6.3.2. Data access configuration
Since the data access interface remains unchanged, the associated configuration files are the same as in the previous version. They are defined in [WEB-INF/src]:

In the screenshot above, the files [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] are those from the previous version.
3.6.3.3. The archives directory
In [WEB-INF/lib], you will find the same libraries as in the previous version, plus the one required by Struts:

3.6.3.4. Application configuration
The application is configured using two files: [web.xml, struts-config.xml] in the [WEB-INF] folder:

The [web.xml] file is as follows:
| <?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>
|
What does this file say?
- The application's home page is [views/index.jsp] (welcome-file)
- URL requests of the form *.do will be redirected to the [strutswebarticles] servlet (servlet-mapping)
- The [strutswebarticles] servlet is an instance of the [istia.st.articles.web.struts.MainServlet] class (servlet-name, servlet-class)
- This servlet accepts two initialization parameters
- the name of the Struts configuration file (config)
- The name of the Spring configuration file (springConfigFileName)
The [struts-config.xml] file is as follows:
| <?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>
|
What does this configuration file say?
- that our controller will handle the following URLs:
| to display the list of articles |
| to display the list of articles |
| to display information about a specific item |
| to purchase a specific item |
| to view the shopping cart |
| to remove a purchase from the shopping cart |
| to confirm a shopping cart |
- The actions listed above correspond one-to-one with the actions handled by the servlet in the previous version. For each of them, the following information is provided:
- the name of the class responsible for handling this action
- the possible responses (= views) after the action is processed. Only one of them will be selected by the controller.
- the name of a message file for the application (message-resources). Here, the file will exist but will be empty. It will not be used. It must be placed in the application’s [ClassPath]. Here it will be placed in [WEB-INF/classes]. In Eclipse, this is achieved by placing it in [WEB-INF/src]:

3.6.4. The JSP Views
The JSP views used here are also those from the previous version. Very few changes have been made: URLs of the form [?action=XX?id=YY& ...] have been changed to [/XX.do?id=YY&....]. We are repeating explanations already given here to avoid having the user go back. It is important to understand that the information passed to the view by the controller is exactly the same in both versions. Nothing has changed in this regard.
3.6.4.1. entete.jsp
To ensure consistency across the different views, they will share the same header, which displays the application name along with the menu:
The menu is dynamic and defined by the controller. The controller includes in the request sent to the JSP page a "actions" key attribute with an associated value of a Hastable[] array. Each element of this array is a dictionary intended to generate a menu option in the header. Each dictionary has two keys:
- href: the URL associated with the menu option
- link: the menu text
The other views of the application will use the header defined by [entete.jsp] using the following JSP tag:
<jsp:include page="entete.jsp"/>
When executed, this tag will include the code from the [entete.jsp] page into the JSP page containing it. Since the page URL is a relative URL (no trailing /), the [entete.jsp] page will be searched for in the same directory as the page containing the <jsp:include> tag.
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>
|
Comments: no changes from the previous version
3.6.4.2. list.jsp
This view displays the list of items available for sale:
It is displayed following a request to /main.do or /validerpanier.do. The controller request parameters are as follows:
| Hashtable[] object - the array of menu options |
| ArrayList of objects of type [Item] |
| String object - message to display at the bottom of the page |
Each [Info] link in the HTML table of articles has a URL in the form [/infos.do?id=ID], where ID is the id field of the displayed article.
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>
|
Comments: one change (highlighted above)
3.6.4.3. infos.jsp
This view displays information about an item and also allows it to be purchased:

It is displayed following a request to /infos.do?id=ID or a request to /achat.do?id=ID when the quantity purchased is incorrect. The elements of the controller request are as follows:
| object Hashtable[] - the array of menu options |
| object of type [Article] - item to display |
| String object - message to display in case of an error with the quantity |
| String object - value to display in the [Qty] input field |
The [msg] and [qte] fields are used in case of an input error regarding the quantity:

This page contains a form that is submitted via the [Buy] button. The POST target URL is [/achat.do?id=ID], where ID is the ID of the purchased item.
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>
|
Comments: a change (highlighted above)
3.6.4.4. cart.jsp
This view displays the contents of the shopping cart:

It is displayed following a request to /panier.do or /retirerachat.do?id=ID. The controller request parameters are as follows:
| Hashtable[] object - the array of menu options |
| object of type [ShoppingCart] - the shopping cart to display |
Each [Remove] link in the HTML shopping cart array has a URL in the form [removeitem.do?id=ID], where ID is the [id] field of the item to be removed from the cart.
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>
|
Comments: one change (highlighted above)
3.6.4.5. emptycart.jsp
This view displays information indicating that the cart is empty:

It is displayed following a request to /panier.do or /retirerachat.do?id=ID. The controller request parameters are as follows:
| Hashtable[] object - the array of menu options |
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>
|
Comments: no changes.
3.6.4.6. errors.jsp
This view is displayed in case of errors:

It is displayed following any request that results in an error, except for the purchase action with an incorrect quantity, which is handled by the [INFOS] view. The elements of the controller request are as follows:
| Hashtable[] object - the array of menu options |
| ArrayList of String objects representing the error messages to display |
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>
|
Comments: no changes.
3.6.4.7. index.jsp
This page is defined as the application's home page in the application's [web.xml] file:
| <?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>
|
The [index.jsp] view simply redirects the client to the application's entry point:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
Comments: one change (highlighted above)
3.6.5. The Struts Controller
Struts has a generic controller called [ActionServlet]. We know that a servlet has an [init] method that allows the application to be initialized when it starts. If we use Struts’ generic controller [ActionServlet], we do not have access to its [init] method. Here, we have tasks to perform when the application starts, primarily instantiating a model access object. Therefore, we need an [init] method. We thus extend the [ActionServlet] class in the following [MainServlet] class:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.apache.struts.action.ActionServlet;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
/**
* @author ST - ISTIA
*
*/
public class MainServlet extends ActionServlet {
// private fields
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters = { SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_ACHAT = "achat.do";
private final String ACTION_INFOS = "infos.do";
private final String ACTION_RETIRER_ACHAT = "retirerachat.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// getters - setters
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public ArrayList getErreurs() {
return erreurs;
}
public void setErreurs(ArrayList erreurs) {
this.erreurs = erreurs;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public void setHActionListe(Hashtable actionListe) {
hActionListe = actionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public void setHActionPanier(Hashtable actionPanier) {
hActionPanier = actionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
public void setHActionValidationPanier(Hashtable actionValidationPanier) {
hActionValidationPanier = actionValidationPanier;
}
public void init() throws ServletException{
// init parent class
super.init();
// retrieve servlet initialization parameters
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// we memorize the error
erreurs.add("Paramètre [" + parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// mistakes?
if (erreurs.size() != 0) {
return;
}
// create a IArticlesDomain business layer access object
try {
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource((String) config
.getInitParameter(SPRING_CONFIG_FILENAME))))
.getBean("articlesDomain");
} catch (Exception ex) {
// we memorize the error
erreurs.add("Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// memorize certain application urls
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// it's over
return;
}
}
|
Comments:
- The value of the class lies in its [init] method and its private fields
- The [init] method does the same thing as the [init] method in the controller from the previous version:
- it checks for the presence of certain configuration parameters
- It instantiates a service to access the application domain using Spring
- it sets up an error list to indicate any initialization errors
- Before starting work, the [init] method calls the [init] method of the parent class [ActionServlet]. This method will process the Struts configuration file [struts-config.xml].
- A number of private fields with their accessors are defined:
- errors: the list of errors detected by [init]
- [hActionListe, hActionPanier, hActionValidationPanier]: dictionaries. Each contains the information needed to display an option in the main menu shown by the [entete.jsp] view
- acticlesDomain: the service providing access to the application model
- The controller of a Struts application is accessible to the [Action] classes responsible for handling the various possible actions. These classes will have access to the preceding private fields because they are provided with public accessors.
This controller is instantiated by the [web.xml] file:
| <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>
|
Any URL ending in .do will be handled by an instance of the [istia.st.articles.web.struts.MainServlet] class
3.6.6. Struts application actions
3.6.6.1. Introduction
Each Struts action will be implemented as a class. In the previous version, each action was implemented as a method in the application controller. Writing the [Action] class usually involves:
- copying and pasting the method used in the previous version
- adapting the code to Struts conventions
3.6.6.2. main.do, list.do
These two actions are identical and defined in [struts-config.xml] as follows:
| <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>
|
When one of these actions [main.do, liste.do] is executed in a browser, the following result is obtained:

The code for the [ListeArticlesAction] class is as follows:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST - ISTIA
*
*/
public class ListeArticlesAction extends Action {
/**
* affichage de la liste des articles - s'appuie sur la couche [domain]
*
* @param mapping :
* configuration de l'action dans struts-config.xml
* @param form :
* le formulaire passé à l'action - ici aucun
* @param request :
* la requête HTTP du client
* @param response :
* la réponse HTTP au client
*/
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// list of errors
ArrayList erreurs = new ArrayList();
// the list of items is requested
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// we memorize the error
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
}
}
|
Comments:
- Writing the code for an [Action] class essentially consists of writing the code for its [execute] method
- A certain amount of information has been stored in the controller instance. We retrieve a reference to it using:
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
- We retrieve the list of initialization errors stored by the controller. If this list is not empty, the [ERRORS] view is sent to the client:
| // initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
|
The view that will actually be sent to the client is provided by [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>
This is the [/vues/erreurs.jsp] view. The reader is invited to verify what is expected by this view. This information is provided here by the action in the [request] object as attributes.
- Again, thanks to the controller, the action can retrieve the object providing access to the application model (domain layer):
// domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
- Once this is done, we can request the list of 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());
}
|
- If errors occur, the [ERRORS] view is sent:
| // mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- otherwise, the [LIST] view is sent:
| // displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
|
The view that will actually be sent to the client is provided by [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>
This is the [/vues/liste.jsp] view. The reader is invited to verify what is expected by this view. This information is provided here by the action in the [request] object as attributes.
3.6.6.3. infos.do
This action is used to provide information about one of the items displayed in the [LIST] view:
This action is configured as follows in [struts-config.xml]:
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
The code for the [InfosArticleAction] class is as follows:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class InfosArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// list of errors
ArrayList erreurs = new ArrayList();
// retrieve the requested id
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// the key item id is requested
Article article = null;
try {
article = articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
if (article == null) {
// not normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// put the article in the session
request.getSession().setAttribute("article", article);
// the info page is displayed
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
}
|
Comments:
- The beginning of the [execute] method is identical to the one discussed previously. This will also be the case for the other actions.
- The method retrieves the [id] parameter, which should normally be present in the URL. The URL should indeed be in the form [/infos.do?id=X]. Various checks are performed to verify the presence and validity of the [id] parameter. If there is a problem, the [ERRORS] view is rendered.
- If [id] is valid, the corresponding item is requested from the [domain] layer. If this throws an exception or if the item is not found, the [ERRORS] view is sent again.
- If everything goes well, the retrieved item is stored in the session. This is a debatable point. Here, we assume that the client may purchase this item. If they do, we will retrieve it from the session rather than requesting it again from the [domain] layer.
- Finally, the [INFOS] view is displayed. The view that will actually be sent to the client is provided by [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>
This is the [/vues/infos.jsp] view. The reader is invited to verify what is expected by this view. This information is provided here by the action in the [request] object as attributes.
3.6.6.4. purchase.do
This action is used to purchase the item displayed by the previous [INFOS] view:
Once the item is purchased, the [LIST] view is redisplayed (right view). If we look at the HTML code for the left view above, we see that the <form> tag is defined as follows:
| <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>
|
We can see that the form is submitted to the controller with the action [achat.do].
This action is configured as follows in [struts-config.xml]:
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
The code for the [AchatArticleAction] class is as follows:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST
*
*/
public class AchatArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// the quantity purchased is recovered
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// wrong qty
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
// retrieve the client session
HttpSession session = request.getSession();
// we retrieve the article placed in session
Article article = (Article) session.getAttribute("article");
// session expired?
if(article==null){
// the error page is displayed
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// create the new purchase
Achat achat = new Achat(article, qté);
// the purchase is added to the customer's basket
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// we return to the list of items
return mapping.findForward("afficherListeArticles");
}
}
|
Comments:
- The beginning of the [execute] method is identical to those studied previously.
- Let’s recall the format of the form sent to the controller:
| <form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
|
- There are two parameters in the request: [id]: item number, [qte]: quantity purchased.
- The presence and validity of the [qte] parameter are checked. If this parameter is found to be incorrect, the [INFOS] view is returned to the user along with an error message:
| // the list of errors on this action
ArrayList erreurs = new ArrayList();
// the quantity purchased is recovered
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// wrong qty
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
|
- The purchased item is retrieved from the session. The session may have expired. In this case, the [ERRORS] view is displayed:
| // retrieve the client session
HttpSession session = request.getSession();
// we retrieve the article placed in session
Article article = (Article) session.getAttribute("article");
// session expired?
if(article==null){
// the error page is displayed
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- If the session has not expired, the item is added to the cart, which is also retrieved from the session:
| // retrieve the client session
HttpSession session = request.getSession();
// we retrieve the article placed in session
Article article = (Article) session.getAttribute("article");
// session expired?
if(article==null){
// the error page is displayed
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// create the new purchase
Achat achat = new Achat(article, qté);
// the purchase is added to the customer's basket
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- Finally, we send the [LIST] view:
// on revient à la liste des articles
return mapping.findForward("afficherListeArticles");
- The view that will actually be sent to the client is provided by [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>
This is the [/main.do] view. This view is not a view but an action. The [/main.do] action described above will therefore execute and display the list of items.
3.6.6.5. cart.do
This action is used to display all of the customer's purchases. It is available via the [View Cart] menu option:
The HTML code associated with the [View Cart] link is as follows:
<a href="panier.do">Voir le panier</a>
The [panier.do] action is configured as follows in [struts-config.xml]:
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
The code for the [VoirPanierAction] class is as follows:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class VoirPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// the basket is displayed
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// empty basket
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
} else {
// there's something in the basket
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(), mainServlet.getHActionValidationPanier() });
return mapping.findForward("afficherPanier");
}
}
}
|
Comments:
- The beginning of the [execute] method is identical to those discussed previously.
- The shopping cart is retrieved from the session where it is normally located. The session may have expired, in which case there is no shopping cart. We do not treat this as an error but simply assume that the shopping cart is empty.
- If the cart is empty, the [EMPTY CART] view is displayed
- otherwise, it is the [PANIER] view
The views actually sent to the client are defined by the 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. checkout.do
This action removes an item from the shopping cart:
After the [retirerachat.do] action, the shopping cart is re-displayed (view on the right above). If we look at the HTML code for the [Confirm Cart] link in the view on the left above, we see the following:
<a href="retirerachat.do?id=3">Retirer</a>
The [retirerachat.do] action therefore receives, as a parameter, the ID of the item to be removed from the cart. This action is configured as follows in [struts-config.xml]:
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
The code for the [RetirerAchatAction] class is as follows:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class RetirerAchatAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// we pick up the basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
}
// retrieve the id of the item to be removed
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// we remove the purchase
panier.enlever(id);
// the basket is displayed again
return mapping.findForward("afficherPanier");
}
}
|
Comments:
- The beginning of the [execute] method is identical to those studied previously.
- The following code checks for the presence and validity of the [id] parameter. If it is incorrect, the [ERRORS] view is sent.
- Otherwise, the purchase is removed from the cart:
// on enlève l'achat
panier.enlever(id);
- then the cart is re-displayed:
// on affiche de nouveau le panier
return mapping.findForward("afficherPanier");
The view actually sent to the client is defined by the 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>
We can see that, in terms of views, it is the [/cart.do] action that will be triggered. This has already been described. It will display the [CART] or [EMPTY CART] view depending on the state of the cart.
3.6.6.7. confirmCart.do
This action is used to confirm the customer’s purchases. In practice, this involves a single action: the stock levels of the purchased items are reduced by the quantities purchased in the database. This action comes from the following menu:
The HTML code for the [Confirm Cart] link is as follows:
<a href="validerpanier.do">Valider le panier</a>
When this link is clicked, the stock levels are updated and the list of items is displayed again.
This action is configured as follows in [struts-config.xml]:
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
The code for the [ValiderPanierAction] class is as follows:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class ValiderPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// the buyer has confirmed his basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// validate basket
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// recover any errors
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
// everything looks OK - the item list is displayed
return mapping.findForward("afficherListeArticles");
}
}
|
Comments:
- The beginning of the [execute] method is identical to those studied previously.
- We retrieve the shopping cart from the session. If the session has expired, we display the [ERRORS] view:
| // the list of errors on this action
ArrayList erreurs = new ArrayList();
// the buyer has confirmed his basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- We process the purchases in the cart. Errors may occur if stock levels are insufficient to fulfill the purchases. In this case, we display the [ERRORS] view:
| // validate basket
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// recover any errors
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
|
- If everything went well, we display the list of items again:
// tout semble OK - on affiche la liste des articles
return mapping.findForward("afficherListeArticles");
The view actually sent to the client is defined by the 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>
We can see that, in terms of the view, it is the [/main.do] action that will be triggered. This has already been described. It will display the [LIST] view.
3.7. MVC Architecture with Spring
3.7.1. General Application Architecture
Let’s revisit the application’s MVC architecture:
In the first version:
- the controller was handled by a servlet
- the views were handled by JSP pages
- the model was handled by a set of three .jar files
In the Struts version:
- the controller was handled by a servlet derived from Struts' generic [ActionServlet]
- the views were handled by the same JSP pages as in the [Struts] version
- The model was handled by the same three archives
In the Spring version:
- the controller will be handled by a servlet provided by Spring [DispatcherServlet]
- The views will be handled by the same JSP pages as before, with a few minor differences
- The model will be handled by the same three files
We will find that migrating our application from Struts to Spring is straightforward if we are willing to forego using all the elements recommended for a standard Spring MVC architecture. The main changes are as follows:
- actions that were previously handled in specific methods of the servlet/controller, or by instances of classes derived from the [Action] class in Struts, are now handled by class instances implementing the Spring [Controller] interface
- The required configuration files are as follows:
- [web.xml] because this is a web application. This file contains a listener that, when the application is initialized, will use the [applicationContext.xml]
- [applicationContext.xml] to create the beans required by the application, particularly the model access service bean
- The JSP views will be identical to those in Struts. We will need to create a new one.
Let’s recall the STRUTS MVC architecture used in the previous version:
| business classes, data access classes, and the database |
| the JSP pages |
| the servlet for processing client requests, [Action] objects |
With Spring, we use the same architecture:
| business classes, data access classes, and the database |
| the JSP pages |
| the servlet that processes client requests, objects implementing the [Controller] interface |
- The controller is the heart of the application. All client requests pass through it. It is a generic servlet provided by SPRING. It is of type [DispatcherServlet]. From now on, we will refer to this controller as the [Spring] controller.
- The [Spring] controller will route the client’s request to one of the [Controller] instances. There will be one such instance per action to be processed. This is defined in the requested URL, just as with Struts. Thus, we will know that the requested action is the [list] action because the requested URL is [list.do]
- If C is the application context, the [Spring] controller uses a [C-servlet.xml] file that serves the same role as the struts-config.xml configuration file in the Struts version. For each action to be processed by the application, we associate the name of the Controller-type class responsible for handling the request.
- The controller hands control over to the Controller-type object associated with the action. It does this by calling the handleRequest method of that object and passing the client’s request to it. This is where the developer performs the necessary tasks: they may need to call business logic classes or data access classes. At the end of processing, the Controller object returns to the controller the name of the view it must send in response to the client.
- In its configuration file, the controller will find the URL associated with the name of the view it has been asked to display. It then sends the view. The interaction with the client is complete.
3.7.2. The Model
It is the same as in the two previous versions. It consists of the Java archives [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.7.3. Application configuration
3.7.3.1. General Architecture
The general architecture of the Eclipse project is as follows:

3.7.3.2. Data access configuration
Since the data access interface remains unchanged, the associated configuration files are the same as in the previous version. They are defined in [WEB-INF/src]:

In the screenshot above, the files [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] are those from previous versions.
3.7.3.3. The archives directory
In [WEB-INF/lib], you will find the same libraries as in the previous version, except for those from Struts, which is no longer required:

3.7.3.4. Application configuration
The application is configured using three files: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] in the [WEB-INF] folder:

The [web.xml] file is as follows:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- application spring context loader -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- the servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- url mapping -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- entry document -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
What does this file say?
- The application's home page is [/vues/index.jsp] (welcome-file)
- URL requests of the form *.do will be redirected to the [springwebarticles] servlet (servlet-mapping)
- The [springwebarticles] servlet is an instance of the [org.springframework.web.servlet.DispatcherServlet] class (servlet-name, servlet-class) provided by Spring.
- The [org.springframework.web.context.ContextLoaderListener] listener will be launched when the application starts. Its main role will be to instantiate the Spring beans defined in the [applicationContext.xml] file
The [applicationContext.xml] file is as follows:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- web application configuration-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
Some elements are familiar, while others are less so. Three beans will be instantiated during application initialization:
- articlesDao: service providing access to the [dao] layer
- articlesDomain: service providing access to the model
- config: a bean in which we will gather the information that all clients must share. This bean will play the role traditionally played by the application context but with typed rather than untyped information.
The last file [springwebarticles.xml] defines the actions accepted by the application in a manner very similar to that used by the Struts file [struts-config.xml]. Its content is as follows:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- stock managers = controllers -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- application mapping-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- view manager -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- message file -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
What does this configuration file say?
- that our controller will handle the following URLs:
| to display the list of articles |
| to display the list of articles |
| to display information about a specific item |
| to purchase a specific item |
| to view the shopping cart |
| to remove a purchase from the shopping cart |
| to confirm a shopping cart |
- The actions listed above correspond one-to-one with the actions handled by the controller in previous versions. For each one, the name of the class responsible for handling it is specified. Let’s take the example of the [/panier.do] action:
- it must be handled by the [VoirPanierController] bean. This name is arbitrary. It is simply a key.
<prop key="/panier.do">VoirPanierController</prop>
- The key [VoirPanierController] is the name of a bean defined in the same configuration file:
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
- The [VoirPanierController] bean defines:
- the class to instantiate [istia.st.articles.web.spring.VoirPanierController] to handle the action
- how to instantiate it. Here, the [config] bean defined by [applicationContext.xml] and instantiated at application startup is provided as a parameter. This will be done for all [Controller] actions. Thus, each of them will have, in a private field, the [config] object in which it will find all the information shared among all clients.
- How view names should be resolved:
<!-- 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>
As with Struts, the [Controller] instance that processes an action will return, after processing, a key to the Spring controller to indicate which view it should display. Based on this key, there may be various strategies for generating the view associated with the key. The strategy used is the one defined by the [viewResolver] bean. Here, this bean is associated with the [org.springframework.web.servlet.view.InternalResourceViewResolver] class with various initialization parameters. Without going into detail, the [viewResolver] bean specifies here that if the view key is "XX", then the generated view will be [/views/XX.jsp]. The type of views sent to the client can be changed in various ways:
- by changing the implementation class for the [viewResolver] bean
- by changing the initialization parameters of the implementation class
Thus, you can switch from an HTML view to an XML view simply by changing the value of the [viewResolver] bean
- the name of a message file for the application (messageSource). Here, the file will exist but will be empty. It will not be used. It must be placed in the application’s [ClassPath]. Here it will be placed in [WEB-INF/classes]. In Eclipse, this is achieved by placing it in [WEB-INF/src]:

3.7.4. The JSP views
The JSP views used will be those used by Struts. None are modified:

A single new view is created: redirpanier.jsp. It is used solely to redirect the client to the [/panier.do] action. Its code is as follows:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
Readers are encouraged to review the definitions of the various views in the Struts version.
3.7.5. Action Processing
The classes required for processing the various actions have been grouped in the [istia.st.articles.web.spring] package:

Let’s review how the Spring application works using an example:
- The user requests the URL [http://localhost:8080/springwebarticles/main.do]

What happened?
- The [web.xml] file of the [springwebarticles] application was consulted:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- application spring context loader -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- the servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- url mapping -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- entry document -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
- If this was the first request to the application, a number of things were triggered:
- the listener [org.springframework.web.context.ContextLoaderListener] was loaded
- it parsed the configuration file [applicationContext.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- web application configuration-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
- The beans above were created in the application context
- The [springwebarticles-servlet.xml] file was then used:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- stock managers = controllers -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- application mapping-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- view manager -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- message file -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
- The [Controller] beans defined by this file have also been created
- Everything is now in place to handle the client's request. The request was: [http://localhost:8080/springwebarticles]. Here, we are not requesting a URL from the context but the context itself. It is therefore the [welcome-file] section of the [web.xml] file that is used.
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
- The [index.jsp] view is as follows:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
- The client is therefore prompted to redirect to the URL [http://localhost:8080/springwebarticles/main.do]. It does so.
- The Spring controller then receives a new request. It uses its [springwebarticles-servlet.xml] file:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- stock managers = controllers -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
....
</bean>
<!-- application mapping-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
</beans>
|
- This file tells it that the action [/main.do] must be handled by the [ListController] bean.
- The client's request is passed to the [handleRequest] method of the [ListController] bean. This method does its job and returns the key of the view to be displayed to the controller. Here, if all goes well, this key will be [list].
- The Spring controller uses the [viewResolver] bean from the [springwebarticles-servlet.xml] configuration file to determine the view associated with this key. Here, it will be the [/vues/liste.jsp] view
- The view [/vues/liste.jsp] is sent to the client
3.7.6. Initializing the Spring application
We mentioned that when the application starts, the beans in the [applicationContext.xml] file are instantiated:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- web application configuration-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
We are familiar with the beans [articlesDao, articlesDomain] but not the bean [config]. This bean is defined by the following Java class:
| package istia.st.articles.web.spring;
import java.util.Hashtable;
import istia.st.articles.domain.IArticlesDomain;
/**
* @author ST - ISTIA
*/
public class Config {
// private fields
private IArticlesDomain articlesDomain = null;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private final String lienActionListe = "Liste des articles";
private final String lienActionPanier = "Voir le panier";
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// getters-setters
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
// init web application
public void init() {
// memorize certain application urls
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// it's over
return;
}
}
|
This class performs the same function as the [init] method of a web application servlet. It initializes the application. Here, this is done as follows:
- because the [config] bean is defined as follows in [applicationContext.xml]:
<!-- la configuration de l'application web-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
When it is created, its private field [articlesDomain] is initialized
- then, due to the [init-method="init"] attribute of the bean above, the [init] method of the class associated with the bean is executed. Here, it initializes the three dictionaries [hActionListe, hActionPanier, hActionValidationPanier] used to generate the three possible menu links offered to the user.
- Public accessors are created to make these private fields accessible to instances of type [Controller] that will handle the actions.
3.7.7. The [Controller] actions of the Spring application
3.7.7.1. Introduction
Each Spring action will be the subject of a class of type [Controller]. In the Struts version, each action was the subject of a class of type [Action]. Writing the [Controller] class most often consists of:
- copying and pasting the [Action] class that was used in the Struts version
- adapting the code to Spring conventions
3.7.7.2. main.do, liste.do
These two actions are identical and defined in [springwebarticles-servlet.xml] by:
| <!-- 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>
|
They are associated with the [istia.st.articles.web.spring.ListController] class, which we will discuss in detail shortly. When one of these actions is executed in a browser, the following result is obtained:

The code for the [istia.st.articles.web.spring.ListController] class is as follows:
| package istia.st.articles.web.spring;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ListController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the list of items is requested
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// we memorize the error
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// send error view
return new ModelAndView("erreurs");
}
// displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// send view
return new ModelAndView("liste");
}
}
|
Comments:
- The class has a private field [config]. This field was initialized by Spring when the [ListController] bean was instantiated:
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
As shown above, the [config] field of [ListController] is initialized with the [config] bean. What is this? It is the [config] bean defined in [applicationContext.xml], i.e., an instance of [istia.st.articles.web.spring.Config] described above.
- Writing the code for a [Controller] class essentially involves writing the code for its [handleRequest] method
- We request the list of articles from the model. This is accessible via [config.getArticlesDomain()]. If an exception occurs, the [ERRORS] view is rendered. The result returned by [handleRequest] must be of type [ModelAndView]. This class can be instantiated in various ways. Here, and this will always be the case, we create an instance of [ModelAndView] by passing it the key of the view to be displayed. Recall that, based on the configuration of the [viewResolver] bean, requesting the view with key XX will result in the [/vues/XX.jsp] view being sent.
| // 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");
}
|
- If there are no errors, the [LIST] view is sent:
| // 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
This action is used to provide information about one of the items displayed in the [LIST] view:
This action is defined in [springwebarticles-servlet.xml] by:
| <!-- 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>
|
The code for the [InfosController] class is as follows:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class InfosController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// error list
ArrayList erreurs = new ArrayList();
// retrieve the requested id
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// the id key item is requested
Article article = null;
try {
article = config.getArticlesDomain().getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
if (article == null) {
// not normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// put the article in the session
request.getSession().setAttribute("article", article);
// the info page is displayed
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("infos");
}
}
|
Comments:
- The [handleRequest] method retrieves the [id] parameter, which should normally be present in the URL. The URL should indeed be in the form [/infos.do?id=X]. Various checks are performed to verify the presence and validity of the [id] parameter. If there is a problem, the [ERRORS] view is displayed.
- If [id] is valid, the corresponding item is requested from the [domain] layer. If this throws an exception or if the item is not found, the [ERROR] view is sent again.
- If everything goes well, the retrieved item is stored in the session. This is a debatable point. Here, we assume that the customer might purchase this item. If they do, we will retrieve it from the session rather than requesting it again from the [domain] layer.
- Finally, the [INFO] view is displayed.
3.7.7.4. purchase.do
This action is used to purchase the item displayed by the previous [INFOS] view:

If we look at the HTML code for this view, we see that the <form> tag is defined as follows:
<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>
We can see that the form is submitted to the controller with the [achat.do] action.
This action is configured as follows in [springwebarticles-servlet.xml]:
<!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/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>
The code for the [AchatController] class is as follows:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class AchatController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// the quantity purchased is recovered
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// wrong qty
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
// retrieve the client session
HttpSession session = request.getSession();
// we retrieve the session item
Article article = (Article) session.getAttribute("article");
// session expired?
if (article == null) {
// the error page is displayed
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// create the new purchase
Achat achat = new Achat(article, qté);
// the purchase is added to the customer's basket
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// we return to the list of items
return new ModelAndView("index");
}
}
|
Comments:
- Let's review the format of the form sent to the controller:
<form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
- There are two parameters in the request: [id]: item number, [qte]: quantity purchased.
- The presence and validity of the [qte] parameter are checked. If this parameter is found to be incorrect, the [INFOS] view is returned to the user along with an error message:
| // 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");
}
|
- The purchased item is retrieved from the session. The session may have expired. In this case, the [ERRORS] view is sent:
| // 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");
}
|
- If the session has not expired, the item is added to the cart, which is also retrieved from the 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);
|
- Finally, we send the [LIST] view:
// on revient à la liste des articles
return new ModelAndView("index");
- Above, we send the view [/views/index.jsp]. We know that this view instructs the client browser to redirect to the URL [/main.do]. It is this redirect that will display the list of items.
3.7.7.5. cart.do
This action is used to display all of the customer's purchases. It is available via the menu:

The HTML code associated with the link above is as follows:
<a href="panier.do">Voir le panier</a>
The page returned by this link is as follows:

This action is configured as follows in [springwebarticles-servlet.xml]:
| <!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/panier.do">VoirPanierController</prop>
...
</props>
</property>
</bean>
...
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
The code for the [VoirPanierController] class is as follows:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class VoirPanierController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the basket is displayed
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// empty basket
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("paniervide");
} else {
// there's something in the basket
request.setAttribute("actions", new Hashtable[] {
config.getHActionListe(), config.getHActionValidationPanier() });
return new ModelAndView("panier");
}
}
}
|
Comments:
- The shopping cart is retrieved from the session where it is normally stored. The session may have expired, in which case there is no shopping cart. We do not treat this as an error but simply assume that the shopping cart is empty.
- If the cart is empty, the [EMPTY CART] view is displayed
- otherwise, the [CART] view is displayed
3.7.7.6. removePurchase.do
This action is used to remove a purchase from the cart:

If we look at the HTML code for the link above, we see the following:
<a href="retirerachat.do?id=3">Retirer</a>
The [retirerachat.do] action therefore receives, as a parameter, the ID of the item to be removed from the cart. This action is configured as follows in [springwebarticles-servlet.xml]:
| <!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/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>
|
The code for the [RetirerAchatController] class is as follows:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class RetirerAchatController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// we pick up the basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// retrieve the id of the item to be removed
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// we remove the purchase
panier.enlever(id);
// the basket is displayed again
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
}
}
|
Comments:
- The code checks for the presence and validity of the [id] parameter. If it is incorrect, the [ERRORS] view is sent.
- Otherwise, the item is removed from the cart:
// on enlève l'achat
panier.enlever(id);
- then the cart is reloaded:
// on affiche de nouveau le panier
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
Let's review the view code [/vues/redirpanier.jsp]:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
We can see that the client will be redirected to the action [/panier.do]. This has already been described. It will display the view [PANIER] or [PANIERVIDE] depending on the state of the shopping cart.
3.7.7.7. confirmcart.do
This action is used to confirm the customer’s purchases. In practice, this involves a single action: the stock levels of the purchased items are decremented by the quantities purchased in the database. This action comes from the following menu:

The HTML code for the [Confirm Cart] link is as follows:
<a href="validerpanier.do">Valider le panier</a>
When this link is clicked, the stock levels are updated, and the list of items is displayed again.
This action is configured as follows in [springwebarticles-servlet.xml]:
| <!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/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>
|
The code for the [ValiderPanierController] class is as follows:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ValiderPanierController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// the buyer has confirmed his basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// validate basket
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// recover any errors
erreurs = config.getArticlesDomain().getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe(), config.getHActionPanier() });
return new ModelAndView("erreurs");
}
// everything looks OK - the item list is displayed
return new ModelAndView("index");
}
}
|
Comments:
- We retrieve the shopping cart from the session. If the session has expired, we display the [ERRORS] view:
| // 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");
}
|
- We process the purchases in the cart. Errors may occur if stock levels are insufficient to fulfill the purchases. In this case, we display the [ERRORS] view:
| // 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");
}
|
- If everything went well, we display the list of items again:
// tout semble OK - on affiche la liste des articles
return new ModelAndView("index");
We know that the view [/vues/index.jsp] redirects the client to the action [/main.do]. This action will display the [LISTE] view.