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

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

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

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

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

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:
- 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.
- 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.
- 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
- 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.
- 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:
- las clases de negocio
- las clases de acceso a datos
- 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);
|
| clave primaria que identifica un artículo de manera única |
| nombre del artículo |
| su precio |
| su stock actual |
| 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:
- un constructor que permite establecer los 5 datos de un artículo
- 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.
- una verificación de los datos ingresados en el artículo. En caso de datos erróneos, se lanza una excepción.
- 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:
| devuelve todos los artículos de la tabla ARTICLES en una lista de objetos [Article] |
| vacía la tabla ARTICLES |
| devuelve el objeto [Article] identificado por su clave primaria |
| permite agregar un artículo a la tabla ARTICLES |
| permite modificar un artículo de la tabla [ARTICLES] |
| permite eliminar un artículo de la tabla [ARTICLES] |
| 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#>=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();
}
|
| 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 |
| 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:
| el objeto de acceso a datos proporcionado por la capa de acceso a datos |
| 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:
| el artículo comprado |
| la cantidad comprada |
| muestra el monto de la compra |
| 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:
| 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 |
| muestra el total de las compras |
| devuelve la cadena de identificación del carrito |
| 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:

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:
| // 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:
| // 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:
| // 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:
| las clases de negocio, las clases de acceso a datos y la base de datos |
| las páginas JSP |
| 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.jsp | Las vistas se encuentran en la carpeta [vues] de la aplicación  |
| infos.jsp |
| panier.jsp |
| paniervide.jsp |
| 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:
| significado | Acción del controlador | respuestas posibles |
| El cliente quiere la lista de artículos | - solicita la lista de artículos a la capa de negocio | - [LISTE] - [ERREURS] |
| 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] |
| 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 |
| el cliente quiere eliminar un compra de su carrito | - recupera el carrito de la sesión y lo modifica | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| El cliente quiere ver su carrito | - recupera el carrito de la sesión | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| 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:
- el cambio de las URL de las diferentes vistas
- el cambio de las clases que implementan las interfaces [IArticlesDao] y [IArticlesDomain]
- 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:
- 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.
- 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#>=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
| |
 |  |
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:
| objeto Hashtable[] — la tabla de opciones del menú |
| ArrayList de objetos del tipo [Article] |
| 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:

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:
| objeto Hashtable[] — la tabla de opciones del menú |
| objeto de tipo [Article]: artículo que se mostrará |
| objeto String: mensaje que se mostrará en caso de error en la cantidad |
| 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:

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:

Se muestra tras una solicitud /main?action=carrito o /main?action=retirarcompra&id=ID. Los elementos de la solicitud del controlador son los siguientes:
| objeto Hashtable[] - la tabla de opciones del menú |
| 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:

Se muestra tras una solicitud /main?action=carrito o /main?action=retirarcompra&id=ID. Los elementos de la solicitud del controlador son los siguientes:
| objeto Hashtable[] - la tabla de opciones del menú |
Código:
| <%@ 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:

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:
| objeto Hashtable[] — la tabla de opciones del menú |
| 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:
| <%@ 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:
| /**
* @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 |
| GET /main?action=lista | - solicitar la lista de artículos a la clase de negocio - mostrarla | [LISTE] o [ERREURS] |
| GET /main?action=infos&id=ID | - solicitar el artículo con id=ID a la clase de negocio - mostrarlo | [INFOS] o [ERREURS] |
| 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] |
| 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] |
| GET /main?action=carrito | - Mostrar la canasta de la sesión del cliente | [PANIER] o [PANIERVIDE] |
| 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
| 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:

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]:
| // 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:
| las clases de negocio, las clases de acceso a datos y la base de datos |
| las páginas JSP |
| 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:
| las clases de negocio, las clases de acceso a datos y la base de datos |
| las páginas JSP |
| 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:

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

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:

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

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:
| para mostrar la lista de artículos |
| para mostrar la lista de artículos |
| para ver información sobre un artículo específico |
| para comprar un artículo específico |
| para ver el carrito de compras |
| para eliminar un artículo de la canasta |
| 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]:

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:
| objeto Hashtable[] — la tabla de opciones del menú |
| ArrayList de objetos del tipo [Article] |
| 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:

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:
| objeto Hashtable[] — la tabla de opciones del menú |
| objeto de tipo [Article]: artículo a mostrar |
| objeto String: mensaje que se mostrará en caso de error en la cantidad |
| 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:

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:

Se muestra tras una solicitud /panier.do o /retirerachat.do?id=ID. Los elementos de la solicitud del controlador son los siguientes:
| objeto Hashtable[] — la tabla de opciones del menú |
| 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:

Se muestra tras una solicitud /panier.do o /retirerachat.do?id=ID. Los elementos de la solicitud del controlador son los siguientes:
| objeto Hashtable[] — la tabla de opciones del menú |
Código:
| <%@ 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:

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:
| objeto Hashtable[] — la tabla de opciones del menú |
| 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:
| <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:

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:
| // ¿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]:
| // ¿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]:
| // 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:
| <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:
| <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:
| las clases de negocio, las clases de acceso a datos y la base de datos |
| las páginas JSP |
| el servlet de procesamiento de solicitudes de los clientes, los objetos [Action] |
Con Spring, adoptamos una arquitectura idéntica:
| las clases de negocio, las clases de acceso a datos y la base de datos |
| las páginas JSP |
| 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:

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

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:

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

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:
| para mostrar la lista de artículos |
| para mostrar la lista de artículos |
| para ver información sobre un artículo específico |
| para comprar un artículo específico |
| para ver el carrito de compras |
| para eliminar un artículo de la canasta |
| 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]:

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

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

Recordemos el funcionamiento de la aplicación Spring con un ejemplo:
- el usuario solicita la URL [http://localhost:8080/springwebarticles/main.do]

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

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

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:
| // 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ú:

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:

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:

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

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