Skip to content

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

Image

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

Image

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

Image

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

Image

  1. the [ERRORS] view, which reports any application errors

Image

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:

  1. 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.
  2. 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.
  3. 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
  4. 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.
  5. 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:

  1. business classes
  2. data access classes
  3. 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);
id
primary key uniquely identifying an item
name
item name
price
its price
current stock
current stock
minimum 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:

  1. a constructor for setting the 5 pieces of information for an item
  2. 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.
  3. validation of the data entered for the item. If the data is invalid, an exception is thrown.
  4. 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:

getAllArticles
returns all items from the ARTICLES table in a list of [Article] objects
clearAllArticles
clears the ARTICLES table
getArticleById
returns the [Article] object identified by its primary key
addArticle
allows you to add an article to the ARTICLES table
modifyArticle
allows you to modify an item in the [ARTICLES] table
deleteItem
allows you to delete an item from the [ARTICLES] table
updateItemStock
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#&gt;=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();
}
List getAllArticles()
returns the list of [Article] objects to be displayed to the client
Article getArticleById(int idArticle)
returns the [Article] object identified by [idArticle]
void buy(Cart cart)
processes the customer's cart by decrementing the stock of purchased items by the quantity purchased—may fail if stock is insufficient
ArrayList getErrors()
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:

IArticlesDao articlesDao
the data access object provided by the data access layer
ArrayList errors
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:

item
the purchased item
qty
the quantity purchased
double getTotal()
returns the purchase amount
String toString()
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:

purchases
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
void remove(int itemId)
removes the purchase for item idArticle
double getTotal()
returns the total amount of purchases
String toString()
returns the string representation of the shopping cart
ArrayList getPurchases()
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:

Image

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:
1
2
3
4
5
6
7
8
    // 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:

1
2
3
4
        // 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:

1
2
3
        // 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:

M = Model
Business classes, data access classes, and the database
V = Views
JSP pages
C = Controller
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
list.jsp
The views are located in the [vues] folder of the application
INFO
info.jsp
CART
cart.jsp
EMPTY CART
empty-cart.jsp
ERRORS
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:

request
meaning
controller action
possible responses
action=list
the client wants the list of
items
- requests the list of items from the
business
- [LIST]
- [ERRORS]
action=info
The client requests
Information about one of the items displayed in the view
[LIST]
- requests the item from the business layer
- [INFO]
- [ERRORS]
action=purchase
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
action=remove purchase
the customer wants to remove an
purchase from their cart
- retrieve the cart from the session and modify it
- [CART]
- [EMPTY CART]
- [ERRORS]
action=cart
the customer wants to view their
cart
- retrieves the cart from the session
- [SHOPPING CART]
- [EMPTY CART]
- [ERRORS]
action=validate-cart
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:

  1. changes to the URLs of the various views
  2. changes to the classes implementing the [IArticlesDao] and [IArticlesDomain] interfaces
  3. 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:

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

  1. 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#&gt;=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
the views

3.5.9. JSP views

JSP views use the JSTL tag library.

3.5.9.1. header.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 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:

actions
Hashtable[] object - the array of menu options
listarticles
ArrayList of objects of type [Item]
message
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:

Image

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:

actions
Hashtable[] object - the array of menu options
item
object of type [Article] - item to display
msg
String object - message to display in case of an error with the quantity
qte
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:

Image

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:

Image

It is displayed following a request to /main?action=cart or /main?action=remove&id=ID. The controller request parameters are as follows:

actions
Hashtable[] object - the array of menu options
cart
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:

Image

It is displayed following a request to /main?action=cart or /main?action=remove&id=ID. The controller request parameters are as follows:

actions
Hashtable[] object - the array of menu options

Code:

1
2
3
4
5
6
7
8
9
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>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:

Image

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:

actions
Hashtable[] object - the array of menu options
errors
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:

1
2
3
4
<%@ 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:
1
2
3
4
5
6
7
8
/**
   * @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
doList
GET /main?action=list
- request the list of items
from the business class
- display it
[LIST] or [ERRORS]
doInfo
GET /main?action=info&id=ID
- retrieve the item with id=ID from
the business class
- display it
[INFO] or [ERRORS]
doPurchase
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]
doRemovePurchase
GET /main?action=removePurchase&id=ID
- remove the item with id=ID from the
shopping list in the
the customer's session
[CART]
doCart
GET /main?action=cart
- Display the
client session
[CART] or [EMPTY_CART]
doCartValidation
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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
package istia.st.articles.web;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import istia.st.articles.domain.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:

Image

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:
1
2
3
4
5
6
7
        // 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:

M=model
business classes, data access classes, and the database
V=views
JSP pages
C=controller
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:

M=model
business classes, data access classes, and the database
V = Views
the JSP pages
C = Controller
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:

Image

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

Image

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:

Image

3.6.3.4. Application configuration

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

Image

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:
main.do
to display the list of articles
liste.do
to display the list of articles
info.do
to display information about a specific item
purchase.do
to purchase a specific item
cart.do
to view the shopping cart
remove-purchase.do
to remove a purchase from the shopping cart
confirmcart.do
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]:

Image

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:

actions
Hashtable[] object - the array of menu options
listarticles
ArrayList of objects of type [Item]
message
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:

Image

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:

actions
object Hashtable[] - the array of menu options
item
object of type [Article] - item to display
msg
String object - message to display in case of an error with the quantity
qte
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:

Image

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:

Image

It is displayed following a request to /panier.do or /retirerachat.do?id=ID. The controller request parameters are as follows:

actions
Hashtable[] object - the array of menu options
cart
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:

Image

It is displayed following a request to /panier.do or /retirerachat.do?id=ID. The controller request parameters are as follows:

actions
Hashtable[] object - the array of menu options

Code:

1
2
3
4
5
6
7
8
9
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>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:

Image

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:

actions
Hashtable[] object - the array of menu options
errors
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:

1
2
3
4
5
6
7
8
        <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:

Image

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:
1
2
3
4
5
6
7
8
    // 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:
1
2
3
4
5
6
7
8
    // 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:
1
2
3
4
5
6
    // 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:

1
2
3
4
5
6
7
8
9
        <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:
1
2
3
4
5
6
7
8
9
        <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:

M = Model
business classes, data access classes, and the database
V = Views
the JSP pages
C = Controller
the servlet for processing client requests, [Action] objects

With Spring, we use the same architecture:

M=model
business classes, data access classes, and the database
V=views
the JSP pages
C = Controller
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:

Image

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

Image

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:

Image

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:

Image

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:
main.do
to display the list of articles
liste.do
to display the list of articles
info.do
to display information about a specific item
purchase.do
to purchase a specific item
cart.do
to view the shopping cart
remove-purchase.do
to remove a purchase from the shopping cart
confirmcart.do
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]:

Image

3.7.4. The JSP views

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

Image

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:

Image

Let’s review how the Spring application works using an example:

  • The user requests the URL [http://localhost:8080/springwebarticles/main.do]

Image

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:

Image

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:
1
2
3
4
5
6
7
        // 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:

Image

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:
1
2
3
4
5
6
7
8
9
    // 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:

Image

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:

Image

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:

Image

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:

Image

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.