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,
NAME VARCHAR(30) NOT NULL,
PRICE NUMERIC(15,2) NOT NULL,
CURRENTSTOCK INTEGER NOT NULL,
MINIMUM_STOCK INTEGER NOT NULL
);
/* constraints */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_CURRENT_STOCK check (CURRENT_STOCK >= 0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM >= 0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRICE check (PRICE >= 0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NAME check (NAME <> '');
/* 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 name;
private double price;
private int currentStock;
private int minimumStock;
/**
* default constructor
*/
public Item() {
}
public Article(int id, String name, double price, int currentStock,
int minimumStock) {
// initialize instance attributes
setId(id);
setName(name);
setPrice(price);
setCurrentStock(currentStock);
setMinStock(minStock);
}
// getters - setters
public int getId() {
return id;
}
public void setId(int id) {
// Is the ID valid?
if (id < 0)
throw new UncheckedAccessArticlesException("Invalid id[" + id + "]");
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
// Is the name valid?
if(name == null || name.trim().equals("")){
throw new UncheckedAccessArticlesException("The name is [null] or empty");
}
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
// Is the price valid?
if(price < 0) throw new UncheckedAccessArticlesException("Invalid price [" + price + "]");
this.price = price;
}
public int getCurrentStock() {
return currentStock;
}
public void setCurrentStock(int currentStock) {
// Is the stock valid?
if (currentStock < 0)
throw new UncheckedAccessArticlesException("currentStock[" + currentStock + "] is invalid");
this.currentStock = currentStock;
}
public int getMinimumStock() {
return stockMinimum;
}
public void setStockMinimum(int stockMinimum) {
// Is the stock valid?
if (stockMinimum < 0)
throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] is invalid");
this.stockMinimum = stockMinimum;
}
public String toString() {
return "[" + id + "," + name + "," + price + "," + currentStock + ","
+ 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: list of all articles
*/
public List getAllArticles();
/**
* @param anArticle:
* : the article to add
*/
public int addArticle(Article anArticle);
/**
* @param articleId:
* ID of the article to delete
*/
public int deleteArticle(int articleId);
/**
* @param article:
* the article to modify
*/
public int modifyArticle(Article anArticle);
/**
* @param idArticle :
* : ID of the article being searched for
* @return: the found article or null
*/
public Article getArticleById(int articleId);
/**
* clears the article table
*/
public void clearAllArticles();
/**
*
* @param itemId ID of the item whose stock is being updated
* @param movement value to add to stock (signed value)
*/
public int updateItemStock(int itemId, int movement);
}
|
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 addArticle(Article article) {}
public synchronized int deleteArticle(int articleId) {}
public synchronized int updateArticle(Article anArticle) {}
public synchronized Article getArticleById(int idArticle) {}
public synchronized void clearAllArticles() { }
public synchronized int updateArticleStock(int articleId, int movement) {}
}
|
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 for the istia.st.articles.dao.Article class -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- ORM mapping: ARTICLES table row - Article class instance -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="name" column="NAME"/>
<result property="price" column="PRICE"/>
<result property="currentStock" column="CURRENTSTOCK"/>
<result property="minStock" column="MINSTOCK"/>
</resultMap>
<!-- the SQL query to retrieve all items -->
<statement id="getAllArticles" resultMap="article">
select id, name, price,
currentStock, minimumStock from ARTICLES
</statement>
<!-- the SQL query to delete all items -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- the SQL query to insert an item -->
<statement id="insertArticle">
insert into ARTICLES (id, name, price,
currentStock, minimumStock) values
(#id#,#name#,#price#,#currentStock#,#minStock#)
</statement>
<!-- the SQL query to delete a given item -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- the SQL query to modify a given item -->
<statement id="modifyArticle">
update ARTICLES set name=#name#,
price=#price#,currentStock=#currentStock#,minStock=#minStock# where
id=#id#
</statement>
<!-- the SQL query to retrieve a specific article -->
<statement id="getArticleById" resultMap="article">
select id, name, price,
currentStock, minimumStock FROM ARTICLES where id=#id#
</statement>
<!-- the SQL query to modify the stock of a given item -->
<statement id="changeItemStock">
update ARTICLES set
currentStock=currentStock+#movement#
where id=#id# and currentStock+#movement#>=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 buy(ShoppingCart shoppingCart);
List getAllArticles();
Article getArticleById(int itemId);
ArrayList getErrors();
}
|
| 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 ItemPurchases implements IArticlesDomain {
// Fields
private IArticlesDao articlesDao;
private ArrayList errors;
// Constructors
public ItemPurchases(IItemDao itemDao) { }
// Methods
public ArrayList getErrors() {}
public List getAllArticles() {}
public Article getArticleById(int id) {}
public void buy(Cart cart) { }
}
|
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 Purchase {
// Fields
private Article article;
private int qty;
// Constructors
public Purchase(Item item, int qty) { }
// Methods
public double getTotal() {}
public Article getArticle() {}
public void setArticle(Article article) { }
public int getQuantity() {}
public void setQuantity() { }
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 ShoppingCart {
// Fields
private ArrayList purchases;
// Constructors
public ShoppingCart() { }
// Methods
public ArrayList getPurchases() {}
public void add(Purchase purchase) { }
public void remove(int purchaseId) { }
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 for the ArticlesDaoSqlMap class
public class JunitModelDaoArticles 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 the articles
listArticles();
}
public void testClearAllArticles() {
// clears the articles table
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
}
public void testAddArticle() {
// Clear the contents of the ARTICLES table
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Insert
articlesDao.addArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.addArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
//displays it
listArticles();
}
public void testDeleteArticle() {
// Delete the contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Insert
articlesDao.addArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.addArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// deletion
articlesDao.deleteArticle(4);
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(1, articles.size());
// displays the table
listArticles();
}
public void testModifyArticle() {
// Delete the contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.addArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.addArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article anArticle = articlesDao.getArticleById(3);
assertEquals(anArticle.getName(), "article3");
anArticle = articlesDao.getArticleById(4);
assertEquals(anArticle.getName(), "article4");
// modification
articlesDao.modifyArticle(new Article(4, "article4", 44, 44, 44));
// getById
anArticle = articlesDao.getArticleById(4);
assertEquals(anArticle.getPrice(), 44, 1e-6);
// display the table
listArticles();
}
public void testGetArticleById() {
// Clear the contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Insert
articlesDao.addArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.addArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article anArticle = articlesDao.getArticleById(3);
assertEquals(anArticle.getName(), "article3");
anArticle = articlesDao.getArticleById(4);
assertEquals(anArticle.getName(), "article4");
}
private void listArticles() {
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
// displays the retrieved articles
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
public void testChangeArticleStock() throws InterruptedException {
// Clear the contents of ARTICLES
articlesDao.clearAllArticles();
// insertion
int nbArticles = articlesDao.addArticle(new Article(3, "article3",
30, 101, 3));
assertEquals(nbArticles, 1);
nbArticles = articlesDao.addArticle(new Article(4, "article4", 40,
40, 4));
assertEquals(nbArticles, 1);
// Create 100 threads to update the stock of item 3
Thread[] tasks = new Thread[100];
for (int i = 0; i < tasks.length; i++) {
tasks[i] = new StockUpdateThread("thread-" + i, itemDao);
tasks[i].start();
}
// wait for the threads to finish
for (int i = 0; i < tasks.length; i++) {
tasks[i].join();
}
// retrieve article 3 and check its stock
Article anArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
assertEquals(1, unArticle.getCurrentStock());
// update stock for item 4
boolean error = false;
int lineCount = articlesDao.updateArticleStock(4, -100);
assertEquals(0, nbLines);
// display the table
listItems();
}
}
|
Comments:
- The test class stores, using its setUp method, an instance of the class under test:
| // 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");
}
|
- 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>
<!-- the 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 {
/**
* thread name
*/
private String name;
/**
* data access object
*/
private IArticlesDao articlesDao;
/**
*
* @param name
* : the thread name for identification
* @param articlesDao
* the DBMS data access object
*/
public ThreadMajStock(String name, IArticlesDao articlesDao) {
this.name = name;
this.articlesDao = articlesDao;
}
/**
* Decreases the stock of item 3 by one unit and displays a progress bar for
* operations
*/
public void run() {
// tracking
System.out.println(name + " started");
// update stock for item 3
articlesDao.updateStockItem(3, -1);
// tracking
System.out.println(name + " completed");
}
}
|
- 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.Purchase;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.ShoppingCart;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// Test for the ArticlesDaoSqlMap class
public class JunitDomainModelArticles 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 article
public void testGetArticleById() {
// clearing the contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Insert
articlesDao.addArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.addArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// getById
Article anArticle = articlesDomain.getArticleById(3);
assertEquals(unArticle.getName(), "article3");
anArticle = articlesDao.getArticleById(4);
assertEquals(anArticle.getName(), "article4");
}
// display on screen
private void listArticles() {
// reads the ARTICLES table
List articles = articlesDomain.getAllArticles();
// display the retrieved articles
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
// purchasing items
public void testCartPurchase(){
// clear the contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Insert
Article article3 = new Article(3, "article3", 30, 30, 3);
articlesDao.addArticle(article3);
Article article4 = new Article(4, "article4", 40, 40, 4);
articlesDao.addArticle(article4);
// reads the ARTICLES table
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// Create a shopping cart with two items
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.add(new Purchase(item3, 10));
shoppingCart.add(new Purchase(item4, 10));
// Checks
assertEquals(700.0, shoppingCart.getTotal(), 1e-6);
assertEquals(2, basket.getItems().size());
// validate cart
domain.buy(cart);
// checks
assertEquals(0, itemsDomain.getErrors().size());
assertEquals(0, cart.getPurchases().size());
// find item #3
item3 = domainItems.getItemById(3);
assertEquals(20, item3.getCurrentStock());
// find item #4
item4 = itemsDomain.getItemById(4);
assertEquals(30, article4.getCurrentStock());
// new shopping cart
cart.add(new Purchase(item3, 100));
// validate cart
articlesDomain.buy(shoppingCart);
// checks - we bought too much
// there should be an error
assertEquals(1, articlesDomain.getErrors().size());
// find item #3
item3 = domainItems.getItemById(3);
// its stock shouldn't have changed
assertEquals(20, article3.getCurrentStock());
}
// remove purchases
public void testRemovePurchases(){
// Delete the contents of ARTICLES
articlesDao.clearAllArticles();
// Read the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// Insert
Article article3 = new Article(3, "article3", 30, 30, 3);
articlesDao.addArticle(article3);
Article article4 = new Article(4, "article4", 40, 40, 4);
articlesDao.addArticle(article4);
// reads the ARTICLES table
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// creates a shopping cart with two items
ShoppingCart shoppingCart = new ShoppingCart();
cart.add(new Purchase(item3, 10));
cart.add(new Purchase(item4, 10));
// checks
assertEquals(700.0, cart.getTotal(), 1e-6);
assertEquals(2, cart.getPurchases().size());
// add an item that has already been purchased
cart.add(new Purchase(item3, 10));
// checks
// the total must now be 1000
assertEquals(1000.0, basket.getTotal(), 1e-6);
// still 2 items in the cart
assertEquals(2, cart.getPurchases().size());
// Quantity of item 3 must have been changed to 20
Purchase purchase = (Purchase) basket.getPurchases().get(0);
assertEquals(20, purchase.getQty());
// Remove item 3 from the cart
cart.remove(3);
// checks
// the total should now be 400
assertEquals(400.0, cart.getTotal(), 1e-6);
// Only 1 item in the cart
assertEquals(1, cart.getItems().size());
// this must be item #4
assertEquals(4, ((Purchase) cart.getPurchases().get(0)).getItem().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.
| // 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");
}
|
- 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>
<!-- the 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:
| // retrieves a domain access instance
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:
| // retrieves a data access instance
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>urlErrors</param-name>
<param-value>/views/errors.jsp</param-value>
</init-param>
<init-param>
<param-name>list-url</param-name>
<param-value>/views/list.jsp</param-value>
</init-param>
<init-param>
<param-name>urlInfo</param-name>
<param-value>/views/info.jsp</param-value>
</init-param>
<init-param>
<param-name>cart-url</param-name>
<param-value>/views/cart.jsp</param-value>
</init-param>
<init-param>
<param-name>emptyCartUrl</param-name>
<param-value>/views/empty-cart.jsp</param-value>
</init-param>
<init-param>
<param-name>urlDebug</param-name>
<param-value>/views/debug.jsp</param-value>
</init-param>
</servlet>
<welcome-file-list>
<welcome-file>/views/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>
<!-- the 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 for the istia.st.articles.dao.Article class -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- ORM mapping: ARTICLES table row - Article class instance -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="name" column="NAME"/>
<result property="price" column="PRICE"/>
<result property="currentStock" column="CURRENTSTOCK"/>
<result property="minStock" column="MINSTOCK"/>
</resultMap>
<!-- the SQL query to retrieve all items -->
<statement id="getAllArticles" resultMap="article">
select id, name, price,
currentStock, minimumStock from ARTICLES
</statement>
<!-- the SQL query to delete all items -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- the SQL query to insert an item -->
<statement id="insertArticle">
insert into ARTICLES (id, name, price,
currentStock, minimumStock) values
(#id#,#name#,#price#,#currentStock#,#minStock#)
</statement>
<!-- the SQL query to delete a given item -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- the SQL query to modify a given item -->
<statement id="modifyArticle">
update ARTICLES set name=#name#,
price=#price#,currentStock=#currentStock#,minStock=#minStock# where
id=#id#
</statement>
<!-- The SQL query to retrieve a specific article -->
<statement id="getArticleById" resultMap="article">
select id, name, price,
currentStock, minimumStock FROM ARTICLES where id=#id#
</statement>
<!-- the SQL query to modify the stock of a given item -->
<statement id="changeItemStock">
update ARTICLES set
currentStock=currentStock+#movement#
where id=#id# and currentStock+#movement#>=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>Online Store</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>List of articles</h2>
<table border="1">
<tr>
<th>NAME</th><th>PRICE</th>
</tr>
<c:forEach var="article" items="${listarticles}">
<tr>
<td><c:out value="${article.name}"/></td>
<td><c:out value="${article.price}"/></td>
<td><a href="<c:out value="?action=infos&id=${article.id}"/>">Info</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 with ID [<c:out value="${article.id}"/>]</h2>
<table border="1">
<tr>
<th>NAME</th><th>PRICE</th><th>CURRENT STOCK</th><th>MINIMUM STOCK</th>
</tr>
<tr>
<td><c:out value="${article.name}"/></td>
<td><c:out value="${article.price}"/></td>
<td><c:out value="${article.currentStock}"/></td>
<td><c:out value="${article.minStock}"/></td>
</tr>
</table>
<p>
<form method="post" action="?action=purchase&id=<c:out value="${article.id}"/>"/>
<table>
<tr>
<td><input type="submit" value="Buy"></td>
<td>Qty <input type="text" name="qty" size="3" value="<c:out value="${qty}"/>"></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>Cart Contents</h2>
<table border="1">
<tr>
<td>Item</td><td>Qty</td><td>Price</td><td>Total</td>
</tr>
<c:forEach var="purchase" items="${cart.purchases}">
<tr>
<td><c:out value="${purchase.item.name}"/></td>
<td><c:out value="${purchase.quantity}"/></td>
<td><c:out value="${purchase.item.price}"/></td>
<td><c:out value="${purchase.total}"/></td>
<td><a href="<c:out value="?action=retirerachat&id=${achat.article.id}"/>">Pick up</a></td>
</tr>
</c:forEach>
</table>
<p>
Order total: <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>Contents of your shopping cart</h2>
<p>
Your shopping cart is empty.
</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>The following errors have occurred</h2>
<ul>
<c:forEach var="error" items="${errors}">
<li><c:out value="${error}"/></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>/views/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=list"/>
|
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 the client's request
* @param response the response to the client
* @throws IOException
* @throws ServletException
*/
private void performAction(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.Purchase;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.ShoppingCart;
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 errors = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String MAIN_URL = "mainURL";
private final String errorURL = "errorURL";
private final String URL_LIST = "urlList";
private final String infoURL = "infoURL";
private final String URL_SHOPPING_CART = "urlShoppingCart";
private final String URL_EMPTY_CART = "urlEmptyCart";
private final String URL_DEBUG = "urlDebug";
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters =
{
URL_MAIN,
URL_ERRORS,
URL_LIST,
URL_INFO,
SHOP_CART,
EMPTY_CART_URL,
URL_DEBUG,
SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LIST = "list";
private final String ACTION_CART = "cart";
private final String ACTION_CHECKOUT = "checkout";
private final String ACTION_INFO = "info";
private final String ACTION_REMOVE_PURCHASE = "remove_purchase";
private final String ACTION_CONFIRM_CART = "confirmcart";
private String actionListUrl;
private final String listActionLink = "List of items";
private String actionCartUrl;
private final String actionLinkCart = "View Cart";
private String cartConfirmationActionURL;
private final String actionLinkConfirmCart = "Confirm Cart";
private Hashtable hActionList = new Hashtable(2);
private Hashtable hCartAction = new Hashtable(2);
private Hashtable hCartValidationAction = new Hashtable(2);
public void init() {
// retrieve the servlet's initialization parameters
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// log the error
errors.add(
"Parameter ["
+ parameters[i]
+ "] missing in the [web.xml] file");
}
}
// Any errors?
if (errors.size() != 0) {
return;
}
// create an IArticlesDomain object to access the business layer
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// log the error
errors.add(
"Data access configuration error: "
+ ex.toString());
return;
}
// we store certain URLs from the application
hActionList.put("href", "?action=" + ACTION_LIST);
hActionList.put("link", actionListLink);
hActionCart.put("href", "?action=" + ACTION_CART);
hActionCart.put("link", cartActionLink);
hActionValidationCart.put(
"href",
"?action=" + ACTION_CART_VALIDATION);
hCartValidationAction.put("link", cartValidationActionLink);
// done
return;
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// Check how the servlet initialization went
if (errors.size() != 0) {
// Do we have the URL for the error page?
if (config.getInitParameter(ERROR_URL) == null) {
throw new ServletException(errors.toString());
}
// display the error page
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(ERROR_URL))
.forward(request, response);
// end
return;
}
// process the action
String action = request.getParameter("action");
if (action == null) {
// list of items
doList(request, response);
return;
}
if (action.equals(ACTION_LIST)) {
// list of items
doList(request, response);
return;
}
if (action.equals(ACTION_INFO)) {
// article details
doInfo(request, response);
return;
}
if (action.equals(ACTION_PURCHASE)) {
// purchase an item
doPurchase(request, response);
return;
}
if (action.equals(ACTION_CART)) {
// display the shopping cart
doCart(request, response);
return;
}
if (action.equals(ACTION_REMOVE_ITEM)) {
// Remove an item from the cart
removeItem(request, response);
return;
}
if (action.equals(ACTION_VALIDATE_CART)) {
// validate the cart
doCartValidation(request, response);
return;
}
// unknown action
ArrayList errors = new ArrayList();
errors.add("unknown action [" + action + "]");
// display the error page
request.setAttribute("actions", new Hashtable[] { hActionList });
displayErrors(request, response, errors);
// end
return;
}
private void validateCart(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// The buyer has confirmed their shopping cart
ShoppingCart shoppingCart = (ShoppingCart) request.getSession().getAttribute("shoppingCart");
// We validate this cart
try {
articlesDomain.buy(shoppingCart);
} catch (UncheckedAccessArticlesException ex) {
// abnormal
errors.add("Data access error [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionList });
displayErrors(request, response, errors);
return;
}
// retrieve the errors
ArrayList errors = domainArticles.getErrors();
if (errors.size() != 0) {
request.setAttribute(
"actions",
new Hashtable[] { hActionList, hActionCart });
displayErrors(request, response, errors);
return;
}
// display the list of items
request.setAttribute("message", "Your cart has been validated");
doList(request, response);
// end
return;
}
private void doRemovePurchase(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Remove a purchase from the cart
try {
ShoppingCart shoppingCart =
(Cart) request.getSession().getAttribute("cart");
String purchaseId = request.getParameter("id");
shoppingCart.remove(Integer.parseInt(strPurchaseId));
} catch (NumberFormatException ignored) {
} catch (NullPointerException ignored) {
}
// display the shopping cart
doCart(request, response);
}
private void doCart(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// display the shopping cart
ShoppingCart shoppingCart = (ShoppingCart) request.getSession().getAttribute("shoppingCart");
// Is the shopping cart empty?
if (shoppingCart == null || shoppingCart.getItems().size() == 0) {
request.setAttribute("actions", new Hashtable[] { hActionList });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_EMPTY_CART))
.forward(request, response);
// end
return;
}
// there is something in the cart
request.setAttribute("cart", cart);
request.setAttribute(
"actions",
new Hashtable[] { hActionList, hCartValidationAction });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER))
.forward(request, response);
// end
return;
}
private void doPurchase(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// purchase an item
// retrieve the quantity
int qty = 0;
try {
qty = Integer.parseInt(request.getParameter("qty"));
if (qty <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// Incorrect quantity
request.setAttribute("msg", "Incorrect quantity");
request.setAttribute("qty", request.getParameter("qty"));
String url =
config.getInitParameter(URL_MAIN)
+ "?action=infos&id="
+ request.getParameter("id");
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// end
return;
}
// retrieve the client's session
HttpSession session = request.getSession();
// create the purchase
Item item = (Item) session.getAttribute("item");
Purchase purchase = new Purchase(article, qty);
// Add the purchase to the customer's cart
ShoppingCart shoppingCart = (ShoppingCart) session.getAttribute("shoppingCart");
if (cart == null) {
cart = new Cart();
session.setAttribute("cart", cart);
}
cart.add(purchase);
// return to the list of items
String url = config.getInitParameter(URL_MAIN) + "?action=list";
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// end
return;
}
private void displayDebugInfo(
HttpServletRequest request,
HttpServletResponse response,
ArrayList info)
throws ServletException, IOException {
// display the list of items
request.setAttribute("infos", infos);
getServletContext()
.getRequestDispatcher(config.getInitParameter(DEBUG_URL))
.forward(request, response);
// end
return;
}
public void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// same as get
doGet(request, response);
}
private void doInfos(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// the list of errors
ArrayList errors = new ArrayList();
// retrieve the requested ID
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// Not normal
errors.add("invalid action([info, id=null]");
request.setAttribute("actions", new Hashtable[] { hActionList });
displayErrors(request, response, errors);
return;
}
// convert strId to an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// Not normal
errors.add("invalid action([info, id=" + strId + "]");
request.setAttribute("actions", new Hashtable[] { hActionList });
displayErrors(request, response, errors);
return;
}
// request the article with key id
Article article = null;
try {
article = articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// Not normal
errors.add("Data access error [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionList });
displayErrors(request, response, errors);
return;
}
if (article == null) {
// not normal
errors.add("Key [" + id + "] does not exist");
request.setAttribute("actions", new Hashtable[] { hActionList });
displayErrors(request, response, errors);
return;
}
// Store the article in the session
request.getSession().setAttribute("article", article);
// display the info page
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 displayErrors(
HttpServletRequest request,
HttpServletResponse response,
ArrayList errors)
throws ServletException, IOException {
// display the error page
request.setAttribute("errors", errors);
getServletContext()
.getRequestDispatcher(config.getInitParameter(ERROR_URL))
.forward(request, response);
// end
return;
}
private void doList(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// the list of errors
ArrayList errors = new ArrayList();
// request the list of articles
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// log the error
errors.add(
"Error retrieving all articles: "
+ ex.toString());
}
// Any errors?
if (errors.size() != 0) {
// display the errors page
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { hActionList });
getServletContext()
.getRequestDispatcher(config.getInitParameter(ERROR_URL))
.forward(request, response);
// end
return;
}
// display the list of articles
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// end
return;
}
/**
* console logging for debugging
* @param message: the message to display
*/
private void display(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 the servlet's initialization parameters
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// log the error
errors.add(
"Parameter ["
+ parameters[i]
+ "] missing in the [web.xml] file");
}
}
// Any errors?
if (errors.size() != 0) {
return;
}
// create an IArticlesDomain object to access the business layer
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// log the error
errors.add(
"Data access configuration error: "
+ ex.toString());
return;
}
// store certain URLs from the application
hActionList.put("href", "?action=" + ACTION_LIST);
hActionList.put("link", actionListLink);
hActionCart.put("href", "?action=" + ACTION_CART);
hActionCart.put("link", cartActionLink);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("link", linkActionValidationPanier);
// done
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 servlet initialization went
if (errors.size() != 0) {
// Do we have the URL of the error page?
if (config.getInitParameter(ERROR_URL) == null) {
throw new ServletException(errors.toString());
}
// display the error page
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(ERROR_URL))
.forward(request, response);
// end
return;
}
// process the action
String action = request.getParameter("action");
if (action == null) {
// list of items
doList(request, response);
return;
}
if (action.equals(ACTION_LIST)) {
// list of items
doList(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// information about an article
doInfos(request, response);
return;
}
if (action.equals(ACTION_PURCHASE)) {
// purchase an item
doPurchase(request, response);
return;
}
if (action.equals(ACTION_CART)) {
// display the shopping cart
doCart(request, response);
return;
}
if (action.equals(ACTION_REMOVE_ITEM)) {
// Remove an item from the cart
removeItem(request, response);
return;
}
if (action.equals(ACTION_VALIDATE_CART)) {
// validate the cart
doCartValidation(request, response);
return;
}
// unknown action
ArrayList errors = new ArrayList();
errors.add("unknown action [" + action + "]");
// display the error page
request.setAttribute("actions", new Hashtable[] { hActionList });
displayErrors(request, response, errors);
// 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 {
// the list of errors
ArrayList errors = new ArrayList();
// request the list of articles
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// log the error
errors.add(
"Error retrieving all articles: "
+ ex.toString());
}
// Any errors?
if (errors.size() != 0) {
// display the errors page
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { hActionList });
getServletContext()
.getRequestDispatcher(config.getInitParameter(ERROR_URL))
.forward(request, response);
// end
return;
}
// display the list of articles
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:
// private fields
private IArticlesDomain articlesDomain = null;
- Using this access service, we can request the list of articles:
| // the list of errors
ArrayList errors = new ArrayList();
// request the list of articles
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// log the error
errors.add(
"Error retrieving all articles: "
+ ex.toString());
}
|
- If errors occur, the [ERRORS] view is sent:
| // Any errors?
if (errors.size() != 0) {
// display the errors page
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { hActionList });
getServletContext()
.getRequestDispatcher(config.getInitParameter(ERROR_URL))
.forward(request, response);
// end
return;
}
|
- otherwise, the [LIST] view is sent:
| // display the list of articles
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>/views/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="displayArticleList" path="/views/list.jsp"/>
<forward name="displayErrors" path="/views/errors.jsp"/>
</action>
<action path="/list" type="istia.st.articles.web.struts.ListArticlesAction">
<forward name="displayArticleList" path="/views/list.jsp"/>
<forward name="displayErrors" path="/views/errors.jsp"/>
</action>
<action path="/info" type="istia.st.articles.web.struts.ArticleInfoAction">
<forward name="displayArticleInfo" path="/views/info.jsp"/>
<forward name="displayErrors" path="/views/errors.jsp"/>
</action>
<action
path="/purchase" type="istia.st.articles.web.struts.PurchaseArticleAction">
<forward name="displayArticleInfo" path="/views/info.jsp"/>
<forward name="displayArticleList" path="/main.do"/>
</action>
<action
path="/cart" type="istia.st.articles.web.struts.ViewCartAction">
<forward name="displayCart" path="/views/cart.jsp"/>
<forward name="displayEmptyCart" path="/views/emptyCart.jsp"/>
</action>
<action
path="/cancelPurchase" type="istia.st.articles.web.struts.CancelPurchaseAction">
<forward name="displayCart" path="/cart.do"/>
<forward name="displayErrors" path="/views/errors.jsp"/>
</action>
<action
path="/confirmCart" type="istia.st.articles.web.struts.ConfirmCartAction">
<forward name="displayItemList" path="/main.do"/>
<forward name="displayErrors" path="/views/errors.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>Online Store</h2></td>
<c:forEach items="${actions}" var="action">
<td>|</td>
<td><a href="<c:out value="${action.href}"/>"><c:out value="${action.link}"/></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>List of articles</h2>
<table border="1">
<tr>
<th>NAME</th><th>PRICE</th>
</tr>
<c:forEach var="article" items="${listarticles}">
<tr>
<td><c:out value="${article.name}"/></td>
<td><c:out value="${article.price}"/></td>
<td><a href="<c:out value="infos.do?id=${article.id}"/>">Info</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>Item with ID [<c:out value="${article.id}"/>]</h2>
<table border="1">
<tr>
<th>NAME</th><th>PRICE</th><th>CURRENT STOCK</th><th>MINIMUM STOCK</th>
</tr>
<tr>
<td><c:out value="${article.name}"/></td>
<td><c:out value="${article.price}"/></td>
<td><c:out value="${article.currentStock}"/></td>
<td><c:out value="${article.minStock}"/></td>
</tr>
</table>
<p>
<form method="post" action="achat.do?id=<c:out value="${article.id}"/>"/>
<table>
<tr>
<td><input type="submit" value="Buy"></td>
<td>Qty <input type="text" name="qty" size="3" value="<c:out value="${qty}"/>"></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>Cart Contents</h2>
<table border="1">
<tr>
<td>Item</td><td>Qty</td><td>Price</td><td>Total</td>
</tr>
<c:forEach var="purchase" items="${cart.purchases}">
<tr>
<td><c:out value="${purchase.item.name}"/></td>
<td><c:out value="${purchase.quantity}"/></td>
<td><c:out value="${purchase.item.price}"/></td>
<td><c:out value="${purchase.total}"/></td>
<td><a href="<c:out value="retirerachat.do?id=${achat.article.id}"/>">Pick Up</a></td>
</tr>
</c:forEach>
</table>
<p>
Order total: <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>Contents of your shopping cart</h2>
<p>
Your shopping cart is empty.
</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>The following errors occurred</h2>
<ul>
<c:forEach var="error" items="${errors}">
<li><c:out value="${error}"/></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>/views/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 errors = 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_LIST = "list.do";
private final String ACTION_SHOPPING_CART = "shopping-cart.do";
private final String ACTION_CHECKOUT = "checkout.do";
private final String ACTION_INFO = "info.do";
private final String ACTION_REMOVE_PURCHASE = "removepurchase.do";
private final String ACTION_CONFIRM_CART = "confirmcart.do";
private String actionListUrl;
private final String actionListLink = "List of items";
private String actionCartUrl;
private final String actionLinkToCart = "View Cart";
private String cartConfirmationActionURL;
private final String cartConfirmActionLink = "Confirm Cart";
private Hashtable hActionList = new Hashtable(2);
private Hashtable hCartAction = new Hashtable(2);
private Hashtable hCartValidationAction = new Hashtable(2);
// getters - setters
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public ArrayList getErrors() {
return errors;
}
public void setErrors(ArrayList errors) {
this.errors = errors;
}
public Hashtable getActionList() {
return hActionList;
}
public void setActionList(Hashtable actionList) {
hActionList = actionList;
}
public Hashtable getActionBasket() {
return hActionCart;
}
public void setHActionCart(Hashtable actionCart) {
hActionPanier = actionPanier;
}
public Hashtable getShoppingCartValidationActions() {
return hActionValidationCart;
}
public void setCartValidationAction(Hashtable cartValidationAction) {
hActionValidationPanier = actionValidationPanier;
}
public void init() throws ServletException{
// Initialize parent class
super.init();
// retrieve the servlet's initialization parameters
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// log the error
errors.add("Parameter [" + parameters[i]
+ "] missing from the [web.xml] file");
}
}
// Any errors?
if (errors.size() != 0) {
return;
}
// create an IArticlesDomain object to access the business layer
try {
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource((String) config
.getInitParameter(SPRING_CONFIG_FILENAME))))
.getBean("articlesDomain");
} catch (Exception ex) {
// log the error
errors.add("Data access configuration error: "
+ ex.toString());
return;
}
// we store certain URLs from the application
hActionList.put("href", ACTION_LIST);
hActionList.put("link", actionListLink);
hActionCart.put("href", ACTION_CART);
hActionCart.put("link", cartActionLink);
hActionValidationCart.put("href", ACTION_VALIDATION_CART);
hActionValidationCart.put("link", actionCartValidationLink);
// done
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="displayArticleList" path="/views/list.jsp"/>
<forward name="displayErrors" path="/views/errors.jsp"/>
</action>
<action path="/list" type="istia.st.articles.web.struts.ListArticlesAction">
<forward name="displayArticleList" path="/views/list.jsp"/>
<forward name="displayErrors" path="/views/errors.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 ArticleListAction extends Action {
/**
* Display the list of articles - relies on the [domain] layer
*
* @param mapping:
* action configuration in struts-config.xml
* @param form:
* the form passed to the action - none here
* @param request:
* the client's HTTP request
* @param response:
* the HTTP response to the 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 initErrors = mainServlet.getErrors();
if (initErrors.size() != 0) {
// display the error page
request.setAttribute("errors", initErrors);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("displayErrors");
}
// the domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// the list of errors
ArrayList errors = new ArrayList();
// request the list of articles
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// log the error
errors.add("Error retrieving all articles: "
+ ex.toString());
}
// Any errors?
if (errors.size() != 0) {
// display the errors page
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// display the list of articles
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("displayProductList");
}
}
|
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 initErrors = mainServlet.getErrors();
if (initErrors.size() != 0) {
// display the errors page
request.setAttribute("errors", initErrors);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("displayErrors");
}
|
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="displayArticleList" path="/views/list.jsp"/>
<forward name="displayErrors" path="/views/errors.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):
// the domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
- Once this is done, we can request the list of articles:
| // the list of errors
ArrayList errors = new ArrayList();
// request the list of articles
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// log the error
errors.add("Error retrieving all articles: "
+ ex.toString());
}
|
- If errors occur, the [ERRORS] view is sent:
| // Any errors?
if (errors.size() != 0) {
// display the errors page
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
|
- otherwise, the [LIST] view is sent:
| // Display the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("displayProductList");
|
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="displayItemList" path="/views/list.jsp"/>
<forward name="displayErrors" path="/views/errors.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="displayArticleInfo" path="/views/infos.jsp"/>
<forward name="displayErrors" path="/views/errors.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 ArticleInfoAction 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 initErrors = mainServlet.getErrors();
if (initErrors.size() != 0) {
// display the error page
request.setAttribute("errors", initErrors);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("displayErrors");
}
// the domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// the list of errors
ArrayList errors = new ArrayList();
// retrieve the requested ID
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// Not normal
errors.add("invalid action([info, id=null]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// convert strId to an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// Not normal
errors.add("incorrect action([info, id=" + strId + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// retrieve the article with the key "id"
Article article = null;
try {
article = articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// Not normal
errors.add("Data access error [" + ex.toString() + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
if (article == null) {
// not normal
errors.add("Key [" + id + "] does not exist");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// we put the article in the session
request.getSession().setAttribute("article", article);
// display the info page
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("displayArticleInfo");
}
}
|
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="displayArticleInfo" path="/views/infos.jsp"/>
<forward name="displayErrors" path="/views/errors.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="Buy"></td>
<td>Qty <input type="text" name="qty" 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="/purchase" type="istia.st.articles.web.struts.PurchaseItemAction">
<forward name="displayItemInfo" path="/views/info.jsp"/>
<forward name="displayItemList" 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.Purchase;
import istia.st.articles.domain.ShoppingCart;
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 PurchaseItemAction 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 initErrors = mainServlet.getErrors();
if (initErrors.size() != 0) {
// display the error page
request.setAttribute("errors", initErrors);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("displayErrors");
}
// the list of errors for this action
ArrayList errors = new ArrayList();
// retrieve the quantity purchased
int qty = 0;
try {
qty = Integer.parseInt(request.getParameter("qty"));
if (qty <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// Incorrect quantity
request.setAttribute("msg", "Incorrect quantity");
request.setAttribute("qty", request.getParameter("qty"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayItemInfo");
}
// retrieve the client's session
HttpSession session = request.getSession();
// retrieve the article stored in the session
Article article = (Article) session.getAttribute("article");
// Has the session expired?
if (article == null) {
// display the error page
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// create the new purchase
Purchase purchase = new Purchase(item, qty);
// add the purchase to the customer's cart
Cart cart = (Cart) session.getAttribute("cart");
if (shoppingCart == null) {
cart = new Cart();
session.setAttribute("cart", cart);
}
cart.add(purchase);
// return to the list of items
return mapping.findForward("displayItemList");
}
}
|
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="Buy"></td>
<td>Qty <input type="text" name="qty" 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 for this action
ArrayList errors = new ArrayList();
// retrieve the quantity purchased
int qty = 0;
try {
qty = Integer.parseInt(request.getParameter("qty"));
if (qty <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// Incorrect quantity
request.setAttribute("msg", "Incorrect quantity");
request.setAttribute("qty", request.getParameter("qty"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayItemInfo");
}
|
- The purchased item is retrieved from the session. The session may have expired. In this case, the [ERRORS] view is displayed:
| // retrieve the client's session
HttpSession session = request.getSession();
// retrieve the item stored in the session
Article article = (Article) session.getAttribute("article");
// Has the session expired?
if (article == null) {
// display the error page
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
|
- If the session has not expired, the item is added to the cart, which is also retrieved from the session:
| // retrieve the client's session
HttpSession session = request.getSession();
// retrieve the item stored in the session
Article article = (Article) session.getAttribute("article");
// Has the session expired?
if (article == null) {
// display the error page
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// create the new purchase
Purchase purchase = new Purchase(item, qty);
// add the purchase to the customer's cart
Cart cart = (Cart) session.getAttribute("cart");
if (shoppingCart == null) {
cart = new Cart();
session.setAttribute("cart", cart);
}
cart.add(purchase);
|
- Finally, we send the [LIST] view:
// we return to the list of items
return mapping.findForward("displayItemList");
- The view that will actually be sent to the client is provided by [struts-config.xml]:
<action
path="/purchase" type="istia.st.articles.web.struts.PurchaseItemAction">
<forward name="displayArticleInfo" path="/views/info.jsp"/>
<forward name="displayArticleList" 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">View Cart</a>
The [panier.do] action is configured as follows in [struts-config.xml]:
<action
path="/cart" type="istia.st.articles.web.struts.ViewCartAction">
<forward name="displayCart" path="/views/cart.jsp"/>
<forward name="displayEmptyCart" path="/views/emptyCart.jsp"/>
</action>
The code for the [VoirPanierAction] class is as follows:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.ShoppingCart;
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 ViewCartAction 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 initErrors = mainServlet.getErrors();
if (initErrors.size() != 0) {
// display the error page
request.setAttribute("errors", initErrors);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("displayErrors");
}
// display the shopping cart
ShoppingCart shoppingCart = (ShoppingCart) request.getSession().getAttribute("shoppingCart");
if (shoppingCart == null || shoppingCart.getPurchases().size() == 0) {
// empty cart
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayEmptyCart");
} else {
// there is something in the cart
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionList(), mainServlet.getHActionCartValidation() });
return mapping.findForward("viewCart");
}
}
}
|
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="/cart" type="istia.st.articles.web.struts.ViewCartAction">
<forward name="displayCart" path="/views/cart.jsp"/>
<forward name="displayEmptyCart" path="/views/emptyCart.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">Remove</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="displayCart" path="/cart.do"/>
<forward name="displayErrors" path="/views/errors.jsp"/>
</action>
The code for the [RetirerAchatAction] class is as follows:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.ShoppingCart;
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 RemovePurchaseAction 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 initErrors = mainServlet.getErrors();
if (initErrors.size() != 0) {
// display the error page
request.setAttribute("errors", initErrors);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("displayErrors");
}
// the list of errors for this action
ArrayList errors = new ArrayList();
// retrieve the shopping cart
ShoppingCart shoppingCart = (ShoppingCart) request.getSession().getAttribute("shoppingCart");
if (shoppingCart == null) {
// session expired
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayEmptyCart");
}
// retrieve the ID of the item to be removed
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// Not normal
errors.add("invalid action([removepurchase,id=null]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// Convert strId to an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// Not normal
errors.add("incorrect action([cancelpurchase,id=" + strId + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// remove the item from the cart
cart.remove(id);
// display the cart again
return mapping.findForward("displayCart");
}
}
|
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:
// remove the item
cart.remove(id);
- then the cart is re-displayed:
// display the cart again
return mapping.findForward("displayCart");
The view actually sent to the client is defined by the action:
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="displayCart" path="/cart.do"/>
<forward name="displayErrors" path="/views/errors.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">Confirm Cart</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="/confirmCart" type="istia.st.articles.web.struts.ConfirmCartAction">
<forward name="displayItemList" path="/main.do"/>
<forward name="displayErrors" path="/views/errors.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.Cart;
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 ValidateCartAction 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 initErrors = mainServlet.getErrors();
if (initErrors.size() != 0) {
// display the error page
request.setAttribute("errors", initErrors);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("displayErrors");
}
// the domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// the list of errors for this action
ArrayList errors = new ArrayList();
// the buyer has confirmed their cart
ShoppingCart shoppingCart = (ShoppingCart) request.getSession().getAttribute("shoppingCart");
if (shoppingCart == null) {
// session expired
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// validate the cart
try {
articlesDomain.buy(cart);
} catch (UncheckedAccessArticlesException ex) {
// abnormal condition
errors.add("Data access error [" + ex.toString() + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// retrieve any errors
errors = articlesDomain.getErrors();
if (errors.size() != 0) {
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList(), mainServlet.getCartAction() });
return mapping.findForward("displayErrors");
}
// Everything seems OK—display the list of items
return mapping.findForward("displayItemList");
}
}
|
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 for this action
ArrayList errors = new ArrayList();
// the buyer has confirmed their cart
ShoppingCart shoppingCart = (ShoppingCart) request.getSession().getAttribute("shoppingCart");
if (cart == null) {
// session expired
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
|
- 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:
| // We validate the shopping cart
try {
articlesDomain.buy(cart);
} catch (UncheckedAccessArticlesException ex) {
// abnormal condition
errors.add("Data access error [" + ex.toString() + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList() });
return mapping.findForward("displayErrors");
}
// retrieve any errors
errors = articlesDomain.getErrors();
if (errors.size() != 0) {
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getActionList(), mainServlet.getCartAction() });
return mapping.findForward("displayErrors");
}
|
- If everything went well, we display the list of items again:
// Everything seems OK—display the list of items
return mapping.findForward("displayItemList");
The view actually sent to the client is defined by the action:
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="displayItemList" path="/main.do"/>
<forward name="displayErrors" path="/views/errors.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>
<!-- the Spring context loader for the application -->
<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>
<!-- the input document -->
<welcome-file-list>
<welcome-file>/views/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>
<!-- the 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>
<!-- action handlers = 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="ViewCartController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RemovePurchaseController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValidateCartController"
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="/info.do">InfoController</prop>
<prop key="/purchase.do">PurchaseController</prop>
<prop key="/cart.do">ViewCartController</prop>
<prop key="/cancelpurchase.do">CancelPurchaseController</prop>
<prop key="/confirmCart.do">ConfirmCartController</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>/views/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- the 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:
<!-- view resolver -->
<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>/views/</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>
<!-- the Spring context loader for the application -->
<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>
<!-- the welcome file -->
<welcome-file-list>
<welcome-file>/views/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>
<!-- the 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>
<!-- Action handlers = 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="ViewCartController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RemovePurchaseController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValidateCartController"
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="/info.do">InfoController</prop>
<prop key="/purchase.do">PurchaseController</prop>
<prop key="/cart.do">ViewCartController</prop>
<prop key="/cancelpurchase.do">CancelPurchaseController</prop>
<prop key="/confirmCart.do">ConfirmCartController</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>/views/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- the 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>/views/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>
<!-- action handlers = 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="/info.do">InfoController</prop>
<prop key="/purchase.do">PurchaseController</prop>
<prop key="/cart.do">ViewCartController</prop>
<prop key="/cancelpurchase.do">CancelPurchaseController</prop>
<prop key="/confirmCart.do">ConfirmCartController</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>
<!-- the 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_LIST = "list.do";
private final String ACTION_CART = "cart.do";
private final String ACTION_VALIDATE_CART = "validateCart.do";
private final String listActionLink = "List of items";
private final String actionLinkCart = "View Cart";
private final String actionLinkConfirmCart = "Confirm Cart";
private Hashtable hActionList = new Hashtable(2);
private Hashtable hCartAction = new Hashtable(2);
private Hashtable hActionConfirmCart = new Hashtable(2);
// getters-setters
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public Hashtable getActionList() {
return hActionList;
}
public Hashtable getHActionCart() {
return hActionCart;
}
public Hashtable getHActionCartValidation() {
return hActionValidationCart;
}
// Initialize web application
public void init() {
// store certain URLs from the application
hActionList.put("href", ACTION_LIST);
hActionList.put("link", actionListLink);
hActionCart.put("href", ACTION_CART);
hActionCart.put("link", cartActionLink);
hActionValidationCart.put("href", ACTION_VALIDATION_CART);
hActionValidationCart.put("link", actionCartValidationLink);
// done
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]:
<!-- web application configuration -->
<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:
| <!-- 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>
...
</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 application configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processing the request
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// retrieve the list of articles
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// store the error
ArrayList errors = new ArrayList();
errors.add("Error retrieving all items: "
+ ex.toString());
// display the errors page
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
// send the errors view
return new ModelAndView("errors");
}
// display the list of articles
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// send the view
return new ModelAndView("list");
}
}
|
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.
| // request the list of articles
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// store the error
ArrayList errors = new ArrayList();
errors.add("Error retrieving all articles: "
+ ex.toString());
// display the error page
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
// Send the errors view
return new ModelAndView("errors");
}
|
- If there are no errors, the [LIST] view is sent:
| // display the list of articles
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// send the view
return new ModelAndView("list");
|
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:
| <!-- application mapping -->
<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 application configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processing the request
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// list of errors
ArrayList errors = new ArrayList();
// retrieve the requested ID
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// Not normal
errors.add("Invalid action([info, id=null]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("errors");
}
// convert strId to an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// Not normal
errors.add("incorrect action([info, id=" + strId + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("errors");
}
// retrieve the article with key id
Article article = null;
try {
article = config.getArticlesDomain().getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// Not normal
errors.add("Data access error [" + ex.toString() + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("errors");
}
if (article == null) {
// not normal
errors.add("Key [" + id + "] does not exist");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("errors");
}
// we put the article in the session
request.getSession().setAttribute("article", article);
// display the info page
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("info");
}
}
|
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="Buy"></td>
<td>Qty <input type="text" name="qty" 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]:
<!-- application mapping -->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/purchase.do">PurchaseController</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.Purchase;
import istia.st.articles.domain.ShoppingCart;
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 PurchaseController implements Controller {
// Web application configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processing the request
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// list of errors for this action
ArrayList errors = new ArrayList();
// retrieve the quantity purchased
int qty = 0;
try {
qty = Integer.parseInt(request.getParameter("qty"));
if (qty <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// Incorrect quantity
request.setAttribute("msg", "Incorrect quantity");
request.setAttribute("qty", request.getParameter("qty"));
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("info");
}
// retrieve the client's session
HttpSession session = request.getSession();
// retrieve the article stored in the session
Article article = (Article) session.getAttribute("article");
// Has the session expired?
if (article == null) {
// display the error page
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("errors");
}
// create the new purchase
Purchase purchase = new Purchase(item, qty);
// add the purchase to the customer's cart
ShoppingCart shoppingCart = (ShoppingCart) session.getAttribute("shoppingCart");
if (shoppingCart == null) {
cart = new Cart();
session.setAttribute("cart", cart);
}
cart.add(purchase);
// 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="Buy"></td>
<td>Qty <input type="text" name="qty" 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 for this action
ArrayList errors = new ArrayList();
// retrieve the quantity purchased
int qty = 0;
try {
qty = Integer.parseInt(request.getParameter("qty"));
if (qty <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// Incorrect quantity
request.setAttribute("msg", "Incorrect quantity");
request.setAttribute("qty", request.getParameter("qty"));
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("info");
}
|
- The purchased item is retrieved from the session. The session may have expired. In this case, the [ERRORS] view is sent:
| // retrieve the client's session
HttpSession session = request.getSession();
// retrieve the article stored in the session
Article article = (Article) session.getAttribute("article");
// Has the session expired?
if (article == null) {
// display the error page
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("errors");
}
|
- If the session has not expired, the item is added to the cart, which is also retrieved from the session:
| // create the new purchase
Purchase purchase = new Purchase(item, qty);
// add the purchase to the customer's cart
Cart cart = (Cart) session.getAttribute("cart");
if (shoppingCart == null) {
cart = new Cart();
session.setAttribute("cart", cart);
}
cart.add(purchase);
|
- Finally, we send the [LIST] view:
// return to the list of items
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">View Cart</a>
The page returned by this link is as follows:

This action is configured as follows in [springwebarticles-servlet.xml]:
| <!-- application mapping -->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/cart.do">ViewCartController</prop>
...
</props>
</property>
</bean>
...
<bean id="ViewCartController"
class="istia.st.articles.web.spring.ViewCartController">
<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.ShoppingCart;
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 ViewCartController implements Controller {
// Web application configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// Process the request
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// display the shopping cart
ShoppingCart shoppingCart = (ShoppingCart) request.getSession().getAttribute("shoppingCart");
if (shoppingCart == null || shoppingCart.getItems().size() == 0) {
// empty cart
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("emptyCart");
} else {
// there is something in the cart
request.setAttribute("actions", new Hashtable[] {
config.getHActionList(), config.getHActionCartValidation() });
return new ModelAndView("cart");
}
}
}
|
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">Remove</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]:
| <!-- application mapping -->
<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.ShoppingCart;
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 RemovePurchaseController implements Controller {
// Web application configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processing the request
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the list of errors for this action
ArrayList errors = new ArrayList();
// retrieve the shopping cart
ShoppingCart shoppingCart = (ShoppingCart) request.getSession().getAttribute("shoppingCart");
if (shoppingCart == null) {
// session expired
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("errors");
}
// retrieve the ID of the item to be removed
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// Not normal
errors.add("invalid action([removepurchase,id=null]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("errors");
}
// convert strId to an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// Not normal
errors.add("incorrect action([cancelpurchase,id=" + strId + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("errors");
}
// Remove the item from the cart
cart.remove(id);
// reload the cart
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:
// remove the item
cart.remove(id);
- then the cart is reloaded:
// re-display the cart
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">Confirm Cart</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]:
| <!-- application mapping -->
<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.Cart;
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 ValidateCartController implements Controller {
// Web application configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processing the request
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// list of errors for this action
ArrayList errors = new ArrayList();
// The buyer has confirmed their cart
ShoppingCart shoppingCart = (ShoppingCart) request.getSession().getAttribute("shoppingCart");
if (shoppingCart == null) {
// session expired
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("errors");
}
// validate the cart
try {
config.getArticlesDomain().buy(cart);
} catch (UncheckedAccessArticlesException ex) {
// abnormal condition
errors.add("Data access error [" + ex.toString() + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("errors");
}
// retrieve any errors
errors = config.getArticlesDomain().getErrors();
if (errors.size() != 0) {
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getHActionList(), config.getHActionCart() });
return new ModelAndView("errors");
}
// Everything seems OK—display the list of items
return new ModelAndView("index");
}
}
|
Comments:
- We retrieve the shopping cart from the session. If the session has expired, we display the [ERRORS] view:
| // the list of errors for this action
ArrayList errors = new ArrayList();
// the buyer has confirmed their cart
Cart cart = (Cart) request.getSession().getAttribute("cart");
if (shoppingCart == null) {
// session expired
errors.add("Your session has expired");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("errors");
}
|
- 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:
| // We validate the cart
try {
config.getArticlesDomain().buy(cart);
} catch (UncheckedAccessArticlesException ex) {
// abnormal condition
errors.add("Data access error [" + ex.toString() + "]");
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList() });
return new ModelAndView("errors");
}
// retrieve any errors
errors = config.getArticlesDomain().getErrors();
if (errors.size() != 0) {
request.setAttribute("errors", errors);
request.setAttribute("actions", new Hashtable[] { config
.getActionList(), config.getCartAction() });
return new ModelAndView("errors");
}
|
- If everything went well, we display the list of items again:
// Everything seems OK—display the list of items
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.