Skip to content

3. Artículo 2 - Ejemplos de arquitecturas web de tres capas

Objetivos del artículo:

  • arquitecturas de tres capas
  • arquitectura web básica MVC
  • arquitectura Struts MVC
  • arquitectura Spring MVC

Herramientas utilizadas:

  • Spring: http://www.springframework.org/
  • iBatis SqlMap: http://www.ibatis.com/
  • JUnit: http://www.junit.org/index.htm
  • Eclipse: http://www.eclipse.org/
  • Struts: http://struts.apache.org/
  • Firebird: http://firebird.sourceforge.net/: SGBD, controlador JDBC. De hecho, cualquier fuente JDBC sirve.
  • IBExpert, edición personal: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (marzo de 2005). IBExpert permite administrar gráficamente el SGBD Firebird.
  • Tomcat: http://jakarta.apache.org/tomcat/
  • Complemento de Tomcat para Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. Véase también el documento https://tahe.developpez.com/java/eclipse/

Para comprender este documento se requieren varios conocimientos previos. Algunos de ellos se pueden adquirir en documentos que he escrito. En ese caso, los menciono. Es evidente que esto es solo una sugerencia y que el lector puede utilizar sus documentos favoritos.

  • lenguaje Java: [https://tahe.developpez.com/java/cours]
  • programación web en Java: [https://tahe.developpez.com/java/web/]
  • Programación web con Java, Eclipse y Tomcat: [https://tahe.developpez.com/java/eclipse/]
  • programación web con Struts: [https://tahe.developpez.com/java/struts/]
  • Uso del aspecto IoC de Spring: [https://tahe.developpez.com/java/springioc]
  • Biblioteca de etiquetas JSTL: [https://tahe.developpez.com/java/eclipse/] (en parte)
  • Documentación de iBatis SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (marzo de 2005).

Las ideas de este documento provienen de un libro que leí durante el verano de 2004, una magnífica obra de Rod Johnson: J2EE Development without EJB, publicado por Wrox.


3.1. La aplicación webarticles

Aquí queremos presentar algunos elementos de una aplicación web de comercio electrónico. Esta permitirá a los usuarios de la web

  • consulte una lista de artículos provenientes de una base de datos
  • agregar algunos de ellos a un carrito de compras
  • y validar dicho carrito. Esta validación tendrá como único efecto actualizar, en la base de datos, los inventarios de los artículos comprados.

Las diferentes vistas que se le mostrarán al usuario serán las siguientes:

  • la vista [LISTE], que muestra una lista de los artículos a la venta

Image

  • la vista [INFOS], que brinda información adicional sobre un producto:

Image

  1. la vista [PANIER], que muestra el contenido de la canasta del cliente

Image

  1. la vista [PANIERVIDE] para el caso en que el carrito del cliente esté vacío

Image

  1. la vista [ERREURS], que notifica cualquier error de la aplicación

Image

3.2. Arquitectura general de la aplicación

Se desea desarrollar una aplicación con la siguiente estructura de tres capas:

  • Las tres capas son independientes gracias al uso de interfaces de Java
  • La integración de las diferentes capas se lleva a cabo mediante Spring
  • cada capa se encuentra en paquetes separados: web (capa de interfaz de usuario), domain (capa de negocio) y dao (capa de acceso a datos).

Supondremos aquí que las capas [domain] y [dao] ya están listas. Nos centraremos únicamente en la capa [web], que nos proponemos desarrollar de varias maneras:

  • utilizando una tecnología clásica de servlet controlador - páginas JSP
  • utilizando la tecnología Struts MVC
  • utilizando la tecnología Spring: MVC

En todos los casos, la aplicación seguirá una arquitectura MVC (Modelo - Vista - Controlador). Si retomamos el esquema de capas anterior, la arquitectura MVC se integra en él de la siguiente manera:

El procesamiento de una solicitud de un cliente se lleva a cabo siguiendo los siguientes pasos:

  1. el cliente envía una solicitud al controlador. Este controlador es un servlet por el que pasan todas las solicitudes de los clientes. Es la puerta de entrada de la aplicación. Es la C de MVC.
  2. El controlador procesa esta solicitud. Para ello, puede necesitar la ayuda de la capa de negocio, lo que se conoce como el modelo M en la estructura MVC.
  3. El controlador recibe una respuesta de la capa de negocio. La solicitud del cliente ha sido procesada. Esto puede dar lugar a varias respuestas posibles. Un ejemplo clásico es
    • una página de errores si la solicitud no se pudo procesar correctamente
    • una página de confirmación, en caso contrario
  4. el controlador elige la respuesta (= vista) que se enviará al cliente. Esta suele ser una página que contiene elementos dinámicos. El controlador proporciona estos elementos a la vista.
  5. La vista se envía al cliente. Es la V de MVC.

3.3. El modelo

Aquí analizamos la M de MVC. El modelo se compone de los siguientes elementos:

  1. las clases de negocio
  2. las clases de acceso a datos
  3. la base de datos

3.3.1. La base de datos

La base de datos contiene únicamente una tabla llamada ARTICLES. Esta se generó con los siguientes comandos SQL:

CREATE TABLE ARTICLES (
    ID            INTEGER NOT NULL,
    NOM           VARCHAR(30) NOT NULL,
    PRIX          NUMERIC(15,2) NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);


/* restricciones */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');

/* clave primaria */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
clave primaria que identifica un artículo de manera única
nom
nombre del artículo
prix
su precio
stockactuel
su stock actual
stockminimum
el nivel de existencias por debajo del cual se debe realizar un pedido de reabastecimiento

En las pruebas siguientes, se utilizó una base de datos [Firebird]. [Firebird] es una versión de «código abierto» de SGBD. El controlador JDBC [firebirdsql-full.jar] se encuentra en la carpeta [WEB-INF/lib] de la aplicación web.

3.3.2. Los paquetes de la plantilla

El modelo M se proporciona aquí en forma de tres archivos comprimidos:

  • istia.st.articles.dao: contiene las clases de acceso a los datos de la capa [dao]
  • istia.st.articles.exception: contiene una clase de excepción para esta gestión de artículos
  • istia.st.articles.domain: contiene las clases de negocio de la capa [domain]
archive
contenu
rôle
istia.st.articles.dao
- contiene el paquete [istia.st.articles.dao], que a su vez contiene los siguientes elementos:
- [IArticlesDao]: la interfaz de acceso a la capa DAO. Es la única interfaz que ve la capa [domain]. No ve ninguna otra.
- [Article]: clase que define un artículo
- [ArticlesDaoSqlMap]: clase de implementación de la interfaz [IArticlesDao] con la herramienta SqlMap
capa de acceso a datos – se encuentra íntegramente en la capa [dao] de la arquitectura de tres capas de la aplicación web
istia.st.articles.domain
- contiene el paquete [istia.st.articles.domain], que a su vez contiene los siguientes elementos:
- [IArticlesDomain]: la interfaz de acceso a la capa [domain]. Es la única interfaz que ve la capa web. No ve ninguna otra.
- [AchatsArticles]: una clase que implementa [IArticlesDomain]
- [Achat]: clase que representa la compra de un cliente
- [Panier]: clase que representa el conjunto de compras de un cliente
representa el modelo de compras en la web; se encuentra íntegramente en la capa [domain] de la arquitectura de tres capas de la aplicación web
istia.st.articles.exception
- contiene el paquete [istia.st.articles.exception], que a su vez contiene los siguientes elementos:
- [UncheckedAccessArticlesException]: clase que define una excepción de tipo [RuntimeException]. Este tipo de excepción es lanzada por la capa [dao] tan pronto como se presenta un problema de acceso a los datos.
 

3.3.3. El paquete [istia.st.articles.dao]

La clase que define un artículo es la siguiente:

package istia.st.articles.dao;
import istia.st.articles.exception.UncheckedAccessArticlesException;

/**
 * @author ST - ISTIA
 *  
 */
public class Article {
  private int id;
  private String nom;
  private double prix;
  private int stockActuel;
  private int stockMinimum;

  /**
   * constructeur par défaut
   */
  public Article() {
  }

  public Article(int id, String nom, double prix, int stockActuel,
      int stockMinimum) {
     // inicializa los atributos de instancia
    setId(id);
    setNom(nom);
    setPrix(prix);
    setStockActuel(stockActuel);
    setStockMinimum(stockMinimum);
  }

     // getters - setters
  public int getId() {
    return id;
  }

  public void setId(int id) {
     // ¿ID válido?
    if (id < 0)
      throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
    this.id = id;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
     // ¿Nombre válido?
    if(nom==null || nom.trim().equals("")){
      throw new UncheckedAccessArticlesException("Le nom est [null] ou vide");
    }
    this.nom = nom;
  }

  public double getPrix() {
    return prix;
  }

  public void setPrix(double prix) {
     // ¿Precio válido?
    if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
    this.prix = prix;
  }

  public int getStockActuel() {
    return stockActuel;
  }

  public void setStockActuel(int stockActuel) {
     // ¿Existencias válidas?
    if (stockActuel < 0)
      throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
    this.stockActuel = stockActuel;
  }

  public int getStockMinimum() {
    return stockMinimum;
  }

  public void setStockMinimum(int stockMinimum) {
     // ¿Existencias válidas?
    if (stockMinimum < 0)
      throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
    this.stockMinimum = stockMinimum;
  }

  public String toString() {
    return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
        + stockMinimum + "]";
  }
}

Esta clase ofrece:

  1. un constructor que permite establecer los 5 datos de un artículo
  2. métodos de acceso, a menudo llamados «getters» y «setters», que sirven para leer y escribir los 5 datos. Los nombres de estos métodos siguen la norma JavaBean. Es habitual utilizar objetos JavaBean en la capa DAO para interactuar con los datos de SGBD.
  3. una verificación de los datos ingresados en el artículo. En caso de datos erróneos, se lanza una excepción.
  4. un método toString que permite obtener el valor de un artículo en forma de cadena de caracteres. Esto suele ser útil para la depuración de una aplicación.

La interfaz [IArticlesDao] se define de la siguiente manera:

package istia.st.articles.dao;

import istia.st.articles.domain.Article;
import java.util.List;

/**
 * @author ST-ISTIA
 *
 */
public interface IArticlesDao {

  /**
   * @return : liste de tous les articles
   */
  public List getAllArticles();

  /**
   * @param unArticle :
   *          l'article à ajouter
   */
  public int ajouteArticle(Article unArticle);

  /**
   * @param idArticle :
   *          id de l'article à supprimer
   */
  public int supprimeArticle(int idArticle);

  /**
   * @param unArticle :
   *          l'article à modifier
   */
  public int modifieArticle(Article unArticle);

  /**
   * @param idArticle :
   *          id de l'article cherché
   * @return : l'article trouvé ou null
   */
  public Article getArticleById(int idArticle);

  /**
   * vide la table des articles
   */
  public void clearAllArticles();

  /**
   *
   * @param idArticle id de l'article dont on change le stock
   * @param mouvement valeur à ajouter au stock (valeur signée)
   */
  public int changerStockArticle(int idArticle, int mouvement);
}

La función de los distintos métodos de la interfaz es la siguiente:

getAllArticles
devuelve todos los artículos de la tabla ARTICLES en una lista de objetos [Article]
clearAllArticles
vacía la tabla ARTICLES
getArticleById
devuelve el objeto [Article] identificado por su clave primaria
ajouteArticle
permite agregar un artículo a la tabla ARTICLES
modifieArticle
permite modificar un artículo de la tabla [ARTICLES]
supprimerArticle
permite eliminar un artículo de la tabla [ARTICLES]
changerStockArticle
permite modificar el stock de un artículo de la tabla [ARTICLES]

La interfaz pone a disposición de los programas de los clientes una serie de métodos definidos únicamente por sus firmas. No se ocupa de la forma en que estos métodos se implementarán realmente. Esto aporta flexibilidad a una aplicación. El programa del cliente realiza sus llamadas a una interfaz y no a una implementación específica de la misma.

La elección de una implementación específica se realizará mediante un archivo de configuración de Spring. Aquí nos proponemos implementar la interfaz IArticlesDao utilizando un producto de código abierto llamado SqlMap. Esto nos permitirá eliminar cualquier instrucción SQL del código Java.

La clase de implementación [ArticlesDaoSqlMap] se define de la siguiente manera:

package istia.st.articles.dao;

// Importaciones
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;

public class ArticlesDaoSqlMap implements IArticlesDao {

   // Campos
  private SqlMapClient sqlMap;

   // Constructores
  public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }

   // Métodos
  public SqlMapClient getSqlMap() {}
  public void setSqlMap(SqlMapClient sqlMap) { }
  public synchronized List getAllArticles() {}
  public synchronized int ajouteArticle(Article unArticle) {}
  public synchronized int supprimeArticle(int idArticle) {}
  public synchronized int modifieArticle(Article unArticle) {}
  public synchronized Article getArticleById(int idArticle) {}
  public synchronized void clearAllArticles() { }
  public synchronized int changerStockArticle(int idArticle, int mouvement) {}
}

Todos los métodos de acceso a los datos se han sincronizado para evitar problemas de acceso concurrente a la fuente de datos. En un momento dado, solo un hilo tiene acceso a un método específico.

La clase [ArticlesDaoSqlMap] utiliza la herramienta [Ibatis SqlMap]. La ventaja de esta herramienta es que permite extraer el código SQL de acceso a los datos del código Java. Este se coloca entonces en un archivo de configuración. Tendremos ocasión de volver sobre esto más adelante. Para crearse, la clase [ArticlesDaoSqlMap] necesita un archivo de configuración cuyo nombre se pasa como parámetro al constructor de la clase. Este archivo de configuración define la información necesaria para:

  • acceder al SGBD en el que se encuentran los artículos
  • administrar un grupo de conexiones
  • administrar las transacciones

En nuestro ejemplo, se llamará [sqlmap-config-firebird.xml] y definirá el acceso a una base de datos Firebird:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
  <transactionManager type="JDBC">
      <dataSource type="SIMPLE">
            <property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
                <property name="JDBC.ConnectionURL"
                    value="jdbc:firebirdsql:localhost/3050:D:/data/Databases/firebird/dbarticles.gdb"/>
                <property name="JDBC.Username" value="sysdba"/>
                <property name="JDBC.Password" value="masterkey"/>
                <property name="JDBC.DefaultAutoCommit" value="true"/>
        </dataSource>
  </transactionManager>
  <sqlMap resource="articles.xml"/>
</sqlMapConfig>

El archivo de configuración [articles.xml] mencionado anteriormente permite definir cómo crear una instancia de la clase [istia.st.articles.dao.Article] a partir de una fila de la tabla [ARTICLES] del SGBD. También define las consultas SQL que permitirán a la capa [dao] obtener los datos de la fuente de datos Firebird.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">

     <!-- un alias para la clase istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- la asignación  ORM: fila de la tabla ARTICLES - instancia de la clase Artículo -->
  <resultMap id="article" class="article">
    <result property="id" column="ID"/>
    <result property="nom" column="NOM"/>
    <result property="prix" column="PRIX"/>
    <result property="stockActuel" column="STOCKACTUEL"/>
    <result property="stockMinimum" column="STOCKMINIMUM"/>
  </resultMap>

     <!-- la consulta SQL para obtener todos los artículos -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- la consulta SQL para eliminar todos los artículos -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- la consulta SQL para insertar un artículo -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#(id#,#nombre#,#precio#,#stockactual#,#stockmínimo#)
</statement>

     <!-- la consulta SQL para eliminar un artículo específico -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- la consulta SQL para modificar un artículo específico -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nombre#,
    prix=#precio#, stockactual=#stockactual#, stockmínimo=#stockmínimo# donde
    id=#id#
</statement>

     <!-- la consulta SQL para obtener un artículo específico -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- la consulta SQL para modificar el stock de un artículo específico -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#movimiento#
    where id=#id# y stockActuel+#movimiento#&gt;=0
</statement>
</sqlMap>

El código del paquete [dao] se encuentra en el anexo.

3.3.4. El paquete [istia.st.articles.domain]

La interfaz [IArticlesDomain] desacopla la capa [métier] de la capa [web]. Esta última accede a la capa [métier/domain] a través de esta interfaz sin preocuparse por la clase que realmente la implementa. La interfaz define las siguientes acciones para acceder a la capa de negocio:

 package istia.st.articles.domain;

// Importaciones
import java.util.ArrayList;
import java.util.List;

public abstract interface IArticlesDomain {

   // Métodos
  void acheter(Panier panier);
  List getAllArticles();
  Article getArticleById(int idArticle);
  ArrayList getErreurs();
}
List getAllArticles()
devuelve la lista de objetos [Article] para presentar al cliente
Article getArticleById(int idArticle)
devuelve el objeto [Article] identificado por [idArticle]
void acheter(Panier panier)
valida el carrito del cliente restando de los inventarios de los artículos comprados la cantidad adquirida; puede fallar si el inventario es insuficiente
ArrayList getErreurs()
devuelve la lista de errores que se han producido; queda vacía si no hay errores

Aquí, la interfaz [IArticlesDomain] será implementada por la siguiente clase [AchatsArticles]:

package istia.st.articles.domain;

// Importaciones
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.List;

public class AchatsArticles implements IArticlesDomain {

   // Campos
  private IArticlesDao articlesDao;
  private ArrayList erreurs;

   // Constructores
  public AchatsArticles(IArticlesDao articlesDao) { }

   // Métodos
  public ArrayList getErreurs() {}
  public List getAllArticles() {}
  public Article getArticleById(int id) {}
  public void acheter(Panier panier) { }
}

Esta clase implementa los cuatro métodos de la interfaz [IArticlesDomain]. Cuenta con dos campos privados:

IArticlesDao articlesDao
el objeto de acceso a datos proporcionado por la capa de acceso a datos
ArrayList erreurs
la lista de posibles errores

Para crear una instancia de la clase, se debe proporcionar el objeto que permite el acceso a los datos de SGBD:

public AchatsArticles(IArticlesDao articlesDao)
constructor

La clase [Achat] representa una compra del cliente:

package istia.st.articles.domain;

public class Achat {

   // Campos
  private Article article;
  private int qte;

  // Constructores
  public Achat(Article article, int qte) { }

  // Métodos
  public double getTotal() {}
  public Article getArticle() {}
  public void setArticle(Article article) { }
  public int getQte() {}
  public void setQte() { }
  public String toString() {}
}

La clase [Achat] es una JavaBean con los siguientes campos y métodos:

article
el artículo comprado
qte
la cantidad comprada
double getTotal()
muestra el monto de la compra
String toString()
cadena de identidad del objeto

La clase [Panier] representa el conjunto de compras del cliente:

package istia.st.articles.domain;

// Importaciones
import java.util.ArrayList;

public class Panier {

  // Campos
  private ArrayList achats;

   // Constructores
  public Panier() { }

   // Métodos
  public ArrayList getAchats() {}
  public void ajouter(Achat unAchat) { }
  public void enlever(int idAchat) { }
  public double getTotal() {}
  public String toString() { }
}

La clase [Panier] es una JavaBean con los siguientes campos y métodos:

achats
la lista de compras del cliente: lista de objetos de tipo [Achat]
void ajouter(Achat unAchat)
agrega una compra a la lista de compras
void enlever(int idArticle)
elimina la compra del artículo idArticle
double getTotal()
muestra el total de las compras
String toString()
devuelve la cadena de identificación del carrito
ArrayList getAchats()
muestra la lista de compras

El código del paquete [domain] se encuentra en el anexo.

3.3.5. El paquete [istia.st.articles.exception]

Este paquete contiene la clase que define la excepción lanzada por la capa [dao] cuando encuentra un problema de acceso a la fuente de datos:

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. Pruebas del modelo

El modelo M se ha probado en Eclipse con la siguiente configuración:

Image

Comentarios:

  • en [WEB-INF/lib] se encuentran:
    • los archivos necesarios para la herramienta [ibatis SqlMap], encargada de acceder a SGBD Firebird: ibatis-*.jar
    • el archivo necesario para la herramienta [spring]: spring.jar
    • el controlador JDBC de SGBD [Firebird]: firebirdsql-full.jar
    • los archivos necesarios para los registros: log4-*.jar, commons-logging.jar
    • los tres archivos del modelo probado: istia.st.articles.*.jar
    • el archivo necesario para la herramienta de prueba [junit]
  • En [WEB-INF/src] se encuentran los archivos de configuración que Eclipse copiará automáticamente a [WEB-INF/classes]:
    • los archivos de configuración de la herramienta [sqlmap]: sqlmap-config-firebird.xml, articles.xml
    • los de la herramienta [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
    • el archivo de configuración de la herramienta [log4j]: log4j.properties
  • en el paquete [istia.st.articles.tests] se encuentran las clases de prueba del modelo

3.3.6.1. Pruebas de la capa [dao]

La clase de prueba JUnit de la capa [dao] es la siguiente. Al leerla, se puede entender cómo se utilizan los métodos de la interfaz [IArticlesDao]:

package istia.st.articles.tests.dao;

import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.dao.Article;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

// prueba de la clase ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {

     // una instancia de la clase probada
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // obtiene una instancia de acceso a los datos
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }

    public void testGetAllArticles() {
         // muestra los artículos
        listArticles();
    }

    public void testClearAllArticles() {
         // vacía la tabla de artículos
        articlesDao.clearAllArticles();
         // lee la tabla ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
    }

    public void testAjouteArticle() {
         // elimina el contenido de ARTICLES
        articlesDao.clearAllArticles();
         // lee la tabla ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserción
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // lee la tabla ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         //la muestra
        listArticles();
    }

    public void testSupprimeArticle() {
         // elimina el contenido de ARTICLES
        articlesDao.clearAllArticles();
         // lee la tabla ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserción
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // lee la tabla ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // eliminación
        articlesDao.supprimeArticle(4);
         // lee la tabla ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(1, articles.size());
         // muestra la tabla
        listArticles();
    }

    public void testModifieArticle() {
         // elimina el contenido de ARTICLES
        articlesDao.clearAllArticles();
         // lee la tabla ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserción
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // lee la tabla ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
         // modificación
        articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
         // getById
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getPrix(), 44, 1e-6);
         // muestra la tabla
        listArticles();
    }

    public void testGetArticleById() {
         // elimina el contenido de ARTICLES
        articlesDao.clearAllArticles();
         // lee la tabla ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserción
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // lee la tabla ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

    private void listArticles() {
         // lee la tabla ARTICLES
        List articles = articlesDao.getAllArticles();
         // se muestran los artículos leídos
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

    public void testChangerStockArticle() throws InterruptedException {
         // elimina el contenido de ARTICLES
        articlesDao.clearAllArticles();
         // inserción
        int nbArticles = articlesDao.ajouteArticle(new Article(3, "article3",
                30, 101, 3));
        assertEquals(nbArticles, 1);
        nbArticles = articlesDao.ajouteArticle(new Article(4, "article4", 40,
                40, 4));
        assertEquals(nbArticles, 1);
         // creación de 100 subprocesos para actualizar el inventario del artículo 3
        Thread[] taches = new Thread[100];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
            taches[i].start();
        }
         // se espera a que terminen los hilos
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // recuperar el artículo 3 y verificar su stock
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        assertEquals(1, unArticle.getStockActuel());
         // modificación del stock del artículo 4
        boolean erreur = false;
        int nbLignes = articlesDao.changerStockArticle(4, -100);
        assertEquals(0, nbLignes);
         // muestra la tabla
        listArticles();
    }
}

Comentarios:

  • la clase de prueba almacena, mediante su método setUp, una instancia de la clase que se va a probar:
1
2
3
4
5
6
7
8
     // una instancia de la clase que se está probando
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // obtiene una instancia de acceso a datos
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }
  • El objeto a probar es proporcionado por [Spring]. En el ejemplo anterior, se solicita el bean de Spring denominado [articlesDao]. Este bean se define en el archivo de configuración de Spring [spring-config-test-dao.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la clase de acceso a datos -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
</beans>

Como se ve arriba, el bean [articlesDao] es una instancia de la clase [istia.st.articles.dao.ArticlesDaoSqlMap]. Esta clase tiene un constructor que espera como parámetro el nombre del archivo de configuración de la herramienta [SqlMap]. Aquí se le proporciona ese nombre: [sqlmap-config-firebird.xml]. Este último ya se ha descrito anteriormente y proporciona toda la información necesaria para acceder a los datos de SGBD.

El método [testChangerStockArticle] crea 100 subprocesos encargados de reducir el inventario de un artículo determinado. El objetivo aquí es probar los accesos concurrentes al SGBD. Debido a que el método [changerStockArticle] de la clase [istia.st.articles.dao.ArticlesDaoSqlMap] se ha sincronizado, esta prueba pasa. Si se elimina la sincronización, ya no pasa. La clase encargada de actualizar el inventario es la siguiente:

package istia.st.articles.tests;

import istia.st.articles.dao.IArticlesDao;

public class ThreadMajStock extends Thread {

    /**
     * nom du thread
     */
    private String name;

    /**
     * objet d'accès aux données
     */
    private IArticlesDao articlesDao;

    /**
     * 
     * @param name
     *            le nom du thread afin de l'identifier
     * @param articlesDao
     *            l'objet d'accès aux données du sgbd
     */
    public ThreadMajStock(String name, IArticlesDao articlesDao) {
        this.name = name;
        this.articlesDao = articlesDao;
    }

    /**
     * décrémente le stock de l'article 3 d'une unité fait un suivi écran des
     * opérations
     */
    public void run() {
         // seguimiento
        System.out.println(name + " lancé");
         // modificación del inventario del artículo 3
        articlesDao.changerStockArticle(3, -1);
         // seguimiento
        System.out.println(name + " terminé");
    }
}
  • La clase anterior reduce en 1 el inventario del artículo n.º 3

3.3.6.2. Pruebas de la capa [domain]

La clase de prueba JUnit de la capa [domain] es la siguiente:

package istia.st.articles.tests.domain;

import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.Article;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

// prueba de la clase ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {

     // una instancia de la clase de acceso al dominio
    private IArticlesDomain articlesDomain;

     // una instancia de la clase de acceso a datos
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // obtiene una instancia de acceso al dominio
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // obtiene una instancia de acceso a datos
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }

     // recuperación de un artículo específico
    public void testGetArticleById() {
         // elimina el contenido de ARTICLES
        articlesDao.clearAllArticles();
         // lee la tabla ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserción
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // lee la tabla ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDomain.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

     // visualización en pantalla
    private void listArticles() {
         // lee la tabla ARTICLES
        List articles = articlesDomain.getAllArticles();
         // se muestran los artículos leídos
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

     // compras de artículos
    public void testAchatPanier(){
         // elimina el contenido de ARTICLES
        articlesDao.clearAllArticles();
         // lee la tabla ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserción
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // lee la tabla ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // creación de un carrito con dos compras
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // verificaciones
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // validación del carrito
        articlesDomain.acheter(panier);
         // verificaciones
        assertEquals(0,articlesDomain.getErreurs().size());
        assertEquals(0,panier.getAchats().size());
         // buscar el artículo n.º 3
        article3=articlesDomain.getArticleById(3);
        assertEquals(20,article3.getStockActuel());
         // buscar artículo n.º 4
        article4=articlesDomain.getArticleById(4);
        assertEquals(30,article4.getStockActuel());
         // nueva canasta
        panier.ajouter(new Achat(article3,100));
         // confirmar carrito
        articlesDomain.acheter(panier);
         // verificaciones: se ha comprado de más
         // Debe haber un error
        assertEquals(1,articlesDomain.getErreurs().size());
         // buscar artículo n.º 3
        article3=articlesDomain.getArticleById(3);
         // su stock no debe haber cambiado
        assertEquals(20,article3.getStockActuel());    
    }

     // eliminar compras
    public void testRetirerAchats(){
         // eliminación del contenido de ARTICLES
        articlesDao.clearAllArticles();
         // lee la tabla ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // inserción
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // lee la tabla ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // creación de un carrito con dos compras
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // verificaciones
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // agregar un artículo ya comprado
        panier.ajouter(new Achat(article3,10));
         // verificaciones
         // el total debe aumentarse a 1000
        assertEquals(1000.0,panier.getTotal(),1e-6);
         // Sigue habiendo 2 artículos en el carrito
        assertEquals(2,panier.getAchats().size());
         // La cantidad del artículo 3 debe haber cambiado a 20
        Achat achat=(Achat)panier.getAchats().get(0);
        assertEquals(20,achat.getQte());
         // se retira el artículo 3 del carrito
        panier.enlever(3);
         // verificaciones
         // el total debe haber cambiado a 400
        assertEquals(400.0,panier.getTotal(),1e-6);
         // Solo un artículo en el carrito
        assertEquals(1,panier.getAchats().size());
         // debe ser el artículo n.º 4
        assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
    }
}

Comentarios:

  • la clase de prueba almacena, mediante su método setUp, una instancia de la clase que se va a probar, así como una instancia de la clase de acceso a datos. Este último punto es controvertido. En teoría, la clase de prueba no debería necesitar tener acceso a la capa [dao], que ni siquiera se supone que conozca. En este caso, hemos hecho caso omiso de esta «ética» que, de haberse respetado, nos habría obligado a crear nuevos métodos en nuestra interfaz [IArticlesDomain].
     // una instancia de la clase de acceso al dominio
    private IArticlesDomain articlesDomain;

     // una instancia de la clase de acceso a datos
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // obtiene una instancia de acceso al dominio
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // obtiene una instancia de acceso a datos
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }
  • El objeto a probar lo proporciona [Spring]. Arriba, se solicita el bean de Spring denominado [articlesDomain]. Este bean se define en el archivo de configuración de Spring [spring-config-test-domain.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la clase de acceso a datos -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la clase de negocio -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Como se puede ver arriba, el bean [articlesDomain] es una instancia de la clase [istia.st.articles.domain.AchatsArticles]. Esta clase cuenta con un constructor que espera como parámetro un objeto de acceso a la capa [dao] de tipo [IArticlesDao]. Aquí, el archivo de configuración indica que este objeto es el bean denominado [articlesDao]. Esto obliga a Spring a instanciar este bean. La instanciación del bean [articlesDao] ya se explicó anteriormente. Así que, al final, se han instanciado dos beans:

  • [articlesDao] de tipo [istia.st.articles.dao.ArticlesDaoSqlMap]
  • [articlesDomain] de tipo [ istia.st.articles.domain.AchatsArticles]

Estas dos instanciaciones son provocadas por la primera llamada a Spring:

1
2
3
4
         // obtiene una instancia de acceso al dominio
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");

A continuación, se recupera el bean [articlesDomain]. Durante la segunda llamada a Spring:

1
2
3
         // obtiene una instancia de acceso a datos
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");

[Spring] simplemente devuelve una referencia al bean [articlesDao], que ya se había creado en la llamada anterior. Este es el principio del singleton. Si se solicita un bean a Spring, este lo instancia si aún no existe; de lo contrario, devuelve una referencia al bean existente.

3.4. Aplicación web MVC de tres capas

A continuación, queremos construir la siguiente aplicación web de tres capas:

La aplicación tendrá una arquitectura MVC. El modelo M ya se ha escrito y probado. Es el que se describió anteriormente. Se nos proporciona en tres archivos [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Ahora debemos escribir el controlador C y las vistas V.

Consideremos, en primer lugar, un método clásico, aquel en el que:

  • el controlador C está a cargo de un único servlet
  • las vistas V están a cargo de páginas JSP

3.5. Arquitectura MVC basada en un servlet controlador y páginas JSP

La arquitectura MVC de la aplicación será la siguiente:

M=modèle
las clases de negocio, las clases de acceso a datos y la base de datos
V=vues
las páginas JSP
C=contrôleur
el servlet de procesamiento de solicitudes de los clientes

3.5.1. El modelo

Ya se presentó anteriormente. Está compuesto por los archivos Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.5.2. Las vistas

Las vistas corresponden a las que se presentaron al inicio del documento:

LISTE
liste.jsp
Las vistas se encuentran en la carpeta [vues] de la aplicación
INFOS
infos.jsp
PANIER
panier.jsp
PANIERVIDE
paniervide.jsp
ERREURS
erreurs.jsp

3.5.3. El controlador

El controlador estará compuesto por un único servlet llamado [WebArticles]. Este procesará las diferentes solicitudes de los clientes. Dichas solicitudes se formalizarán mediante la presencia de un parámetro [action] en la solicitud HTTP del cliente:

requête
significado
Acción del controlador
respuestas posibles
action=liste
El cliente quiere la lista de
artículos
- solicita la lista de artículos a la capa
de negocio
- [LISTE]
- [ERREURS]
action=infos
El cliente solicita
información sobre uno de los artículos que aparecen en la vista
[LISTE]
- solicita el artículo a la capa de negocio
- [INFOS]
- [ERREURS]
action=achat
el cliente compra un artículo
- solicita el artículo a la capa de negocio y
lo agrega al carrito del cliente
- [INFOS] si hay un error de cantidad
- [LISTE] si no hay error
action=retirerachat
el cliente quiere eliminar un
compra de su carrito
- recupera el carrito de la sesión y lo modifica
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
El cliente quiere ver su
carrito
- recupera el carrito de la sesión
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
El cliente ha terminado sus compras
y pasa a la etapa de pago
- actualiza en la base de datos los inventarios de los
artículos comprados
- elimina de la canasta del cliente los artículos cuya
se haya confirmado su compra
- [LISTE]
- [ERREURS]

3.5.4. Configuración de la aplicación

Buscaremos configurar la aplicación de manera que sea lo más flexible posible ante cambios tales como:

  1. el cambio de las URL de las diferentes vistas
  2. el cambio de las clases que implementan las interfaces [IArticlesDao] y [IArticlesDomain]
  3. el cambio de SGBD, de la base de datos, de la tabla de artículos

3.5.5. Los cambios en las URL

Los nombres de las URL de las vistas se colocarán en el archivo de configuración de la aplicación [web.xml] junto con algunos otros parámetros:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
        <servlet-name>webarticles</servlet-name>
        <servlet-class>istia.st.articles.web.WebArticles</servlet-class>
        <init-param>
            <param-name>springConfigFileName</param-name>
            <param-value>spring-config-sqlmap-firebird.xml</param-value>
        </init-param>
        <init-param>
            <param-name>urlMain</param-name>
            <param-value>/main</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlListe</param-name>
            <param-value>/vues/liste.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlInfos</param-name>
            <param-value>/vues/infos.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlPanier</param-name>
            <param-value>/vues/panier.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlPanierVide</param-name>
            <param-value>/vues/paniervide.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlDebug</param-name>
            <param-value>/vues/debug.jsp</param-value>
        </init-param>
    </servlet>
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
    <servlet-mapping>
        <servlet-name>webarticles</servlet-name>
        <url-pattern>/main</url-pattern>
    </servlet-mapping>
</web-app>

En [web.xml] se encuentran

  • las URL de las diferentes vistas de la aplicación
  • el nombre [springConfigFileName] del archivo de configuración de Spring que permitirá la creación de objetos singleton para acceder a las capas de negocio y DAO
  • la vista [/vues/index.jsp] que se mostrará cuando la URL solicitada por el cliente sea /<context>, donde <context> es el contexto de la aplicación

3.5.6. El cambio de las clases de implementación de las interfaces

De acuerdo con el principio de las arquitecturas de tres capas, las capas deben ser independientes unas de otras. Esta independencia se logra de la siguiente manera:

  • las capas se comunican entre sí mediante interfaces y no mediante clases concretas
  • el código de una capa nunca instancia por sí mismo la clase de otra capa para utilizarla. Simplemente solicita a una herramienta externa, en este caso [Spring], una instancia de implementación de la interfaz de la capa que desea utilizar. Para ello, sabemos que no necesita conocer el nombre de la clase de implementación, sino solo el nombre del bean de Spring del que desea una referencia.

En nuestra aplicación, el archivo de configuración de Spring podría ser el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la clase de acceso a datos -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la clase de negocio -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Para tener acceso a la capa [métier], una clase de la capa [Interface utilisateur, UI] podrá solicitar el bean [articlesDomain]. Spring creará entonces un objeto de tipo [istia.st.articles.domain.AchatsArticles]. Para esta instanciación, necesita un bean de tipo [articlesDao], es decir, un objeto de tipo [istia.st.articles.dao.ArticlesDaoSqlMap]. Spring creará entonces un objeto de este tipo. Esta instanciación se realizará a partir de la información contenida en el archivo [sqlmap-config-firebird.xml], que es el archivo de configuración para el acceso a los datos mediante SqlMap. Al final de la operación, la clase [UI], que solicitó el bean [articlesDomain], cuenta con toda la cadena que la conecta con los datos de SGBD:

3.5.7. Los cambios relacionados con SGBD o con la base de datos

La independencia de la aplicación web frente a los cambios relacionados con el SGBD o con la base de datos queda garantizada aquí mediante los archivos de configuración del SqlMap. Hay dos:

  1. el archivo [sql-map-config-firebird.xml]
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <transactionManager type="JDBC">
            <dataSource type="SIMPLE">
            <property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
            <property name="JDBC.ConnectionURL"
                value="jdbc:firebirdsql:localhost/3050:d:/data/databases/firebird/dbarticles.gdb"/>
            <property name="JDBC.Username" value="sysdba"/>
            <property name="JDBC.Password" value="masterkey"/>
            <property name="JDBC.DefaultAutoCommit" value="true"/>
        </dataSource>
    </transactionManager>
    <sqlMap resource="articles.xml"/>
</sqlMapConfig>

Este archivo hace referencia aquí a una base de datos Firebird. Basta con cambiar el nombre del controlador JDBC para trabajar con otro, como SGBD.

  1. el archivo [articles.xml], que reúne las diferentes instrucciones SQL necesarias para la aplicación:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">

     <!-- un alias de la clase istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- la asignación  ORM: fila de la tabla ARTICLES - instancia de la clase Artículo -->
  <resultMap id="article" class="article">
    <result property="id" column="ID"/>
    <result property="nom" column="NOM"/>
    <result property="prix" column="PRIX"/>
    <result property="stockActuel" column="STOCKACTUEL"/>
    <result property="stockMinimum" column="STOCKMINIMUM"/>
  </resultMap>

     <!-- la consulta SQL para obtener todos los artículos -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- la consulta SQL para eliminar todos los artículos -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- la consulta SQL para insertar un artículo -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#(id#,#nombre#,#precio#,#stockactual#,#stockmínimo#)
</statement>

     <!-- la consulta SQL para eliminar un artículo específico -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- la consulta SQL para modificar un artículo específico -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nombre#,
    prix=#precio#, stockactual=#stockactual#, stockmínimo=#stockmínimo# donde
    id=#id#
</statement>

     <!-- la consulta SQL para obtener un artículo específico -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- la consulta SQL para modificar el stock de un artículo específico -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#movimiento#
    where id=#id# y stockActuel+#movimiento#&gt;=0
</statement>
</sqlMap>

Si los nombres de la tabla de artículos o de las columnas llegaran a cambiar, habría que reescribir las consultas en este archivo de configuración sin necesidad de modificar el código Java. Esto también aplicaría si una consulta fuera reemplazada por un procedimiento almacenado por razones de rendimiento.

3.5.8. La arquitectura general de la aplicación [webarticles]

Una aplicación web en Java es un rompecabezas con muchos elementos. Dotarla de una arquitectura MVC suele aumentar el número de estos elementos. La estructura de la aplicación [webarticles] bajo [eclipse] es la siguiente:

structure générale - on voit ci-dessous les
 archives Java utilisées par le projet
 Eclipse.
spring : pour Spring
ibatis : pour SqlMap
log4j, commons-logging : pour les logs
 de Spring et Sqlmap
firebird : pour le SGBD firebird
mysql : pour le SGBD MySQL
jstl, standard : pour la bibliothèque
 de balises JSTL
le dossier des sources java : contient le code Java
 ainsi que les fichiers de configuration
 spring et sqlmap. Eclipse recopie
 automatiquement ces fichiers 
dans [WEB-INF/classes]. 
C'est là que l'application les trouvera.
le dossier [WEB-INF] de l'application : contient le
 descripteur [web.xml] de l'application ainsi que le
 fichiers de définition de la bibliothèques JSTL
les vues

3.5.9. Las vistas JSP

Las vistas JSP utilizan la biblioteca de etiquetas JSTL.

3.5.9.1. entete.jsp

Para dar cierta uniformidad a las diferentes vistas, estas compartirán un mismo encabezado, el que muestra el nombre de la aplicación junto con el menú:

El menú es dinámico y lo define el controlador. Este incluye en la solicitud enviada a la página JSP un atributo de clave «actions» cuyo valor asociado es un array de tipo Hastable[]. Cada elemento de este array es un diccionario destinado a generar una opción del menú del encabezado. Cada diccionario tiene dos claves:

  • href: la URL asociada a la opción del menú
  • lien: el texto del menú

Las demás vistas de la aplicación utilizarán el encabezado definido por [entete.jsp] mediante la siguiente etiqueta JSP:

<jsp:include page="entete.jsp"/>

Al ejecutarse, esta etiqueta tendrá como efecto incluir en el código de la página JSP —que la contiene— el código de la página [entete.jsp]. Dado que la URL de la página es relativa (no incluye /), la página [entete.jsp] se buscará en la misma carpeta que la página que contiene la etiqueta <jsp:include>.

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
        <title>webarticles</title>
    </head>
    <body>
        <table>
            <tr>
                <td><h2>Magasin virtuel</h2></td>
                <c:forEach items="${actions}" var="action">
                    <td>|</td>
                    <td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
                </c:forEach>
            </tr>
        </table>
        <hr>

3.5.9.2. liste.jsp

Esta vista muestra la lista de artículos disponibles para la venta:

Se muestra tras una solicitud /main?action=liste o /main?action=validationpanier. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable[] — la tabla de opciones del menú
listarticles
ArrayList de objetos del tipo [Article]
message
objeto String: mensaje que se mostrará al pie de página

Cada enlace [Infos] de la tabla HTML de artículos tiene una URL con el formato [?action=infos&id=ID], donde ID es el campo id del artículo que se muestra.

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Liste des articles</h2>
        <table border="1">
            <tr>
                <th>NOM</th><th>PRIX</th>
            </tr>
            <c:forEach var="article" items="${listarticles}">
                <tr>
                    <td><c:out value="${article.nom}"/></td>
                    <td><c:out value="${article.prix}"/></td>
                    <td><a href="<c:out value="?action=infos&id=${article.id}"/>">Infos</a></td>
                </tr>
            </c:forEach>
        </table>
        <p>
        <c:out value="${message}"/>
    </body>
</html>

3.5.9.3. infos.jsp

Esta vista muestra información sobre un artículo y también permite su compra:

Image

Se muestra tras una solicitud /main?action=infos&id=ID o una solicitud /main?action=compra&id=ID cuando la cantidad comprada es incorrecta. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable[] — la tabla de opciones del menú
article
objeto de tipo [Article]: artículo que se mostrará
msg
objeto String: mensaje que se mostrará en caso de error en la cantidad
qte
objeto String: valor que se debe mostrar en el campo de entrada [Qte]

Los campos [msg] y [qte] se utilizan en caso de error de entrada en la cantidad:

Image

Esta página contiene un formulario que se envía al presionar el botón [Acheter]. La URL de destino de POST es [?action=achat&id=ID], donde ID es el ID del artículo comprado.

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Article d'id [<c:out value="${article.id}"/>]</h2>
        <table border="1">
            <tr>
                <th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
            </tr>
                <tr>
                    <td><c:out value="${article.nom}"/></td>
                    <td><c:out value="${article.prix}"/></td>
                    <td><c:out value="${article.stockActuel}"/></td>
                    <td><c:out value="${article.stockMinimum}"/></td>
                </tr>
        </table>
        <p>
        <form method="post" action="?action=achat&id=<c:out value="${article.id}"/>"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
                    <td><c:out value="${msg}"/></td>
                </tr>
            </table>
        </form>
    </body>
</html>

3.5.9.4. panier.jsp

Esta vista muestra el contenido de la canasta:

Image

Se muestra tras una solicitud /main?action=carrito o /main?action=retirarcompra&id=ID. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable[] - la tabla de opciones del menú
panier
objeto de tipo [Panier]: la canasta que se va a mostrar

Cada enlace [Retirer] de la tabla HTML de las compras del carrito tiene una URL con el formato [?action=retirerachat&id=ID], donde ID es el campo [id] delartículo que queremos eliminar del carrito.

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Contenu de votre panier</h2>
        <table border="1">
            <tr>
                <td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
            </tr>
            <c:forEach var="achat" items="${panier.achats}">
                <tr>
                    <td><c:out value="${achat.article.nom}"/></td>
                    <td><c:out value="${achat.qte}"/></td>
                    <td><c:out value="${achat.article.prix}"/></td>
                    <td><c:out value="${achat.total}"/></td>
                    <td><a href="<c:out value="?action=retirerachat&id=${achat.article.id}"/>">Retirer</a></td>
                </tr>
            </c:forEach>
        </table>
        <p>
        Total de la commande : <c:out value="${panier.total}"/> euros
    </body>
</html>

3.5.9.5. paniervide.jsp

Esta vista muestra la información que indica que el carrito está vacío:

Image

Se muestra tras una solicitud /main?action=carrito o /main?action=retirarcompra&id=ID. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable[] - la tabla de opciones del menú

Código:

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>Contenu de votre panier</h2>
        <p>
        Votre panier est vide.
    </body>
</html>

3.5.9.6. erreurs.jsp

Esta vista se muestra en caso de errores:

Image

Se muestra tras cualquier solicitud que genere un error, excepto en el caso de la acción de compra con una cantidad errónea, que se procesa mediante la vista [INFOS]. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable[] — la tabla de opciones del menú
erreurs
ArrayList de objetos String que representan los mensajes de error que se deben mostrar

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Les erreurs suivantes se sont produites</h2>
        <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li><c:out value="${erreur}"/></li>
            </c:forEach>
        </ul>
    </body>
</html>

3.5.9.7. index.jsp

Esta página está definida como la página de inicio de la aplicación en el archivo [web.xml] de la aplicación:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
....
    </servlet>
    <servlet-mapping>
....
    </servlet-mapping>
        <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>    
</web-app>

La vista [index.jsp] simplemente redirige al cliente al punto de entrada de la aplicación:

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

<c:redirect url="/main?action=liste"/>

3.5.10. El controlador

Aún nos queda por escribir el corazón de nuestra aplicación web: el controlador. Su función consiste en:

  • recibir la solicitud del cliente,
  • procesar la acción solicitada por este utilizando las clases de negocio,
  • enviar como respuesta la vista correspondiente.

3.5.10.1. Inicialización del controlador

Cuando el servidor de servlets carga la clase del controlador, se ejecuta su método [init]. Esto ocurrirá una sola vez. Una vez cargado en memoria, el controlador permanecerá allí y procesará las solicitudes de los distintos clientes. Cada cliente cuenta con un hilo de ejecución propio, por lo que los métodos del controlador se ejecutan simultáneamente en diferentes hilos. Cabe recordar que, por esta razón, el controlador no debe tener campos que sus métodos puedan modificar. Sus campos deben ser de solo lectura. Son inicializados por el método [init], cuya función principal es precisamente esa. De hecho, este método tiene la particularidad de ejecutarse una sola vez por un único hilo. Por lo tanto, no hay problemas de acceso concurrente a los campos del controlador en este método. El método [init] tiene como objetivo inicializar los objetos necesarios para la aplicación web, los cuales serán compartidos en modo de solo lectura por todos los hilos de los clientes. Estos objetos compartidos pueden ubicarse en dos lugares:

  • los campos privados del controlador
  • el contexto de ejecución de la aplicación (ServletContext)

El método [init] de la aplicación [webarticles] realizará las siguientes acciones:

  • verificará la presencia, en el archivo [web.xml], de los parámetros necesarios para el correcto funcionamiento de la aplicación. Estos se describieron en el párrafo 3.5.5.
  • asignará un campo privado [ArrayList erreurs] con la lista de posibles errores. Esta lista estará vacía si no hay errores, pero existirá de todos modos.
  • Si se han producido errores, el método [init] se detiene ahí. De lo contrario, crea un objeto de tipo [IArticlesDomain], que será el objeto de negocio que el controlador utilizará para sus necesidades. Como se explicó en 3.5.6, el controlador solicitará al marco Spring el bean que necesita. Esta operación de instanciación puede generar diversos errores. Si es así, estos se almacenarán, una vez más, en el campo [erreurs] del controlador.

3.5.10.2. Métodos doGet, doPost

Estos dos métodos procesan las solicitudes HTTP, GET y POST de los clientes. Estas se procesarán indistintamente. De este modo, el método [doPost] podrá redirigir al método [doGet] o viceversa. La solicitud del cliente se procesará de la siguiente manera:

  • se verificará el campo [erreurs]. Si no está vacío, significa que se produjeron errores durante la inicialización de la aplicación y que esta no puede funcionar. En ese caso, se enviará, como respuesta, la vista [ERREURS].
  • Se recuperará y verificará el parámetro [action] de la solicitud. Si no corresponde a una acción conocida, se enviará la vista [ERREURS] junto con un mensaje de error adecuado.
  • Si el parámetro [action] es válido, la solicitud del cliente se pasa a un procedimiento específico de la acción para su procesamiento. El procedimiento que procesa la acción [uneAction] tendrá la siguiente firma:
1
2
3
4
5
6
7
8
/**
   * @param request la requête du client
   * @param response la réponse au client
   * @throws IOException
   * @throws ServletException
   */
  private void doUneAction(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException; 

3.5.10.3. Procesamiento de las diferentes acciones

Los métodos que procesan las diferentes acciones posibles de la aplicación son los siguientes:

método
solicitud
procesamiento
respuestas posibles
doListe
GET /main?action=lista
- solicitar la lista de artículos
a la clase de negocio
- mostrarla
[LISTE] o [ERREURS]
doInfos
GET /main?action=infos&id=ID
- solicitar el artículo con id=ID a
la clase de negocio
- mostrarlo
[INFOS] o [ERREURS]
doAchat
POST /main?action=compra&id=ID
- la cantidad comprada forma parte de los parámetros enviados
- solicitar el artículo con id=ID a
la clase de negocio
- incluirlo en el carrito en
la sesión del cliente
[LISTE] o [INFOS]
o [ERREURS]
doRetirerAchat
GET /main?action=retirarcompra&id=ID
- eliminar el artículo con id=ID de la
lista de compras del carrito de
la sesión del cliente
[PANIER]
doPanier
GET /main?action=carrito
- Mostrar la canasta de la
sesión del cliente
[PANIER] o [PANIERVIDE]
doValidationPanier
GET /main?action=validacióncarrito
- Reducir en la base de datos las
existencias de todos los artículos
que se encuentran en el carrito de
sesión del cliente
[LISTE] o [ERREURS]

3.5.10.4. El código

  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.Achat;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST
 *  
 */
public class WebArticles extends HttpServlet {

     // campos privados
    private ArrayList erreurs = new ArrayList();
    private IArticlesDomain articlesDomain = null;
    private final String URL_MAIN = "urlMain";
    private final String URL_ERREURS = "urlErreurs";
    private final String URL_LISTE = "urlListe";
    private final String URL_INFOS = "urlInfos";
    private final String URL_PANIER = "urlPanier";
    private final String URL_PANIER_VIDE = "urlPanierVide";
    private final String URL_DEBUG = "urlDebug";
    private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
    private final String[] parameters =
        {
            URL_MAIN,
            URL_ERREURS,
            URL_LISTE,
            URL_INFOS,
            URL_PANIER,
            URL_PANIER_VIDE,
            URL_DEBUG,
            SPRING_CONFIG_FILENAME };
    private ServletConfig config;
    private final String ACTION_LISTE = "liste";
    private final String ACTION_PANIER = "panier";
    private final String ACTION_ACHAT = "achat";
    private final String ACTION_INFOS = "infos";
    private final String ACTION_RETIRER_ACHAT = "retirerachat";
    private final String ACTION_VALIDATION_PANIER = "validationpanier";
    private String urlActionListe;
    private final String lienActionListe = "Liste des articles";
    private String urlActionPanier;
    private final String lienActionPanier = "Voir le panier";
    private String urlActionValidationPanier;
    private final String lienActionValidationPanier = "Valider le panier";
    private Hashtable hActionListe = new Hashtable(2);
    private Hashtable hActionPanier = new Hashtable(2);
    private Hashtable hActionValidationPanier = new Hashtable(2);

    public void init() {
         // se recuperan los parámetros de inicialización del servlet
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // se registra el error
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // ¿Hay errores?
        if (erreurs.size() != 0) {
            return;
        }
         // se crea un objeto IArticlesDomain para acceder a la capa de negocio
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // se registra el error
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // se guardan ciertas URL de la aplicación
        hActionListe.put("href", "?action=" + ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", "?action=" + ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put(
            "href",
            "?action=" + ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // Se ha completado
        return;
    }


    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

         // se verifica cómo se realizó la inicialización del servlet
        if (erreurs.size() != 0) {
             // ¿Tenemos la URL de la página de errores?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // se muestra la página de errores
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fin
            return;
        }
         // se procesa la acción
        String action = request.getParameter("action");
        if (action == null) {
             // lista de artículos
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // lista de artículos
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // información sobre un artículo
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // compra de un artículo
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // visualización del carrito
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // eliminar un artículo de la canasta
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // confirmación del carrito
            doValidationPanier(request, response);
            return;
        }
         // acción desconocida
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // se muestra la página de errores
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // fin
        return;
    }


    private void doValidationPanier(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // el comprador ha confirmado su carrito
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // se valida esta canasta
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // anormal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // se recuperan los errores
        ArrayList erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
            request.setAttribute(
                "actions",
                new Hashtable[] { hActionListe, hActionPanier });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // se muestra la lista de artículos
        request.setAttribute("message", "Votre panier a été validé");
        doListe(request, response);
         // fin
        return;
    }


    private void doRetirerAchat(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // se retira un artículo de la canasta
        try {
            Panier panier =
                (Panier) request.getSession().getAttribute("panier");
            String strIdAchat = request.getParameter("id");
            panier.enlever(Integer.parseInt(strIdAchat));
        } catch (NumberFormatException ignored) {
        } catch (NullPointerException ignored) {
        }
         // se muestra el carrito
        doPanier(request, response);
    }


    private void doPanier(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // se muestra el carrito
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // ¿Canasta vacía?
        if (panier == null || panier.getAchats().size() == 0) {
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
                .forward(request, response);
             // fin
            return;
        }
         // hay algo en el carrito
        request.setAttribute("panier", panier);
        request.setAttribute(
            "actions",
            new Hashtable[] { hActionListe, hActionValidationPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_PANIER))
            .forward(request, response);
         // fin
        return;
    }


    private void doAchat(
        HttpServletRequest request,
        HttpServletResponse response)
        throws ServletException, IOException {

         // compra de un artículo
         // se recupera la cantidad
        int qté = 0;
        try {
            qté = Integer.parseInt(request.getParameter("qte"));
            if (qté <= 0)
                throw new NumberFormatException();
        } catch (NumberFormatException ex) {
             // cantidad incorrecta
            request.setAttribute("msg", "Quantité incorrecte");
            request.setAttribute("qte", request.getParameter("qte"));
            String url =
                config.getInitParameter(URL_MAIN)
                    + "?action=infos&id="
                    + request.getParameter("id");
            getServletContext().getRequestDispatcher(url).forward(
                request,
                response);
             // fin
            return;
        }
         // se recupera la sesión del cliente
        HttpSession session = request.getSession();
         // se crea la compra
        Article article = (Article) session.getAttribute("article");
        Achat achat = new Achat(article, qté);
         // se agrega la compra al carrito del cliente
        Panier panier = (Panier) session.getAttribute("panier");
        if (panier == null) {
            panier = new Panier();
            session.setAttribute("panier", panier);
        }
        panier.ajouter(achat);
         // se regresa a la lista de artículos
        String url = config.getInitParameter(URL_MAIN) + "?action=liste";
        getServletContext().getRequestDispatcher(url).forward(
            request,
            response);
         // fin
        return;
    }


    private void afficheDebugInfos(
        HttpServletRequest request,
        HttpServletResponse response,
        ArrayList infos)
        throws ServletException, IOException {

         // se muestra la lista de artículos
        request.setAttribute("infos", infos);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_DEBUG))
            .forward(request, response);
         // fin
        return;
    }


    public void doPost(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // igual que get
        doGet(request, response);
    }


    private void doInfos(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // la lista de errores
        ArrayList erreurs = new ArrayList();
         // se recupera el ID solicitado
        String strId = request.getParameter("id");
         // ¿algo?
        if (strId == null) {
             // no es normal
            erreurs.add("action incorrecte([infos,id=null]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // se transforma strId en un entero
        int id = 0;
        try {
            id = Integer.parseInt(strId);
        } catch (Exception ex) {
             // no es normal
            erreurs.add("action incorrecte([infos,id=" + strId + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // se solicita el artículo de la clave id
        Article article = null;
        try {
            article=articlesDomain.getArticleById(id);
        } catch (UncheckedAccessArticlesException ex) {
             // anormal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
        if (article == null) {
             // no es normal
            erreurs.add("Article de clé [" + id + "] inexistant");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // se agrega el artículo a la sesión
        request.getSession().setAttribute("article", article);
         // se muestra la página de información
        request.setAttribute("actions", new Hashtable[] { hActionListe });
         /   / request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_INFOS))
            .forward(request, response);
         // fin
        return;
    }

    private void afficheErreurs(
        HttpServletRequest request,
        HttpServletResponse response,
        ArrayList erreurs)
        throws ServletException, IOException {

         // se muestra la página de errores
        request.setAttribute("erreurs", erreurs);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
            .forward(request, response);
         // fin
        return;
    }


    private void doListe(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // la lista de errores
        ArrayList erreurs = new ArrayList();
         // se solicita la lista de artículos
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // se guarda el error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // ¿Hay errores?
        if (erreurs.size() != 0) {
             // se muestra la página de errores
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fin
            return;
        }
         // se muestra la lista de artículos
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // fin
        return;
    }

    /**
     * suivi console pour débogage
     * @param message : le message à afficher
     */
    private void affiche(String message) {
        System.out.println(message);
    }
}

Dejamos que el lector se tome su tiempo para leer y comprender este código. Esperamos que los comentarios le ayuden a hacerlo.

3.5.10.5. Pruebas de la aplicación

Veamos algunas capturas de pantalla de las pruebas. En primer lugar, la página de inicio de la aplicación:

Image

La URL solicitada era, en realidad, [http://localhost:8080/webarticles]. El lector verá que en el archivo [web.xml] definimos una página de inicio para la aplicación:

    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>    

La vista [index.jsp] se define de la siguiente manera:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main?action=liste"/>

Por lo tanto, se produjo una redirección a la URL [http://localhost:8080/webarticles/main?action=liste], como lo muestra la URL del navegador en la captura de pantalla. Por lo tanto, se solicitó la URL [/main?action=liste]. Todavía en [web.xml], la URL /main está asociada al servlet [webarticles]:

    <servlet-mapping>
        <servlet-name>webarticles</servlet-name>
        <url-pattern>/main</url-pattern>
    </servlet-mapping>

También en [web.xml], el servlet [webarticles] está asociado al servlet [ istia.st.articles.web.WebArticles]:

        <servlet-name>webarticles</servlet-name>
        <servlet-class>istia.st.articles.web.WebArticles</servlet-class>

Por lo tanto, el contenedor de servlets Tomcat carga el servlet [ istia.st.articles.web.WebArticles], si aún no lo había hecho, y ejecuta su método [init]:

    public void init() {
         // se recuperan los parámetros de inicialización del servlet
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // se registra el error
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // ¿Hay errores?
        if (erreurs.size() != 0) {
            return;
        }
         // se crea un objeto IArticlesDomain para acceder a la capa de negocio
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // se guarda el error
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // se guardan ciertas URL de la aplicación
        hActionListe.put("href", "?action=" + ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", "?action=" + ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put(
            "href",
            "?action=" + ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // Se ha completado
        return;
    }

Comentarios: el método [init]

  • verifica la presencia de ciertos parámetros de configuración
  • instancia un servicio de acceso al dominio de la aplicación mediante Spring
  • establece una lista de errores para indicar posibles errores de inicialización
  • una serie de campos privados:
    • errores: la lista de errores detectados por [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: diccionarios. Cada uno de ellos contiene la información necesaria para mostrar una opción del menú principal que se muestra en la vista [entete.jsp]
    • acticlesDomain: el servicio de acceso al modelo de la aplicación

El método [init] se ejecuta solo una vez, al cargarse inicialmente el servlet. Posteriormente, se ejecuta uno de los métodos [doGet, doPost] según el tipo [GET, POST] de la solicitud del cliente. En este caso, ambos métodos hacen lo mismo y el código se ha colocado en [doGet]:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
         // se verifica cómo se realizó la inicialización del servlet
        if (erreurs.size() != 0) {
             // ¿Tenemos la URL de la página de errores?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // se muestra la página de errores
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fin
            return;
        }
         // se procesa la acción
        String action = request.getParameter("action");
        if (action == null) {
             // lista de artículos
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // lista de artículos
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // información sobre un artículo
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // compra de un artículo
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // visualización del carrito
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // eliminar un artículo de la canasta
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // confirmación del carrito
            doValidationPanier(request, response);
            return;
        }
         // acción desconocida
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // se muestra la página de errores
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // fin
        return;
    }
  • El método [doGet] comienza por verificar si hubo errores de inicialización al finalizar el método [init]. Si es así, muestra la vista [ERREURS] y ahí termina todo.
  • De lo contrario, recupera el parámetro [action] de la solicitud del cliente. Recordemos que la aplicación se diseñó para responder a solicitudes en las que debe aparecer obligatoriamente un parámetro [action].
  • Hace que se ejecute el método vinculado a la acción. En este caso, será el método [doListe].

El método [doListe] es el siguiente:

    private void doListe(
        HttpServletRequest request,
        HttpServletResponse response)
        throws IOException, ServletException {

         // la lista de errores
        ArrayList erreurs = new ArrayList();
         // se solicita la lista de artículos
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // se guarda el error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // ¿Hay errores?
        if (erreurs.size() != 0) {
             // se muestra la página de errores
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fin
            return;
        }
         // se muestra la lista de artículos
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // fin
        return;
    }
  • Recordemos que el método [init] ha almacenado el servicio de acceso al modelo de la aplicación (capa de dominio) en un campo privado del servlet:
     // campos privados
    private IArticlesDomain articlesDomain = null;
  • A partir de este servicio de acceso, se puede solicitar la lista de artículos:
         // la lista de errores
        ArrayList erreurs = new ArrayList();
         // se solicita la lista de artículos
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // se guarda el error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
  • Si se producen errores, se envía la vista [ERREURS]:
         // ¿Hay errores?
        if (erreurs.size() != 0) {
             // se muestra la página de errores
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // fin
            return;
        }
  • de lo contrario, se envía la vista [LISTE]:
1
2
3
4
5
6
7
         // se muestra la lista de artículos
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);

En nuestro ejemplo, todo salió bien y obtuvimos la vista [LISTE]. Se invita al lector a volver a leer el código de la vista [LISTE] para comprobar que los parámetros dinámicos que espera esta vista son proporcionados correctamente por el controlador. Se debe realizar el mismo tipo de verificación para cada vista:

  • identificar los parámetros dinámicos de la vista
  • asegurarse de que el controlador los incluya correctamente en los atributos de la solicitud enviada a la vista

Ahora simplemente presentamos el recorrido de las pantallas que encuentra un usuario de la aplicación. Se invita al lector a realizar cada vez un razonamiento similar al anterior:

A partir de la lista de artículos, el usuario puede elegir un artículo:

El comprador puede adquirir aquí el artículo n.º 3. Cometamos un error al ingresar la cantidad:

Se ha reportado el error. Ahora compremos algunos artículos:

La compra se ha registrado y se ha vuelto a mostrar la lista de artículos. Revisemos el carrito:

La compra aparece correctamente en el carrito. Quitémela:

El artículo se ha eliminado de la canasta y esta se ha vuelto a mostrar. Aquí está vacía.

Compremos 100 artículos n.º 3 y 2 artículos n.º 4:

No fue posible comprar el artículo n.º 3 porque queríamos comprar 100 y solo había 30 en existencia. Esta compra se quedó en el carrito:

El artículo n.º 4, por su parte, sí se compró, como lo demuestra su nuevo saldo de 39 (40-1):

3.6. Arquitectura MVC con Struts

3.6.1. Arquitectura general de la aplicación

Volvamos a la arquitectura MVC de la aplicación:

En la versión anterior:

  • el controlador estaba a cargo de un servlet
  • las vistas estaban a cargo de páginas JSP
  • el modelo estaba a cargo de un conjunto de tres archivos .jar

En la versión de Struts:

  • el controlador estará a cargo de un servlet derivado del servlet genérico [ActionServlet] de Struts
  • las vistas serán manejadas por las mismas páginas JSP que antes, con algunas pequeñas diferencias
  • el modelo estará a cargo de los mismos tres archivos

Vamos a ver que la migración de la aplicación anterior a Struts consiste en las siguientes tareas:

  • las acciones que antes se procesaban en métodos específicos del servlet o controlador ahora se procesan mediante instancias de clases derivadas de la clase [Action] de Struts
  • escribir los archivos de configuración [web.xml] y [struts-config.xml]
  • realizar algunas modificaciones en las páginas JSP

Recordemos la arquitectura genérica MVC utilizada por STRUTS:

M=modèle
las clases de negocio, las clases de acceso a datos y la base de datos
V=vues
las páginas JSP
C=contrôleur
el servlet de procesamiento de solicitudes de los clientes, los objetos [Action] y los beans [ActionForm] asociados a los formularios.
  • El controlador es el corazón de la aplicación. Todas las solicitudes del cliente pasan por él. Es un servlet genérico proporcionado por STRUTS. En algunos casos, puede ser necesario derivarla. Para los casos sencillos, no es necesario. Este servlet genérico obtiene la información que necesita de un archivo que, por lo general, se llama struts-config.xml.
  • Si la solicitud del cliente contiene parámetros de formulario, el controlador los coloca en un objeto Bean. Los objetos Bean creados de esta manera a lo largo del tiempo se almacenan en la sesión o en la solicitud del cliente. Este aspecto es configurable. No es necesario volver a crearlos si ya se han creado anteriormente.
  • En el archivo de configuración struts-config.xml, a cada URL que deba ser procesado por el programa (y que, por lo tanto, no corresponda a una vista JSP que se pudiera solicitar directamente) se le asocia cierta información:
    • el nombre de la clase de tipo Action encargada de procesar la solicitud. En este caso también, el objeto Action instanciado puede conservarse en la sesión o en la solicitud.
    • si la URL solicitada está configurada (como en el caso del envío de un formulario al controlador), se indica el nombre del bean encargado de almacenar la información del formulario.
  • Con esta información proporcionada por su archivo de configuración, al recibir una solicitud de URL por parte de un cliente, el controlador es capaz de determinar si hay que crear un bean y cuál. Una vez instanciado, el bean puede verificar si los datos que ha almacenado y que provienen del formulario son válidos o no. El controlador invoca automáticamente un método del bean llamado validate. El bean es creado por el desarrollador. Por lo tanto, este incluye en el método validate el código que verifica la validez de los datos del formulario. Si los datos resultan inválidos, el controlador no continuará. Pasará el control a una vista cuyo nombre encontrará en su archivo de configuración. Con esto, el intercambio termina. Cabe señalar que el desarrollador puede solicitar que no se verifique la validez del formulario. Esto también se hace en el archivo struts-config.xml. En este caso, el controlador no invoca el método validate del bean.
  • Si los datos del bean son correctos, o si no hay verificación, o si no hay ningún bean, el controlador pasa el control al objeto de tipo Action asociado al URL. Lo hace solicitando la ejecución del método `execute` de ese objeto, al cual le pasa la referencia del bean que, en su caso, haya construido. Es aquí donde el desarrollador realiza lo que debe hacer: eventualmente deberá recurrir a clases de negocio o a clases de acceso a datos. Al final del procesamiento, el objeto `Action` devuelve al controlador el nombre de la vista que debe enviar como respuesta al cliente.
  • En su archivo de configuración, el controlador encontrará el URL asociado al nombre de la vista que se le ha pedido que muestre. A continuación, envía dicha vista. La comunicación con el cliente ha finalizado.

En nuestra aplicación, no utilizaremos objetos [Bean] como objetos intermediarios entre el cliente y las clases [Action]. El objeto [Action] obtendrá directamente los parámetros de la solicitud del cliente del objeto [HttpServletRequest] que reciba. Esto facilita la adaptación de nuestra aplicación inicial. Por lo tanto, la arquitectura final de nuestra aplicación será la siguiente:

M=modèle
las clases de negocio, las clases de acceso a datos y la base de datos
V=vues
las páginas JSP
C=contrôleur
el servlet de procesamiento de solicitudes de los clientes, los objetos [Action]

3.6.2. El modelo

Ya se presentó anteriormente. Está compuesto por los archivos Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.6.3. Configuración de la aplicación

3.6.3.1. Arquitectura general

La arquitectura general del proyecto Eclipse es la siguiente:

Image

3.6.3.2. Configuración del acceso a los datos

Dado que la interfaz de acceso a los datos no cambia, los archivos de configuración asociados son los mismos que en la versión anterior. Se definen en [WEB-INF/src]:

Image

En la captura de pantalla anterior, los archivos [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] son los de la versión anterior.

3.6.3.3. El directorio de archivos

En [WEB-INF/lib] se encuentran los mismos archivos que en la versión anterior, además del que necesita Struts:

Image

3.6.3.4. Configuración de la aplicación

La aplicación se configura mediante dos archivos: [web.xml, struts-config.xml] en la carpeta [WEB-INF]:

Image

El archivo [web.xml] es el siguiente:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
        <servlet-name>strutswebarticles</servlet-name>
        <servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
        <init-param>
            <param-name>config</param-name>
            <param-value>/WEB-INF/struts-config.xml</param-value>
        </init-param>
        <init-param>
            <param-name>springConfigFileName</param-name>
            <param-value>spring-config-sqlmap-firebird.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>strutswebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
        <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>        
</web-app>

¿Qué dice este archivo?

  • La página de inicio de la aplicación es [vues/index.jsp] (welcome-file)
  • Las solicitudes de URL con el formato *.do serán redirigidas al servlet [strutswebarticles] (servlet-mapping)
  • El servlet [strutswebarticles] es una instancia de la clase [ istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
  • Este servlet admite dos parámetros de inicialización
    • el nombre del archivo de configuración de Struts (config)
    • el nombre del archivo de configuración de Spring (springConfigFileName)

El archivo [struts-config.xml] es el siguiente:

<?xml version="1.0" encoding="ISO-8859-1" ?>

<!DOCTYPE struts-config PUBLIC
          "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
          "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">

<struts-config>
    <action-mappings>
        <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action 
            path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherListeArticles" path="/main.do"/>
        </action>
        <action 
            path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
            <forward name="afficherPanier" path="/vues/panier.jsp"/>
            <forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
        </action>
        <action 
            path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
            <forward name="afficherPanier" path="/panier.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action 
            path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
            <forward name="afficherListeArticles" path="/main.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
    </action-mappings>
    <message-resources parameter="ApplicationResources" null="false" />
</struts-config>

¿Qué indica este archivo de configuración?

  • Que nuestro controlador procesará las siguientes URL:
main.do
para mostrar la lista de artículos
liste.do
para mostrar la lista de artículos
infos.do
para ver información sobre un artículo específico
achat.do
para comprar un artículo específico
panier.do
para ver el carrito de compras
retirerachat.do
para eliminar un artículo de la canasta
validerpanier.do
para confirmar el carrito de compras
  • Las acciones anteriores corresponden una a una a las acciones procesadas por el servlet en la versión anterior. Para cada una de ellas se especifica la siguiente información:
  • el nombre de la clase encargada de procesar esta acción
  • las respuestas (= vistas) posibles tras el procesamiento de la acción. El controlador elegirá solo una de ellas.
  • el nombre de un archivo de mensajes para la aplicación (message-resources). En este caso, el archivo existirá, pero estará vacío. No se utilizará. Debe colocarse en el directorio [ClassPath] de la aplicación. Aquí se colocará en [WEB-INF/classes]. En Eclipse, se obtiene este resultado al colocarlo en [WEB-INF/src]:

Image

3.6.4. Las vistas JSP

Las vistas JSP utilizadas son también las de la versión anterior. Se modifican muy pocas cosas: se trata de las URL con el formato [?action=XX?id=YY& ...], que pasan a ser [/XX.do?id=YY&....]. Repetimos aquí las explicaciones ya dadas para evitar que el usuario tenga que volver atrás. Es importante comprender que la información que el controlador transmite a la vista es exactamente la misma en ambas versiones. No se ha cambiado nada en este aspecto.

3.6.4.1. entete.jsp

Para dar cierta uniformidad a las diferentes vistas, estas compartirán un mismo encabezado, el que muestra el nombre de la aplicación junto con el menú:

El menú es dinámico y lo define el controlador. Este incluye en la solicitud enviada a la página JSP un atributo de clave «actions» cuyo valor asociado es un array de tipo Hastable[]. Cada elemento de este array es un diccionario destinado a generar una opción del menú del encabezado. Cada diccionario tiene dos claves:

  • href: la URL asociada a la opción del menú
  • lien: el texto del menú

Las demás vistas de la aplicación utilizarán el encabezado definido por [entete.jsp] mediante la siguiente etiqueta JSP:

<jsp:include page="entete.jsp"/>

Al ejecutarse, esta etiqueta incluirá en el código de la página JSP —que la contiene— el código de la página [entete.jsp]. Dado que la URL de la página es relativa (no incluye /), la página [entete.jsp] se buscará en la misma carpeta que la página que contiene la etiqueta <jsp:include>.

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
        <title>webarticles</title>
    </head>
    <body>
        <table>
            <tr>
                <td><h2>Magasin virtuel</h2></td>
                <c:forEach items="${actions}" var="action">
                    <td>|</td>
                    <td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
                </c:forEach>
            </tr>
        </table>
        <hr>

Comentarios: no hay cambios respecto a la versión anterior

3.6.4.2. liste.jsp

Esta vista muestra la lista de artículos disponibles para la venta:

Se muestra tras una consulta /main.do o /validerpanier.do. Los elementos de la consulta del controlador son los siguientes:

actions
objeto Hashtable[] — la tabla de opciones del menú
listarticles
ArrayList de objetos del tipo [Article]
message
objeto String: mensaje que se mostrará al pie de página

Cada enlace [Infos] de la tabla HTML de artículos tiene una URL con el formato [/infos.do?id=ID], donde ID es el campo id del artículo que se muestra.

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Liste des articles</h2>
        <table border="1">
            <tr>
                <th>NOM</th><th>PRIX</th>
            </tr>
            <c:forEach var="article" items="${listarticles}">
                <tr>
                    <td><c:out value="${article.nom}"/></td>
                    <td><c:out value="${article.prix}"/></td>
                    <td><a href="<c:out value="infos.do?id=${article.id}"/>">Infos</a></td>
                </tr>
            </c:forEach>
        </table>
        <p>
        <c:out value="${message}"/>
    </body>
</html>

Comentarios: un cambio (resaltado arriba)

3.6.4.3. infos.jsp

Esta vista muestra información sobre un artículo y también permite su compra:

Image

Se muestra tras una consulta /infos.do?id=ID o una consulta /achat.do?id=ID cuando la cantidad comprada es incorrecta. Los elementos de la consulta del controlador son los siguientes:

actions
objeto Hashtable[] — la tabla de opciones del menú
article
objeto de tipo [Article]: artículo a mostrar
msg
objeto String: mensaje que se mostrará en caso de error en la cantidad
qte
Objeto String: valor que se mostrará en el campo de entrada [Qte]

Los campos [msg] y [qte] se utilizan en caso de error al ingresar la cantidad:

Image

Esta página contiene un formulario que se envía al presionar el botón [Acheter]. La URL de destino de POST es [/achat.do?id=ID], donde ID es el ID del artículo comprado.

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Article d'id [<c:out value="${article.id}"/>]</h2>
        <table border="1">
            <tr>
                <th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
            </tr>
                <tr>
                    <td><c:out value="${article.nom}"/></td>
                    <td><c:out value="${article.prix}"/></td>
                    <td><c:out value="${article.stockActuel}"/></td>
                    <td><c:out value="${article.stockMinimum}"/></td>
                </tr>
        </table>
        <p>
        <form method="post" action="achat.do?id=<c:out value="${article.id}"/>"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
                    <td><c:out value="${msg}"/></td>
                </tr>
            </table>
        </form>
    </body>
</html>

Comentarios: un cambio (resaltado arriba)

3.6.4.4. panier.jsp

Esta vista muestra el contenido de la canasta:

Image

Se muestra tras una solicitud /panier.do o /retirerachat.do?id=ID. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable[] — la tabla de opciones del menú
panier
objeto de tipo [Panier]: la canasta que se debe mostrar

Cada enlace [Retirer] de la tabla HTML de las compras del carrito tiene una URL con el formato [retirerachat.do?id=ID], donde ID es el campo [id] delartículo que queremos eliminar del carrito.

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Contenu de votre panier</h2>
        <table border="1">
            <tr>
                <td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
            </tr>
            <c:forEach var="achat" items="${panier.achats}">
                <tr>
                    <td><c:out value="${achat.article.nom}"/></td>
                    <td><c:out value="${achat.qte}"/></td>
                    <td><c:out value="${achat.article.prix}"/></td>
                    <td><c:out value="${achat.total}"/></td>
                    <td><a href="<c:out value="retirerachat.do?id=${achat.article.id}"/>">Retirer</a></td>
                </tr>
            </c:forEach>
        </table>
        <p>
        Total de la commande : <c:out value="${panier.total}"/> euros
    </body>
</html>

Comentarios: un cambio (resaltado arriba)

3.6.4.5. paniervide.jsp

Esta vista muestra la información que indica que el carrito está vacío:

Image

Se muestra tras una solicitud /panier.do o /retirerachat.do?id=ID. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable[] — la tabla de opciones del menú

Código:

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>Contenu de votre panier</h2>
        <p>
        Votre panier est vide.
    </body>
</html>

Comentarios: sin cambios.

3.6.4.6. erreurs.jsp

Esta vista se muestra en caso de errores:

Image

Se muestra tras cualquier solicitud que genere un error, excepto en el caso de la acción de compra con una cantidad errónea, que se procesa mediante la vista [INFOS]. Los elementos de la solicitud del controlador son los siguientes:

actions
objeto Hashtable[] — la tabla de opciones del menú
erreurs
ArrayList de objetos String que representan los mensajes de error que se deben mostrar

Código:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<jsp:include page="entete.jsp"/>
        <h2>Les erreurs suivantes se sont produites</h2>
        <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li><c:out value="${erreur}"/></li>
            </c:forEach>
        </ul>
    </body>
</html>

Comentarios: sin cambios.

3.6.4.7. index.jsp

Esta página está definida como la página de inicio de la aplicación en el archivo [web.xml] de la aplicación:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
....
    </servlet>
    <servlet-mapping>
....
    </servlet-mapping>
        <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>    
</web-app>

La vista [index.jsp] simplemente redirige al cliente al punto de entrada de la aplicación:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>

Comentarios: un cambio (resaltado arriba)

3.6.5. El controlador Struts

Struts cuenta con un controlador genérico llamado [ActionServlet]. Sabemos que un servlet cuenta con un método [init] que permite inicializar la aplicación al iniciarse. Si utilizamos el controlador genérico de Struts [ActionServlet], no tenemos acceso a su método [init]. En este caso, tenemos tareas que realizar al iniciar la aplicación, principalmente crear una instancia de un objeto de acceso al modelo. Por lo tanto, necesitamos un método [init]. Así pues, derivamos la clase [Actionservlet] en la siguiente clase [MainServlet]:

package istia.st.articles.web.struts;

import istia.st.articles.domain.IArticlesDomain;

import java.util.ArrayList;
import java.util.Hashtable;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;

import org.apache.struts.action.ActionServlet;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST - ISTIA
 *  
 */
public class MainServlet extends ActionServlet {
   // campos privados
  private ArrayList erreurs = new ArrayList();
  private IArticlesDomain articlesDomain = null;
  private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
  private final String[] parameters = { SPRING_CONFIG_FILENAME };
  private ServletConfig config;
  private final String ACTION_LISTE = "liste.do";
  private final String ACTION_PANIER = "panier.do";
  private final String ACTION_ACHAT = "achat.do";
  private final String ACTION_INFOS = "infos.do";
  private final String ACTION_RETIRER_ACHAT = "retirerachat.do";
  private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
  private String urlActionListe;
  private final String lienActionListe = "Liste des articles";
  private String urlActionPanier;
  private final String lienActionPanier = "Voir le panier";
  private String urlActionValidationPanier;
  private final String lienActionValidationPanier = "Valider le panier";
  private Hashtable hActionListe = new Hashtable(2);
  private Hashtable hActionPanier = new Hashtable(2);
  private Hashtable hActionValidationPanier = new Hashtable(2);

     // getters - setters
  public IArticlesDomain getArticlesDomain() {
    return articlesDomain;
  }
  public void setArticlesDomain(IArticlesDomain articlesDomain) {
    this.articlesDomain = articlesDomain;
  }

  public ArrayList getErreurs() {
    return erreurs;
  }
  public void setErreurs(ArrayList erreurs) {
    this.erreurs = erreurs;
  }

  public Hashtable getHActionListe() {
    return hActionListe;
  }
  public void setHActionListe(Hashtable actionListe) {
    hActionListe = actionListe;
  }

  public Hashtable getHActionPanier() {
    return hActionPanier;
  }
  public void setHActionPanier(Hashtable actionPanier) {
    hActionPanier = actionPanier;
  }

  public Hashtable getHActionValidationPanier() {
    return hActionValidationPanier;
  }
  public void setHActionValidationPanier(Hashtable actionValidationPanier) {
    hActionValidationPanier = actionValidationPanier;
  }

  public void init() throws ServletException{

     // inicialización de la clase padre
    super.init();
     // se recuperan los parámetros de inicialización del servlet
    config = getServletConfig();
    String param = null;
    for (int i = 0; i < parameters.length; i++) {
      param = config.getInitParameter(parameters[i]);
      if (param == null) {
         // se registra el error
        erreurs.add("Paramètre [" + parameters[i]
            + "] absent dans le fichier [web.xml]");
      }
    }
     // ¿Hay errores?
    if (erreurs.size() != 0) {
      return;
    }
     // se crea un objeto IArticlesDomain para acceder a la capa de negocio
    try {
      articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
          new ClassPathResource((String) config
              .getInitParameter(SPRING_CONFIG_FILENAME))))
          .getBean("articlesDomain");
    } catch (Exception ex) {
       // se guarda el error
      erreurs.add("Erreur de configuration de l'accès aux données : "
          + ex.toString());
      return;
    }
     // se guardan ciertas URL de la aplicación
    hActionListe.put("href", ACTION_LISTE);
    hActionListe.put("lien", lienActionListe);
    hActionPanier.put("href", ACTION_PANIER);
    hActionPanier.put("lien", lienActionPanier);
    hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
    hActionValidationPanier.put("lien", lienActionValidationPanier);

     // Se ha completado
    return;
  }
}

Comentarios:

  • lo interesante de la clase es su método [init] y sus campos privados
  • El método [init] hace lo mismo que el método [init] del controlador de la versión anterior:
    • verifica la presencia de ciertos parámetros de configuración
    • instancia un servicio de acceso al dominio de la aplicación mediante Spring
    • configura una lista de errores para indicar posibles errores de inicialización
  • antes de comenzar a trabajar, el método [init] llama al método [init] de la clase padre [ActionServlet]. Es esta última la que procesará el archivo de configuración de Struts [struts-config.xml].
  • Se definen varios campos privados con sus métodos de acceso:
    • errores: la lista de errores detectados por [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: diccionarios. Cada uno de ellos contiene la información necesaria para mostrar una opción del menú principal que muestra la vista [entete.jsp]
    • acticlesDomain: el servicio de acceso al modelo de la aplicación
  • El controlador de una aplicación Struts es accesible para las clases [Action], encargadas de procesar las diferentes acciones posibles. Estas clases tendrán acceso a los campos privados anteriores, ya que estos cuentan con accesores públicos.

La instanciación de este controlador se realiza mediante el archivo [web.xml]:

<web-app>
    <servlet>
        <servlet-name>strutswebarticles</servlet-name>
        <servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
        <init-param>
            <param-name>config</param-name>
            <param-value>/WEB-INF/struts-config.xml</param-value>
        </init-param>
        <init-param>
            <param-name>springConfigFileName</param-name>
            <param-value>spring-config-sqlmap-firebird.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>strutswebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

Cualquier URL que termine en .do será procesado por una instancia de la clase [istia.st.articles.web.struts.MainServlet]

3.6.6. Las acciones de la aplicación Struts

3.6.6.1. Introducción

Cada acción de Struts será objeto de una clase. En la versión anterior, cada acción era objeto de un método en el controlador de la aplicación. La implementación de la clase [Action] consiste, en la mayoría de los casos, en:

  • copiar y pegar el método que se había utilizado en la versión anterior
  • adaptar el código a las convenciones de Struts

3.6.6.2. main.do, liste.do

Estas dos acciones son idénticas y se definen en [struts-config.xml] mediante:

1
2
3
4
5
6
7
8
        <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>
        <action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Cuando se ejecuta una de estas acciones, [main.do, liste.do], en un navegador, se obtiene el siguiente resultado:

Image

El código de la clase [ListeArticlesAction] es el siguiente:

package istia.st.articles.web.struts;

import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST - ISTIA
 *  
 */
public class ListeArticlesAction extends Action {

  /**
   * affichage de la liste des articles - s'appuie sur la couche [domain]
   * 
   * @param mapping :
   *          configuration de l'action dans struts-config.xml
   * @param form :
   *          le formulaire passé à l'action - ici aucun
   * @param request :
   *          la requête HTTP du client
   * @param response :
   *          la réponse HTTP au client
   */
  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // el servlet de control
    MainServlet mainServlet = (MainServlet) this.getServlet();

     // ¿Errores de inicialización?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // se muestra la página de errores
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

     // el objeto de acceso al dominio
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();

     // la lista de errores
    ArrayList erreurs = new ArrayList();
     // se solicita la lista de artículos
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // se guarda el error
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
     // ¿Hay errores?
    if (erreurs.size() != 0) {
       // se muestra la página de errores
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // se muestra la lista de artículos
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");
  }
}

Comentarios:

  • escribir el código de una clase [Action] consiste esencialmente en escribir el código de su método [execute]
  • Se ha almacenado cierta información en la instancia del controlador. Se obtiene una referencia a esta mediante:
     // el servlet de control
    MainServlet mainServlet = (MainServlet) this.getServlet();
  • Se recupera la lista de errores de inicialización almacenados por el controlador. Si esta lista no está vacía, se envía la vista [ERREURS] al cliente:
1
2
3
4
5
6
7
8
     // ¿Errores de inicialización?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // se muestra la página de errores
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

La vista que se enviará realmente al cliente nos la proporciona [struts-config.xml]:

        <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Se trata de la vista [/vues/erreurs.jsp]. Se invita al lector a verificar qué se espera de esta vista. Esta información la proporciona aquí la acción en el objeto [request] como atributos.

  • Siempre gracias al controlador, la acción puede recuperar el objeto de acceso al modelo de la aplicación (capa de dominio):
     // el objeto de acceso al dominio
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • Una vez hecho esto, se puede solicitar la lista de artículos:
     // la lista de errores
    ArrayList erreurs = new ArrayList();
     // se solicita la lista de artículos
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // se guarda el error
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • Si se producen errores, se envía la vista [ERREURS]:
1
2
3
4
5
6
7
8
     // ¿Hay errores?
    if (erreurs.size() != 0) {
       // se muestra la página de errores
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • de lo contrario, se envía la vista [LISTE]:
1
2
3
4
5
6
     // se muestra la lista de artículos
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");

La vista que se enviará realmente al cliente nos la proporciona [struts-config.xml]:

        <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
            <forward name="afficherListeArticles" path="/vues/liste.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Se trata de la vista [/vues/liste.jsp]. Se invita al lector a verificar qué se espera de esta vista. Esta información se proporciona aquí por la acción en el objeto [request] como atributos.

3.6.6.3. infos.do

Esta acción sirve para proporcionar información sobre uno de los artículos que se muestran en la vista [LISTE]:

Esta acción está configurada de la siguiente manera en [struts-config.xml]:

        <action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

El código de la clase [InfosArticleAction] es el siguiente:

package istia.st.articles.web.struts;

import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST-ISTIA
 *  
 */
public class InfosArticleAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // el servlet de control
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // ¿Errores de inicialización?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // se muestra la página de errores
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // el objeto de acceso al dominio
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // la lista de errores
    ArrayList erreurs = new ArrayList();
     // se recupera el ID solicitado
    String strId = request.getParameter("id");
     // ¿algo?
    if (strId == null) {
       // no es normal
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // se transforma strId en un entero
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // no es normal
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // se solicita el artículo de la clave id
    Article article = null;
    try {
      article = articlesDomain.getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // anormal
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
    if (article == null) {
       // anormal
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // se agrega el artículo a la sesión
    request.getSession().setAttribute("article", article);
     // se muestra la página de información
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionListe() });
    return mapping.findForward("afficherInfosArticle");
  }
}

Comentarios:

  • El inicio del método [execute] es idéntico al analizado anteriormente. Esto también será así para las demás acciones.
  • El método recupera el parámetro [id], que normalmente debe encontrarse en la URL. De hecho, esta debe tener el formato [/infos.do?id=X]. Se realizan diversas pruebas para verificar la presencia y la validez del parámetro [id]. En caso de problema, se envía la vista [ERREURS].
  • Si [id] es válido, se solicita el artículo correspondiente a la capa [domain]. Si esta genera una excepción o si no se encuentra el artículo, nuevamente se envía la vista [ERREURS].
  • Si todo sale bien, el artículo obtenido se guarda en la sesión. Este es un punto discutible. Aquí se parte del principio de que el cliente tal vez compre este artículo. Si lo hace, se buscará en la sesión, en lugar de solicitarlo nuevamente a la capa [domain].
  • Finalmente, se muestra la vista [INFOS]. La vista que realmente se enviará al cliente nos la proporciona [struts-config.xml]:
        <action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Se trata de la vista [/vues/infos.jsp]. Se invita al lector a verificar qué se espera de esta vista. Esta información se proporciona aquí por la acción en el objeto [request] como atributos.

3.6.6.4. achat.do

Esta acción sirve para comprar el artículo mostrado por la vista [INFOS] anterior:

Una vez comprado el artículo, se vuelve a mostrar la vista [LISTE] (vista de la derecha). Si observamos el código HTML de la vista de la izquierda anterior, vemos que la etiqueta <form> está definida de la siguiente manera:

1
2
3
4
5
6
7
8
9
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>

Por lo tanto, se observa que el formulario se envía al controlador con la acción [achat.do].

Esta acción está configurada de la siguiente manera en [struts-config.xml]:

        <action 
            path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherListeArticles" path="/main.do"/>
        </action>

El código de la clase [AchatArticleAction] es el siguiente:

package istia.st.articles.web.struts;

import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST
 *  
 */
public class AchatArticleAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // el servlet de control
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // ¿Errores de inicialización?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // se muestra la página de errores
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // la lista de errores de esta acción
    ArrayList erreurs = new ArrayList();
     // se recupera la cantidad comprada
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // cantidad incorrecta
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
     // se recupera la sesión del cliente
    HttpSession session = request.getSession();
     // se recupera el artículo agregado a la sesión
    Article article = (Article) session.getAttribute("article");
     // ¿Sesión vencida?
    if(article==null){
       // se muestra la página de errores
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // se crea la nueva compra
    Achat achat = new Achat(article, qté);
     // se agrega la compra al carrito del cliente
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // se regresa a la lista de artículos
    return mapping.findForward("afficherListeArticles");
  }
}

Comentarios:

  • El inicio del método [execute] es idéntico a los analizados anteriormente.
  • Recordemos el formato del formulario enviado al controlador:
1
2
3
4
5
6
7
8
9
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>
  • Hay dos parámetros en la solicitud: [id]: número del artículo comprado; [qte]: cantidad comprada.
  • Se comprueba la presencia y la validez del parámetro [qte]. Si este parámetro se considera incorrecto, se devuelve al usuario la vista [INFOS] acompañada de un mensaje de error:
     // la lista de errores de esta acción
    ArrayList erreurs = new ArrayList();
     // se recupera la cantidad comprada
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // cantidad incorrecta
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
  • El artículo comprado se recupera de la sesión. Es posible que esta haya caducado. En ese caso, se envía la vista [ERREURS]:
     // se recupera la sesión del cliente
    HttpSession session = request.getSession();
     // se recupera el artículo agregado a la sesión
    Article article = (Article) session.getAttribute("article");
     // ¿Sesión vencida?
    if(article==null){
       // se muestra la página de errores
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
  • Si la sesión no ha caducado, el artículo se agrega al carrito, el cual también se recupera de la sesión:
     // se recupera la sesión del cliente
    HttpSession session = request.getSession();
     // se recupera el artículo guardado en la sesión
    Article article = (Article) session.getAttribute("article");
     // ¿Sesión vencida?
    if(article==null){
       // se muestra la página de errores
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // se crea la nueva compra
    Achat achat = new Achat(article, qté);
     // se agrega la compra al carrito del cliente
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • por último, se envía la vista [LISTE]:
     // se regresa a la lista de artículos
    return mapping.findForward("afficherListeArticles");
  • La vista que realmente se enviará al cliente nos la proporciona [struts-config.xml]:
        <action 
            path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherListeArticles" path="/main.do"/>
        </action>

Se trata de la vista [/main.do]. Esta vista no es una vista, sino una acción. Por lo tanto, se ejecutará la acción [/main.do] descrita anteriormente y se mostrará la lista de artículos.

3.6.6.5. panier.do

Esta acción sirve para mostrar todas las compras del cliente. Está disponible a través de la opción de menú [Voir le panier]:

El código HTML asociado al enlace [Voir le panier] es el siguiente:

<a href="panier.do">Voir le panier</a>

La acción [panier.do] está configurada de la siguiente manera en [struts-config.xml]:

        <action 
            path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
            <forward name="afficherPanier" path="/vues/panier.jsp"/>
            <forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
        </action>

El código de la clase [VoirPanierAction] es el siguiente:

package istia.st.articles.web.struts;

import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST-ISTIA
 *  
 */
public class VoirPanierAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // el servlet de control
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // ¿Errores de inicialización?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // se muestra la página de errores
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // se muestra el carrito
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // carrito vacío
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    } else {
       // hay algo en el carrito
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(), mainServlet.getHActionValidationPanier() });
      return mapping.findForward("afficherPanier");
    }
  }
}

Comentarios:

  • El inicio del método [execute] es idéntico al de los analizados anteriormente.
  • La canasta se toma de la sesión en la que se encuentra normalmente. Es posible que la sesión haya caducado y, en ese caso, no haya canasta. No se trata este caso como un error, sino que simplemente se considera que la canasta está vacía.
  • Si el carrito está vacío, se muestra la vista [PANIERVIDE]
  • ; de lo contrario, se muestra la vista [PANIER]

Las vistas que realmente se envían al cliente están definidas por la acción:

        <action 
            path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
            <forward name="afficherPanier" path="/vues/panier.jsp"/>
            <forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
        </action>

3.6.6.6. retirerachat.do

Esta acción sirve para eliminar una compra del carrito:

Después de la acción [retirerachat.do], se vuelve a mostrar el carrito (vista de la derecha arriba). Si observamos el código HTML del enlace [Valider le panier] de la vista de la izquierda arriba, tenemos lo siguiente:

<a href="retirerachat.do?id=3">Retirer</a>

Por lo tanto, la acción [retirerachat.do] recibe como parámetro el ID del artículo que se va a eliminar de la canasta. Esta acción está configurada de la siguiente manera en [struts-config.xml]:

        <action 
            path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
            <forward name="afficherPanier" path="/panier.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

El código de la clase [RetirerAchatAction] es el siguiente:

package istia.st.articles.web.struts;

import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST-ISTIA
 *  
 */
public class RetirerAchatAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // el servlet de control
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // ¿Errores de inicialización?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // se muestra la página de errores
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // la lista de errores de esta acción
    ArrayList erreurs = new ArrayList();
     // se recupera el carrito
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // Sesión vencida
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    }
     // se recupera el ID del artículo que se va a eliminar
    String strId = request.getParameter("id");
     // ¿algo?
    if (strId == null) {
       // no es normal
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // Convertimos strId a un entero
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // no es normal
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // se elimina la compra
    panier.enlever(id);
     // se vuelve a mostrar el carrito
    return mapping.findForward("afficherPanier");
  }
}

Comentarios:

  • El inicio del método [execute] es idéntico al de los analizados anteriormente.
  • El código que sigue tiene como objetivo verificar la presencia y la validez del parámetro [id]. Si resulta incorrecto, se envía la vista [ERREURS].
  • De lo contrario, la compra se elimina del carrito:
     // se elimina la compra
    panier.enlever(id);
  • luego se vuelve a mostrar el carrito:
     // se vuelve a mostrar el carrito
    return mapping.findForward("afficherPanier");

La vista que realmente se envía al cliente la define la acción:

        <action 
            path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
            <forward name="afficherPanier" path="/panier.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Se observa que, en cuanto a la vista, se activará la acción [/panier.do]. Esta ya se ha descrito anteriormente. Mostrará la vista [PANIER] o [PANIERVIDE], según el estado del carrito.

3.6.6.7. validerpanier.do

Esta acción sirve para validar las compras realizadas por el cliente. En concreto, esto se traduce en una única acción: las existencias de los artículos comprados se reducen en las cantidades adquiridas, en la base de datos. Esta acción proviene del siguiente menú:

 

El código HTML del enlace [Valider le panier] es el siguiente:

<a href="validerpanier.do">Valider le panier</a>

Al activar este enlace, se reduce el inventario y se vuelve a mostrar la lista de artículos.

Esta acción está configurada de la siguiente manera en [struts-config.xml]:

        <action 
            path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
            <forward name="afficherListeArticles" path="/main.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

El código de la clase [ValiderPanierAction] es el siguiente:

package istia.st.articles.web.struts;

import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * @author ST-ISTIA
 *  
 */
public class ValiderPanierAction extends Action {

  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {

     // el servlet de control
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // ¿Errores de inicialización?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // se muestra la página de errores
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // el objeto de acceso al dominio
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // la lista de errores en esta acción
    ArrayList erreurs = new ArrayList();
         // el comprador ha confirmado su carrito
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sesión vencida
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
         // se valida el carrito
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // no es normal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
        }
         // se recuperan los posibles errores
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
         // Todo parece estar bien OK - Se muestra la lista de artículos
    return mapping.findForward("afficherListeArticles");
  }
}

Comentarios:

  • El inicio del método [execute] es idéntico al de los analizados anteriormente.
  • Se recupera el carrito de la sesión. Si esta ha caducado, se envía la vista [ERREURS]:
     // la lista de errores de esta acción
    ArrayList erreurs = new ArrayList();
         // el comprador ha confirmado su carrito
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sesión vencida
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • Se validan las compras del carrito. Pueden producirse errores si las existencias son insuficientes para satisfacer las compras. En ese caso, se envía la vista [ERREURS]:
         // se valida el carrito
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // no es normal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
        }
         // se recuperan los posibles errores
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
  • Si todo salió bien, se vuelve a mostrar la lista de artículos:
         // Todo parece estar bien OK - Se muestra la lista de artículos
    return mapping.findForward("afficherListeArticles");

La vista que realmente se envía al cliente la define la acción:

        <action 
            path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
            <forward name="afficherListeArticles" path="/main.do"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Se observa que, en cuanto a la vista, se activará la acción [/main.do]. Esta ya se ha descrito. Mostrará la vista [LISTE].

3.7. Arquitectura de MVC con Spring

3.7.1. Arquitectura general de la aplicación

Volvamos a la arquitectura MVC de la aplicación:

En la primera versión:

  • el controlador estaba a cargo de un servlet
  • las vistas estaban a cargo de páginas JSP
  • el modelo estaba a cargo de un conjunto de tres archivos .jar

En la versión de Struts:

  • el controlador estaba a cargo de un servlet derivado del servlet genérico [ActionServlet] de Struts
  • las vistas estaban a cargo de las mismas páginas JSP que en la versión [Struts]
  • el modelo estaba a cargo de los mismos tres archivos

En la versión de Spring:

  • el controlador estará a cargo de un servlet proporcionado por Spring [DispatcherServlet]
  • las vistas serán manejadas por las mismas páginas JSP que anteriormente, con algunas pequeñas diferencias
  • el modelo estará a cargo de los mismos tres archivos

Veremos que la migración de nuestra aplicación de Struts a Spring es sencilla si aceptamos no utilizar todos los elementos recomendados para una arquitectura Spring ortodoxa MVC. Las principales transformaciones son las siguientes:

  • las acciones que antes se procesaban en métodos específicos del servlet o del controlador, o mediante instancias de clases derivadas de la clase [Action] en Struts, ahora se procesan mediante instancias de clases que implementan la interfaz Spring [Controller]
  • los archivos de configuración necesarios son los siguientes:
    • [web.xml], ya que se trata de una aplicación web. Este archivo contiene un Listener que, al momento de la inicialización de la aplicación, utilizará el archivo [applicationContext.xml]
    • [applicationContext.xml], lo que permitirá crear los beans que necesita la aplicación, en particular el del servicio de acceso al modelo
  • las vistas JSP serán idénticas a las de Struts. Tendremos que crear una nueva.

Recordemos la arquitectura MVC STRUTS utilizada en la versión anterior:

M=modèle
las clases de negocio, las clases de acceso a datos y la base de datos
V=vues
las páginas JSP
C=contrôleur
el servlet de procesamiento de solicitudes de los clientes, los objetos [Action]

Con Spring, adoptamos una arquitectura idéntica:

M=modèle
las clases de negocio, las clases de acceso a datos y la base de datos
V=vues
las páginas JSP
C=contrôleur
el servlet de procesamiento de solicitudes de los clientes, los objetos que implementan la interfaz [Controller]
  • El controlador es el corazón de la aplicación. Todas las solicitudes del cliente pasan por él. Es un servlet genérico proporcionado por SPRING. Es de tipo [DispatcherServlet]. De ahora en adelante llamaremos a este controlador el controlador [Spring].
  • El controlador [Spring] dirigirá la solicitud del cliente hacia una de las instancias de tipo [Controller]. Habrá una instancia de este tipo por cada acción que se deba procesar. Esta se define en la URL solicitada, al igual que con Struts. Así, sabremos que la acción solicitada es la acción [liste] porque la URL solicitada es [liste.do]
  • Si C es el contexto de la aplicación, el controlador [Spring] utiliza un archivo [C-servlet.xml] que desempeña la función del archivo de configuración struts-config.xml, en la versión de Struts. A cada acción que debe procesar la aplicación se le asocia el nombre de la clase de tipo Controller encargada de procesar la solicitud.
  • El controlador le pasa el control al objeto de tipo Controller asociado a la acción. Lo hace solicitando la ejecución del método handleRequest de ese objeto, al cual le transmite la solicitud del cliente. Es aquí donde el desarrollador realiza lo que debe hacer: eventualmente deberá recurrir a clases de negocio o a clases de acceso a datos. Al finalizar el procesamiento, el objeto Controller devuelve al controlador el nombre de la vista que debe enviar como respuesta al cliente.
  • En su archivo de configuración, el controlador encontrará la clase URL asociada al nombre de la vista que se le ha solicitado mostrar. A continuación, envía dicha vista. La comunicación con el cliente ha finalizado.

3.7.2. El modelo

Es el mismo que en las dos versiones anteriores. Está compuesto por los archivos Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.7.3. Configuración de la aplicación

3.7.3.1. Arquitectura general

La arquitectura general del proyecto Eclipse es la siguiente:

Image

3.7.3.2. Configuración del acceso a los datos

Dado que la interfaz de acceso a los datos no cambia, los archivos de configuración asociados son los mismos que en la versión anterior. Se definen en [WEB-INF/src]:

Image

En la captura de pantalla anterior, los archivos [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] son los de las versiones anteriores.

3.7.3.3. El directorio de archivos

En [WEB-INF/lib] se encuentran los mismos archivos que en la versión anterior, excepto los de Struts, que ya no son necesarios:

Image

3.7.3.4. Configuración de la aplicación

La aplicación se configura mediante tres archivos: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] en la carpeta [WEB-INF]:

Image

El archivo [web.xml] es el siguiente:

<?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>
     <!-- el cargador del contexto Spring de la aplicación -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- el servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- la asignación de las URL -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- el documento de entrada -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

¿Qué dice este archivo?

  • La página de inicio de la aplicación es [/vues/index.jsp] (welcome-file)
  • Las solicitudes de URL con el formato *.do serán redirigidas al servlet [springwebarticles] (servlet-mapping)
  • El servlet [springwebarticles] es una instancia de la clase [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class) proporcionada por Spring.
  • El listener [org.springframework.web.context.ContextLoaderListener] se iniciará al arrancar la aplicación. Su función principal será instanciar los beans de Spring definidos en el archivo [applicationContext.xml]

El archivo [applicationContext.xml] es el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la clase de acceso a datos -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la clase de negocio -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- la configuración de la aplicación web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Encontramos elementos conocidos y otros menos conocidos. Se instanciarán tres beans durante la inicialización de la aplicación:

  • articlesDao: servicio de acceso a la capa [dao]
  • articlesDomain: servicio de acceso al modelo
  • config: un bean en el que reuniremos la información que deben compartir todos los clientes. Este bean desempeñará el papel que tradicionalmente desempeña el contexto de la aplicación, pero con información tipada en lugar de no tipada.

El último archivo, [springwebarticles.xml], define las acciones aceptadas por la aplicación de una manera muy similar a la utilizada por el archivo Struts [struts-config.xml]. Su contenido es el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- los gestores de acciones = controladores -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

     <!-- la asignación de la aplicación-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

     <!-- gestor de vistas -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

     <!-- el archivo de mensajes -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>

¿Qué indica este archivo de configuración?

  • Que nuestro controlador procesará las siguientes URL:
main.do
para mostrar la lista de artículos
liste.do
para mostrar la lista de artículos
infos.do
para ver información sobre un artículo específico
achat.do
para comprar un artículo específico
panier.do
para ver el carrito de compras
retirerachat.do
para eliminar un artículo de la canasta
validerpanier.do
para confirmar el carrito de compras
  • Las acciones anteriores corresponden una a una a las acciones procesadas por el controlador en versiones anteriores. Para cada una de ellas se especifica el nombre de la clase encargada de procesarla. Tomemos como ejemplo la acción [/panier.do]:
  • debe ser procesada por el bean [VoirPanierController]. Este nombre es libre. Es simplemente una clave.
                <prop key="/panier.do">VoirPanierController</prop>
  • la clave [VoirPanierController] es el nombre de un bean definido en el mismo archivo de configuración:
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
  • El bean [VoirPanierController] define:
  • la clase que se debe instanciar, [ istia.st.articles.web.spring.VoirPanierController], para procesar la acción
  • cómo instanciarla. Aquí se pasa como parámetro el bean [config], definido por [applicationContext.xml] e instanciado al iniciar la aplicación. Esto se hará para todas las acciones [Controller]. De este modo, cada una de ellas dispondrá, en un campo privado, del objeto [config], en el que encontrará toda la información compartida entre todos los clientes.
  • La forma en que deben resolverse los nombres de las vistas:
     <!-- gestor de vistas -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

Al igual que con Struts, la instancia [Controller] que procesa una acción devolverá, tras el procesamiento, una clave al controlador Spring para indicarle qué vista debe mostrar. A partir de esta clave, pueden existir diversas estrategias para generar la vista asociada a la clave. La estrategia elegida es la definida por el bean [viewResolver]. Aquí, este bean está asociado a la clase [org.springframework.web.servlet.view.InternalResourceViewResolver] con diferentes parámetros de inicialización. Sin entrar en detalles, el bean [viewResolver] indica aquí que si la clave de la vista es «XX», entonces la vista generada será [/vues/XX.jsp]. El tipo de vistas enviadas al cliente se puede modificar de diversas maneras:

  • cambiando la clase de implementación del bean [viewResolver]
  • cambiando los parámetros de inicialización de la clase de implementación

De esta manera, se puede pasar de una vista HTML a una vista XML simplemente cambiando el valor del bean [viewResolver]

  • el nombre de un archivo de mensajes para la aplicación (messageSource). En este caso, el archivo existirá, pero estará vacío. No se utilizará. Debe colocarse en el [ClassPath] de la aplicación. Aquí se colocará en [WEB-INF/classes]. En Eclipse, se obtiene este resultado al colocarlo en [WEB-INF/src]:

Image

3.7.4. Las vistas JSP

Las vistas JSP que se utilizarán serán las que usa Struts. No se modifica ninguna:

Image

Se crea una única vista nueva: redirpanier.jsp. Su único propósito es redirigir al cliente a la acción [/panier.do]. Su código es el siguiente:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/panier.do"/>

Se invita al lector a volver a leer la definición de las diferentes vistas en la versión de Struts.

3.7.5. El procesamiento de las acciones

Las clases necesarias para el procesamiento de las diferentes acciones se han reunido en el paquete [istia.st.articles.web.spring]:

Image

Recordemos el funcionamiento de la aplicación Spring con un ejemplo:

  • el usuario solicita la URL [http://localhost:8080/springwebarticles/main.do]

Image

¿Qué sucedió?

  • Se consultó el archivo [web.xml] de la aplicación [springwebarticles]:
<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
     <!-- el cargador del contexto Spring de la aplicación -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- el servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- la asignación de URL -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- el documento de entrada -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  • Si esta fue la primera solicitud a la aplicación, se activaron varias cosas:
    • se cargó el listener [ org.springframework.web.context.ContextLoaderListener]
    • y procesó el archivo de configuración [applicationContext.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la clase de acceso a datos -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la clase de negocio -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- la configuración de la aplicación web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>
  • se crearon los beans anteriores en el contexto de la aplicación
  • luego se procesó el archivo [springwebarticles-servlet.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- los gestores de acciones = controladores -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

     <!-- la asignación de la aplicación-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

     <!-- gestor de vistas -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

     <!-- el archivo de mensajes -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>
  • Los beans [Controller] definidos por este archivo también se crearon
  • Ahora todo está listo para procesar la solicitud del cliente. Esta era: [http://localhost:8080/springwebarticles]. En este caso, no se solicita un URL del contexto, sino el contexto mismo. Por lo tanto, se utiliza la sección [welcome-file] del archivo [web.xml].
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
  • La vista [index.jsp] es la siguiente:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>
  • Por lo tanto, se le pide al cliente que se redirija a la URL [http://localhost:8080/springwebarticles/main.do]. Y así lo hace.
  • Por lo tanto, el controlador Spring recibe una nueva solicitud. Utiliza su archivo [springwebarticles-servlet.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- los administradores de acciones = controladores -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

     <!-- la asignación de la aplicación-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

</beans>
  • Este archivo le indica que la acción [/main.do] debe ser procesada por el bean [ListController].
  • La solicitud del cliente se transmite al método [handleRequest] del bean [ListController]. Este realiza su trabajo y devuelve al controlador la clave de la vista que se debe mostrar. Aquí, si todo sale bien, esa clave será [liste].
  • El controlador de Spring utiliza el bean [viewResolver] del archivo de configuración [springwebarticles-servlet.xml] para determinar la vista asociada a esta clave. En este caso, será la vista [/vues/liste.jsp]
  • La vista [/vues/liste.jsp] se envía al cliente

3.7.6. Inicialización de la aplicación Spring

Ya mencionamos que, al iniciar la aplicación, se instancian los beans del archivo [applicationContext.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- la clase de acceso a datos -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- la clase de negocio -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- la configuración de la aplicación web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Conocemos los beans [articlesDao, articlesDomain], pero no el bean [config]. Este último está definido por la siguiente clase Java:

package istia.st.articles.web.spring;

import java.util.Hashtable;
import istia.st.articles.domain.IArticlesDomain;

/**
 * @author ST - ISTIA
 */

public class Config {

     // campos privados
    private IArticlesDomain articlesDomain = null;
    private final String ACTION_LISTE = "liste.do";
    private final String ACTION_PANIER = "panier.do";
    private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
    private final String lienActionListe = "Liste des articles";
    private final String lienActionPanier = "Voir le panier";
    private final String lienActionValidationPanier = "Valider le panier";
    private Hashtable hActionListe = new Hashtable(2);
    private Hashtable hActionPanier = new Hashtable(2);
    private Hashtable hActionValidationPanier = new Hashtable(2);

     // métodos getter y setter
    public IArticlesDomain getArticlesDomain() {
        return articlesDomain;
    }

    public void setArticlesDomain(IArticlesDomain articlesDomain) {
        this.articlesDomain = articlesDomain;
    }

    public Hashtable getHActionListe() {
        return hActionListe;
    }

    public Hashtable getHActionPanier() {
        return hActionPanier;
    }

    public Hashtable getHActionValidationPanier() {
        return hActionValidationPanier;
    }

     // inicialización de la aplicación web
    public void init() {
         // se guardan ciertas URL de la aplicación
        hActionListe.put("href", ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // ya está listo
        return;
    }
}

Esta clase realiza lo mismo que el método [init] de un servlet de aplicación web. Inicializa la aplicación. En este caso, esto ocurre de la siguiente manera:

  • porque el bean [config] se define de la siguiente manera en [applicationContext.xml]:
     <!-- la configuración de la aplicación web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>

al crearse, se inicializó su campo privado [articlesDomain]

  • luego, debido al atributo [init-method="init"] del bean anterior, se ejecuta el método [init] de la clase asociada al bean. Aquí, inicializa los tres diccionarios [hActionListe, hActionPanier, hActionValidationPanier] utilizados para generar los tres enlaces posibles del menú que se le ofrece al usuario.
  • Se crean accesores públicos para que estos campos privados sean accesibles a las instancias de tipo [Controller] que procesarán las acciones.

3.7.7. Las acciones [Controller] de la aplicación Spring

3.7.7.1. Introducción

Cada acción de Spring será objeto de una clase del tipo [Controller]. En la versión de Struts, cada acción era objeto de una clase del tipo [Action]. La creación de la clase [Controller] consiste, en la mayoría de los casos, en:

  • copiar y pegar la clase [Action] que se había utilizado en la versión de Struts
  • en adaptar el código a las convenciones de Spring

3.7.7.2. main.do, liste.do

Estas dos acciones son idénticas y se definen en [springwebarticles-servlet.xml] mediante:

     <!-- la asignación de la aplicación-->
    <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>

Están asociadas a la clase [ istia.st.articles.web.spring.ListController], que detallaremos a continuación. Cuando se ejecuta una de estas acciones en un navegador, se obtiene el siguiente resultado:

Image

El código de la clase [istia.st.articles.web.spring.ListController] es el siguiente:

package istia.st.articles.web.spring;

import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class ListController implements Controller {

     // la configuración de la aplicación web
    Config config;

    public void setConfig(Config config) {
        this.config = config;
    }

     // procesamiento de la solicitud
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

         // se solicita la lista de artículos
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // se registra el error
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // se muestra la página de errores
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // enviar la vista de errores
            return new ModelAndView("erreurs");
        }
         // se muestra la lista de artículos
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // enviar la vista
        return new ModelAndView("liste");
    }

}

Comentarios:

  • la clase tiene un campo privado [config]. Este campo fue inicializado por Spring cuando se instanció el bean [ListController]:
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Como se puede ver arriba, el campo [config] de [ListController] se inicializa con el bean [config]. ¿De qué se trata? Se trata del bean [config] definido en [applicationContext.xml] y c.a.d, una instancia de [istia.st.articles.web.spring.Config] descrita anteriormente.

  • Escribir el código de una clase [Controller] consiste esencialmente en escribir el código de su método [handleRequest]
  • Se solicita la lista de artículos al modelo. Se puede acceder a este a través de [config.getArticlesDomain()]. Si se produce una excepción, se envía la vista [ERREURS]. El resultado devuelto por [handleRequest] debe ser de tipo [ModelAndView]. Esta clase se puede instanciar de diferentes maneras. Aquí, y así será siempre, se crea una instancia de [ModelAndView] pasándole la clave de la vista que se va a mostrar. Recordemos que, según la configuración del bean [viewResolver], solicitar la visualización de la vista con clave XX dará como resultado el envío de la vista [/vues/XX.jsp].
         // se solicita la lista de artículos
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // se registra el error
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // se muestra la página de errores
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // enviar la vista de errores
            return new ModelAndView("erreurs");
        }
  • si no hay errores, se envía la vista [LISTE]:
1
2
3
4
5
6
7
         // se muestra la lista de artículos
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // enviar la vista
        return new ModelAndView("liste");

3.7.7.3. infos.do

Esta acción sirve para proporcionar información sobre uno de los artículos que aparecen en la vista [LISTE]:

Esta acción se define en [springwebarticles-servlet.xml] mediante:

     <!-- la estructura de la aplicación-->
    <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>

El código de la clase [InfosController] es el siguiente:

package istia.st.articles.web.spring;

import istia.st.articles.dao.Article;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class InfosController implements Controller {

     // la configuración de la aplicación web
    Config config;

    public void setConfig(Config config) {
        this.config = config;
    }

     // procesamiento de la solicitud
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

     // la lista de errores
    ArrayList erreurs = new ArrayList();
     // se recupera el ID solicitado
    String strId = request.getParameter("id");
     // ¿algo?
    if (strId == null) {
       // no es normal
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // se transforma strId en un entero
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // no es normal
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // se solicita el artículo de la clave id
    Article article = null;
    try {
      article = config.getArticlesDomain().getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // anormal
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
    if (article == null) {
       // no es normal
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // se agrega el artículo a la sesión
    request.getSession().setAttribute("article", article);
     // se muestra la página de información
    request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
        return new ModelAndView("infos");
    }
}

Comentarios:

  • El método [handleRequest] recupera el parámetro [id], que normalmente debe encontrarse en la URL. De hecho, esta debe tener el formato [/infos.do?id=X]. Se realizan diversas pruebas para verificar la presencia y la validez del parámetro [id]. En caso de problema, se envía la vista [ERREURS].
  • Si [id] es válido, se solicita el artículo correspondiente a la capa [domain]. Si esta genera una excepción o si no se encuentra el artículo, nuevamente se envía la vista [ERREURS].
  • Si todo sale bien, el artículo obtenido se guarda en la sesión. Este es un punto discutible. Aquí se parte del principio de que el cliente tal vez compre este artículo. Si lo hace, se buscará en la sesión, en lugar de solicitarlo nuevamente a la capa [domain].
  • Finalmente, se muestra la vista [INFOS].

3.7.7.4. achat.do

Esta acción sirve para comprar el artículo mostrado por la vista anterior [INFOS]:

Image

Si observamos el código HTML de esta vista, vemos que la etiqueta <form> está definida de la siguiente manera:

        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>

Por lo tanto, vemos que el formulario se envía al controlador con la acción [achat.do].

Esta acción está configurada de la siguiente manera en [springwebarticles-servlet.xml]:

     <!-- la asignación de la aplicación-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/achat.do">AchatController</prop>
...
            </props>
        </property>
    </bean>

    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

El código de la clase [AchatController] es el siguiente:

package istia.st.articles.web.spring;

import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class AchatController implements Controller {

   // la configuración de la aplicación web
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // procesamiento de la solicitud
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // la lista de errores en esta acción
    ArrayList erreurs = new ArrayList();
     // se recupera la cantidad comprada
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // cantidad incorrecta
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
     // se recupera la sesión del cliente
    HttpSession session = request.getSession();
     // se recupera el artículo agregado a la sesión
    Article article = (Article) session.getAttribute("article");
     // ¿Sesión vencida?
    if (article == null) {
       // se muestra la página de errores
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // se crea la nueva compra
    Achat achat = new Achat(article, qté);
     // se agrega la compra al carrito del cliente
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // se regresa a la lista de artículos
    return new ModelAndView("index");
  }
}

Comentarios:

  • Recordemos el formato del formulario enviado al controlador:
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>
  • Hay dos parámetros en la consulta: [id]: número del artículo comprado; [qte]: cantidad comprada.
  • Se comprueba la presencia y la validez del parámetro [qte]. Si este parámetro se considera incorrecto, se devuelve al usuario la vista [INFOS] junto con un mensaje de error:
     // la lista de errores de esta acción
    ArrayList erreurs = new ArrayList();
     // se recupera la cantidad comprada
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // cantidad incorrecta
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
  • El artículo comprado se recupera de la sesión. Es posible que esta haya caducado. En ese caso, se envía la vista [ERREURS]:
     // se recupera la sesión del cliente
    HttpSession session = request.getSession();
     // se recupera el artículo agregado a la sesión
    Article article = (Article) session.getAttribute("article");
     // ¿Sesión vencida?
    if (article == null) {
       // se muestra la página de errores
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • Si la sesión no ha caducado, el artículo se agrega al carrito, el cual también se recupera de la sesión:
1
2
3
4
5
6
7
8
9
     // se crea la nueva compra
    Achat achat = new Achat(article, qté);
     // se agrega la compra al carrito del cliente
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • por último, se envía la vista [LISTE]:
     // se regresa a la lista de artículos
    return new ModelAndView("index");
  • En el ejemplo anterior, se envía la vista [/vues/index.jsp]. Sabemos que esta le pide al navegador del cliente que se redirija a la URL [/main.do]. Es esta redirección la que mostrará la lista de artículos.

3.7.7.5. panier.do

Esta acción sirve para mostrar todas las compras del cliente. Está disponible a través del menú:

Image

El código HTML asociado al enlace anterior es el siguiente:

<a href="panier.do">Voir le panier</a>

La página a la que lleva este enlace es la siguiente:

Image

Esta acción está configurada de la siguiente manera en [springwebarticles-servlet.xml]:

     <!-- la asignación de la aplicación-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/panier.do">VoirPanierController</prop>
...
            </props>
        </property>
    </bean>
...
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

El código de la clase [VoirPanierController] es el siguiente:

package istia.st.articles.web.spring;

import istia.st.articles.domain.Panier;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class VoirPanierController implements Controller {

   // la configuración de la aplicación web
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // procesamiento de la solicitud
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // se muestra el carrito
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // carrito vacío
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("paniervide");
    } else {
       // hay algo en el carrito
      request.setAttribute("actions", new Hashtable[] {
          config.getHActionListe(), config.getHActionValidationPanier() });
      return new ModelAndView("panier");
    }
  }
}

Comentarios:

  • La canasta se toma de la sesión en la que se encuentra normalmente. Es posible que la sesión haya caducado y, por lo tanto, no haya canasta. No se trata este caso como un error, sino que simplemente se considera que la canasta está vacía.
  • Si el carrito está vacío, se muestra la vista [PANIERVIDE]
  • de lo contrario, se muestra la vista [PANIER]

3.7.7.6. retirerachat.do

Esta acción sirve para eliminar un artículo de la canasta:

Image

Si observamos el código HTML del enlace anterior, encontramos lo siguiente:

<a href="retirerachat.do?id=3">Retirer</a>

Por lo tanto, la acción [retirerachat.do] recibe como parámetro el ID del artículo que se va a eliminar del carrito. Esta acción está configurada de la siguiente manera en [springwebarticles-servlet.xml]:

     <!-- la asignación de la aplicación-->
    <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>

El código de la clase [RetirerAchatController] es el siguiente:

package istia.st.articles.web.spring;

import istia.st.articles.domain.Panier;

import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class RetirerAchatController implements Controller {

   // la configuración de la aplicación web
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // procesamiento de la solicitud
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // la lista de errores en esta acción
    ArrayList erreurs = new ArrayList();
     // se recupera el carrito
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // Sesión vencida
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // se recupera el ID del artículo que se va a eliminar
    String strId = request.getParameter("id");
     // ¿algo?
    if (strId == null) {
       // no es normal
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // Convertimos strId a un entero
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // no es normal
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // se elimina la compra
    panier.enlever(id);
     // se vuelve a mostrar el carrito
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");
  }
}

Comentarios:

  • el código verifica la presencia y la validez del parámetro [id]. Si resulta incorrecto, se envía la vista [ERREURS].
  • De lo contrario, la compra se elimina del carrito:
     // se elimina la compra
    panier.enlever(id);
  • luego se vuelve a mostrar el carrito:
     // se vuelve a mostrar el carrito
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");

Recordemos el código de la vista [/vues/redirpanier.jsp]:

<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/panier.do"/>

Se observa que el cliente será redirigido a la acción [/panier.do]. Esta ya se ha descrito anteriormente. Mostrará la vista [PANIER] o [PANIERVIDE], según el estado del carrito.

3.7.7.7. validerpanier.do

Esta acción sirve para validar las compras realizadas por el cliente. En concreto, esto se traduce en una única acción: las existencias de los artículos comprados se reducen en las cantidades adquiridas, en la base de datos. Esta acción proviene del siguiente menú:

Image

El código HTML del enlace [Valider le panier] es el siguiente:

<a href="validerpanier.do">Valider le panier</a>

Cuando se activa este enlace, se reducen los inventarios y se vuelve a mostrar la lista de artículos.

Esta acción está configurada de la siguiente manera en [springwebarticles-servlet.xml]:

     <!-- la configuración de la aplicación-->
    <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>

El código de la clase [ValiderPanierController] es el siguiente:

package istia.st.articles.web.spring;

import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;

import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class ValiderPanierController implements Controller {

   // la configuración de la aplicación web
  Config config;

  public void setConfig(Config config) {
    this.config = config;
  }

   // procesamiento de la solicitud
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // la lista de errores en esta acción
    ArrayList erreurs = new ArrayList();
     // el comprador ha confirmado su carrito
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sesión vencida
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // Se valida el carrito
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // no es normal
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // se recuperan los posibles errores
    erreurs = config.getArticlesDomain().getErreurs();
    if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe(), config.getHActionPanier() });
      return new ModelAndView("erreurs");
    }
     // Todo parece estar bien OK - Se muestra la lista de artículos
    return new ModelAndView("index");
  }
}

Comentarios:

  • se recupera el carrito de la sesión. Si esta ha caducado, se envía la vista [ERREURS]:
     // la lista de errores de esta acción
    ArrayList erreurs = new ArrayList();
     // el comprador ha confirmado su carrito
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sesión vencida
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • Se validan las compras del carrito. Pueden producirse errores si las existencias son insuficientes para satisfacer las compras. En ese caso, se envía la vista [ERREURS]:
     // se valida el carrito
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // no es normal
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // se recuperan los posibles errores
    erreurs = config.getArticlesDomain().getErreurs();
    if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe(), config.getHActionPanier() });
      return new ModelAndView("erreurs");
    }
  • Si todo salió bien, se vuelve a mostrar la lista de artículos:
     // Todo parece estar bien OK - Se muestra la lista de artículos
    return new ModelAndView("index");

Sabemos que la vista [/vues/index.jsp] redirige al cliente a la acción [/main.do]. Esta acción mostrará la vista [LISTE].