3. Artigo 2.º - Exemplos de arquiteturas web de três camadas
Objetivos deste artigo:
- Arquiteturas de três camadas
- arquitetura web MVC básica
- Arquitetura Struts MVC
- Arquitetura Spring MVC
Ferramentas 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. Na verdade, qualquer fonte JDBC serve.
- IBExpert, Edição Pessoal: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (março de 2005). O IBExpert permite-lhe administrar graficamente o SGBD Firebird.
- Tomcat: http://jakarta.apache.org/tomcat/
- Plugin Tomcat para o Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. Consulte também o documento https://tahe.developpez.com/java/eclipse/
A compreensão deste documento requer vários pré-requisitos. Alguns deles podem ser encontrados em documentos que escrevi. Nesses casos, faço referência aos mesmos. Obviamente, isto é apenas uma sugestão, e os leitores são livres de utilizar os seus recursos preferidos.
- Linguagem Java: [https://tahe.developpez.com/java/cours]
- Programação Web em Java: [https://tahe.developpez.com/java/web/]
- Programação Web com Java, Eclipse e Tomcat: [https://tahe.developpez.com/java/eclipse/]
- Programação Web com Struts: [https://tahe.developpez.com/java/struts/]
- Utilização do aspeto IoC do Spring: [https://tahe.developpez.com/java/springioc]
- Biblioteca de tags JSTL: [https://tahe.developpez.com/java/eclipse/] (em parte)
- Documentação do Ibatis SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
- Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (março de 2005).
As ideias contidas neste documento têm origem num livro que li no verão de 2004, uma obra magnífica de Rod Johnson: J2EE Development without EJB, publicado pela Wrox.
3.1. A aplicação webarticles
Aqui, gostaríamos de apresentar alguns componentes de uma aplicação web de comércio eletrónico. Esta aplicação permitirá aos clientes web
- visualizar uma lista de itens de uma base de dados
- adicionar alguns deles a um carrinho de compras eletrónico
- confirmar o carrinho. Esta confirmação irá simplesmente atualizar os níveis de inventário dos artigos adquiridos na base de dados.
As diferentes visualizações apresentadas ao utilizador serão as seguintes:
- a vista [LISTA], que exibe uma lista de artigos à venda

- a vista [INFO], que fornece informações adicionais sobre um produto:

- a vista [CART], que apresenta o conteúdo do carrinho do cliente

- a visualização [CARRINHO VAZIO], caso o carrinho do cliente esteja vazio

- a vista [ERROS], que reporta quaisquer erros da aplicação

3.2. Arquitetura geral da aplicação
Queremos criar uma aplicação com a seguinte arquitetura de três camadas:
- As três camadas são tornadas independentes através da utilização de interfaces Java
- A integração das diferentes camadas é gerida pelo Spring
- Cada camada está contida em pacotes separados: web (camada de interface do utilizador), domínio (camada de negócios) e DAO (camada de acesso aos dados).
Partiremos do princípio de que as camadas [domain] e [DAO] já estão implementadas. Iremos concentrar-nos apenas na camada [web], que propomos construir de várias formas:
- utilizando a tecnologia clássica de controladores de servlet — páginas JSP
- utilizando a tecnologia Struts MVC
- utilizando a tecnologia Spring MVC
Em todos os casos, a aplicação seguirá uma arquitetura MVC (Model-View-Controller). Se nos referirmos ao diagrama em camadas acima, a arquitetura MVC encaixa-se nele da seguinte forma:
O processamento de um pedido do cliente segue estes passos:
- O cliente envia uma solicitação ao controlador. Este controlador é um servlet que lida com todas as solicitações do cliente. É o ponto de entrada da aplicação. É o C em MVC.
- O controlador processa esta solicitação. Para tal, pode necessitar da assistência da camada de negócios, conhecida como o «M» na estrutura MVC.
- O controlador recebe uma resposta da camada de negócios. A solicitação do cliente foi processada. Isso pode desencadear várias respostas possíveis. Um exemplo clássico é
- uma página de erro, caso a solicitação não tenha sido processada corretamente
- uma página de confirmação, caso contrário
- O controlador escolhe a resposta (= vista) a enviar ao cliente. Trata-se, na maioria das vezes, de uma página que contém elementos dinâmicos. O controlador fornece estes à vista.
- A vista é enviada ao cliente. Esta é a V em MVC.
3.3. O Modelo
Aqui, analisamos o M em MVC. O modelo é composto pelos seguintes elementos:
- classes de negócio
- classes de acesso a dados
- a base de dados
3.3.1. A base de dados
A base de dados contém apenas uma tabela chamada ARTICLES. Esta tabela foi gerada utilizando os seguintes 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
);
/* constraints */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
|
| chave primária que identifica um item de forma única |
| nome do item |
| o seu preço |
| stock atual |
| o nível de stock abaixo do qual deve ser efetuada uma nova encomenda |
Nos testes a seguir, foi utilizada uma base de dados [Firebird]. O [Firebird] é um SGBD de «código aberto». O controlador JDBC [firebirdsql-full.jar] está localizado na pasta [WEB-INF/lib] da aplicação web.
3.3.2. Os pacotes de modelos
O modelo M é fornecido aqui na forma de três arquivos:
- istia.st.articles.dao: contém as classes de acesso aos dados para a camada [DAO]
- istia.st.articles.exception: contém uma classe de exceção para esta gestão de artigos
- istia.st.articles.domain: contém as classes de negócio da camada [domain]
arquivo | conteúdo | função |
istia.st.articles.dao | - contém o pacote [istia.st.articles.dao], que por sua vez contém os seguintes elementos: - [IArticlesDao]: a interface para aceder à camada Dao. Esta é a única interface visível para a camada [domain]. Não vê outras. - [Article]: classe que define um artigo - [ArticlesDaoSqlMap]: classe de implementação para a interface [IArticlesDao] utilizando a ferramenta SqlMap | camada de acesso a dados – está localizada inteiramente dentro da camada [dao] da arquitetura de três camadas da aplicação web |
istia.st.articles.domain | - contém o pacote [istia.st.articles.domain], que por sua vez contém os seguintes elementos: - [IArticlesDomain]: a interface para aceder à camada [domain]. Esta é a única interface visível para a camada web. Não vê outras. - [ArticlePurchases]: uma classe que implementa [IArticlesDomain] - [Purchase]: uma classe que representa a compra de um cliente - [ShoppingCart]: uma classe que representa o total de compras de um cliente | representa o modelo de compra web - está localizada inteiramente na camada [domain] da arquitetura de 3 camadas da aplicação web |
istia.st.articles.exception | - contém o pacote [istia.st.articles.exception], que por sua vez contém os seguintes elementos: - [UncheckedAccessArticlesException]: classe que define uma exceção [RuntimeException]. Este tipo de exceção é lançado pela camada [dao] assim que ocorre um problema de acesso aos dados. | |
3.3.3. O pacote [istia.st.articles.dao]
A classe que define um artigo é a seguinte:
| package istia.st.articles.dao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
/**
* @author ST - ISTIA
*
*/
public class Article {
private int id;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
/**
* constructeur par défaut
*/
public Article() {
}
public Article(int id, String nom, double prix, int stockActuel,
int stockMinimum) {
// init instance attributes
setId(id);
setNom(nom);
setPrix(prix);
setStockActuel(stockActuel);
setStockMinimum(stockMinimum);
}
// getters - setters
public int getId() {
return id;
}
public void setId(int id) {
// valid id?
if (id < 0)
throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
// valid name?
if(nom==null || nom.trim().equals("")){
throw new UncheckedAccessArticlesException("Le nom est [null] ou vide");
}
this.nom = nom;
}
public double getPrix() {
return prix;
}
public void setPrix(double prix) {
// valid price?
if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
this.prix = prix;
}
public int getStockActuel() {
return stockActuel;
}
public void setStockActuel(int stockActuel) {
// valid stock?
if (stockActuel < 0)
throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
this.stockActuel = stockActuel;
}
public int getStockMinimum() {
return stockMinimum;
}
public void setStockMinimum(int stockMinimum) {
// valid stock?
if (stockMinimum < 0)
throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
this.stockMinimum = stockMinimum;
}
public String toString() {
return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
+ stockMinimum + "]";
}
}
|
Esta classe fornece:
- um construtor para definir as 5 informações de um item
- acessores, frequentemente chamados de getters/setters, utilizados para ler e escrever as 5 informações. Os nomes destes métodos seguem o padrão JavaBean. A utilização de objetos JavaBean na camada DAO para interagir com os dados do SGBD é uma prática padrão.
- validação dos dados introduzidos para o item. Se os dados forem inválidos, é lançada uma exceção.
- Um método toString que devolve o valor de um item como uma cadeia de caracteres. Isto é frequentemente útil para depurar uma aplicação.
A interface [IArticlesDao] é definida da seguinte forma:
| 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);
}
|
As funções dos vários métodos na interface são as seguintes:
| retorna todos os itens da tabela ARTICLES numa lista de objetos [Article] |
| limpa a tabela ARTICLES |
| retorna o objeto [Article] identificado pela sua chave primária |
| permite adicionar um artigo à tabela ARTICLES |
| permite modificar um item na tabela [ARTICLES] |
| permite eliminar um item da tabela [ARTICLES] |
| permite-lhe modificar o stock de um artigo na tabela [ARTICLES] |
A interface fornece aos programas clientes vários métodos definidos exclusivamente pelas suas assinaturas. Não se preocupa com a forma como esses métodos serão efetivamente implementados. Isto confere flexibilidade a uma aplicação. O programa cliente efetua chamadas a uma interface, em vez de a uma implementação específica dessa interface.
A escolha de uma implementação específica será feita através de um ficheiro de configuração do Spring. Aqui, propomos implementar a interface IArticlesDao utilizando um produto de código aberto chamado SqlMap. Isto permitir-nos-á remover todas as instruções SQL do código Java.
A classe de implementação [ArticlesDaoSqlMap] é definida da seguinte forma:
| package istia.st.articles.dao;
// Imports
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;
public class ArticlesDaoSqlMap implements IArticlesDao {
// Fields
private SqlMapClient sqlMap;
// Constructors
public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }
// Methods
public SqlMapClient getSqlMap() {}
public void setSqlMap(SqlMapClient sqlMap) { }
public synchronized List getAllArticles() {}
public synchronized int ajouteArticle(Article unArticle) {}
public synchronized int supprimeArticle(int idArticle) {}
public synchronized int modifieArticle(Article unArticle) {}
public synchronized Article getArticleById(int idArticle) {}
public synchronized void clearAllArticles() { }
public synchronized int changerStockArticle(int idArticle, int mouvement) {}
}
|
Todos os métodos de acesso aos dados foram sincronizados para evitar problemas de acesso simultâneo à fonte de dados. Em qualquer momento, apenas um segmento de execução tem acesso a um determinado método.
A classe [ArticlesDaoSqlMap] utiliza a ferramenta [Ibatis SqlMap]. A vantagem desta ferramenta é que permite separar o código SQL para acesso aos dados do código Java. Este é então colocado num ficheiro de configuração. Teremos oportunidade de voltar a este assunto mais tarde. Para ser instanciada, a classe [ArticlesDaoSqlMap] requer um ficheiro de configuração cujo nome é passado como parâmetro ao construtor da classe. Este ficheiro de configuração define as informações necessárias para:
- aceder ao SGBD que contém os artigos
- gerir um conjunto de ligações
- gerir transações
No nosso exemplo, será denominado [sqlmap-config-firebird.xml] e definirá o acesso a uma base de dados 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>
|
O ficheiro de configuração [articles.xml] referido acima define como construir uma instância da classe [istia.st.articles.dao.Article] a partir de uma linha na tabela [ARTICLES] do SGBD. Define também as consultas SQL que permitirão à camada [dao] recuperar dados da fonte de dados 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">
<!-- an alias to the istia.st.articles.dao.Article class -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- mapping ORM : row table ARTICLES - instance class Article -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- query SQL to obtain all items -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- query SQL to delete all items -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- the SQL query to insert an article -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>
<!-- the SQL query to delete a given item -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- query SQL to modify a given item -->
<statement id="modifyArticle">
update ARTICLES set nom=#nom#,
prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
id=#id#
</statement>
<!-- query SQL to obtain a given item -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- query SQL to modify the stock of a given item -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#mouvement#
where id=#id# and stockActuel+#mouvement#>=0
</statement>
</sqlMap>
|
O código do pacote [dao] pode ser encontrado no apêndice.
3.3.4. O pacote [istia.st.articles.domain]
A interface [IArticlesDomain] desacopla a camada [business] da camada [web]. Esta última acede à camada [business/domain] através desta interface sem se preocupar com a classe que a implementa efetivamente. A interface define as seguintes ações para aceder à camada de negócios:
| package istia.st.articles.domain;
// Imports
import java.util.ArrayList;
import java.util.List;
public abstract interface IArticlesDomain {
// Methods
void acheter(Panier panier);
List getAllArticles();
Article getArticleById(int idArticle);
ArrayList getErreurs();
}
|
| retorna a lista de objetos [Article] a serem exibidos ao cliente |
Artigo getArticleById(int idArtigo)
| retorna o objeto [Article] identificado por [idArticle] |
void comprar(Carrinho carrinho)
| processa o carrinho do cliente, diminuindo o stock dos itens comprados pela quantidade adquirida — pode falhar se o stock for insuficiente |
| retorna a lista de erros que ocorreram — vazia se não houver erros |
Aqui, a interface [IArticlesDomain] será implementada pela seguinte classe [PurchaseItems]:
| package istia.st.articles.domain;
// Imports
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.List;
public class AchatsArticles implements IArticlesDomain {
// Fields
private IArticlesDao articlesDao;
private ArrayList erreurs;
// Manufacturers
public AchatsArticles(IArticlesDao articlesDao) { }
// Methods
public ArrayList getErreurs() {}
public List getAllArticles() {}
public Article getArticleById(int id) {}
public void acheter(Panier panier) { }
}
|
Esta classe implementa os quatro métodos da interface [IArticlesDomain]. Possui dois campos privados:
| o objeto de acesso a dados fornecido pela camada de acesso a dados |
| a lista de eventuais erros |
Para criar uma instância da classe, deve fornecer o objeto que permite o acesso aos dados do SGBD:
public PurchasesItems(IArticlesDao articlesDao)
| construtor |
A classe [Purchase] representa uma compra de um cliente:
| package istia.st.articles.domain;
public class Achat {
// Fields
private Article article;
private int qte;
// Manufacturers
public Achat(Article article, int qte) { }
// Methods
public double getTotal() {}
public Article getArticle() {}
public void setArticle(Article article) { }
public int getQte() {}
public void setQte() { }
public String toString() {}
}
|
A classe [Purchase] é um JavaBean com os seguintes campos e métodos:
| o artigo adquirido |
| a quantidade comprada |
| retorna o valor da compra |
| representação do objeto como string |
A classe [Cart] representa o total das compras do cliente:
| package istia.st.articles.domain;
// Imports
import java.util.ArrayList;
public class Panier {
// Fields
private ArrayList achats;
// Manufacturers
public Panier() { }
// Methods
public ArrayList getAchats() {}
public void ajouter(Achat unAchat) { }
public void enlever(int idAchat) { }
public double getTotal() {}
public String toString() { }
}
|
A classe [Cart] é um JavaBean com os seguintes campos e métodos:
| a lista de compras do cliente - uma lista de objetos do tipo [Purchase] |
void add(Purchase compra)
| adiciona uma compra à lista de compras |
| remove a compra correspondente ao ID do item idArticle |
| retorna o valor total das compras |
| retorna a representação em string do carrinho de compras |
| retorna a lista de compras |
O código do pacote [domain] pode ser encontrado no apêndice.
3.3.5. O pacote [istia.st.articles.exception]
Este pacote contém a classe que define a exceção lançada pela camada [dao] quando esta encontra um problema ao aceder à fonte de dados:
| 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. Teste do modelo
O modelo M foi testado no Eclipse com a seguinte configuração:

Comentários:
- Em [WEB-INF/lib] encontrará:
- os arquivos necessários para a ferramenta [ibatis SqlMap], responsável pelo acesso ao SGBD Firebird: ibatis-*.jar
- o ficheiro necessário para a ferramenta [Spring]: spring.jar
- o controlador JDBC para o SGBD [Firebird]: firebirdsql-full.jar
- os arquivos necessários para o registo: log4-*.jar, commons-logging.jar
- os três arquivos para o modelo testado: istia.st.articles.*.jar
- o arquivo necessário para a ferramenta de testes [junit]
- Em [WEB-INF/src] encontram-se os ficheiros de configuração que serão automaticamente copiados para [WEB-INF/classes] pelo Eclipse:
- os ficheiros de configuração para a ferramenta [sqlmap]: sqlmap-config-firebird.xml, articles.xml
- os ficheiros de configuração da ferramenta [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
- o ficheiro de configuração para a ferramenta [log4j]: log4j.properties
- No pacote [istia.st.articles.tests], encontrará as classes de teste do modelo
3.3.6.1. Testes para a camada [dao]
A classe de teste JUnit para a camada [dao] é a seguinte. A sua leitura ajuda a compreender como os métodos da interface [IArticlesDao] são utilizados:
| package istia.st.articles.tests.dao;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.dao.Article;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// test the ArticlesDaoSqlMap class
public class JunitModeleDaoArticles extends TestCase {
// an instance of the class under test
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// retrieves a data access instance
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
public void testGetAllArticles() {
// displays articles
listArticles();
}
public void testClearAllArticles() {
// empties item table
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
}
public void testAjouteArticle() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
//the poster
listArticles();
}
public void testSupprimeArticle() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// delete
articlesDao.supprimeArticle(4);
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(1, articles.size());
// displays the table
listArticles();
}
public void testModifieArticle() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
// modification
articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
// getById
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getPrix(), 44, 1e-6);
// displays the table
listArticles();
}
public void testGetArticleById() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
private void listArticles() {
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
// display read articles
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
public void testChangerStockArticle() throws InterruptedException {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// insertion
int nbArticles = articlesDao.ajouteArticle(new Article(3, "article3",
30, 101, 3));
assertEquals(nbArticles, 1);
nbArticles = articlesDao.ajouteArticle(new Article(4, "article4", 40,
40, 4));
assertEquals(nbArticles, 1);
// creation of 100 threads to update the stock of item 3
Thread[] taches = new Thread[100];
for (int i = 0; i < taches.length; i++) {
taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
taches[i].start();
}
// we wait for the end of threads
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// retrieve item 3 and check stock
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
assertEquals(1, unArticle.getStockActuel());
// modification stock article 4
boolean erreur = false;
int nbLignes = articlesDao.changerStockArticle(4, -100);
assertEquals(0, nbLignes);
// displays the table
listArticles();
}
}
|
Comentários:
- A classe de teste armazena, utilizando o seu método setUp, uma instância da classe em teste:
| // une instance de la classe testée
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// récupère une instance d'accès aux données
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
|
- O objeto a ser testado é fornecido pelo [Spring]. Acima, solicitamos o bean do Spring denominado [articlesDao]. Este bean está definido no ficheiro de configuração do 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>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
</beans>
|
Como mostrado acima, o bean [articlesDao] é uma instância da classe [istia.st.articles.dao.ArticlesDaoSqlMap]. Esta classe possui um construtor que recebe o nome do ficheiro de configuração da ferramenta [SqlMap] como parâmetro. Esse nome é fornecido aqui. É o [sqlmap-config-firebird.xml]. Este ficheiro já foi descrito. Ele fornece todas as informações necessárias para aceder aos dados do SGBD.
O método [testChangerStockArticle] cria 100 threads responsáveis por diminuir o stock de um determinado artigo. O objetivo aqui é testar o acesso simultâneo ao SGBD. Como o método [changerStockArticle] da classe [istia.st.articles.dao.ArticlesDaoSqlMap] foi sincronizado, este teste é bem-sucedido. Se removemos a sincronização, o teste já não é bem-sucedido. A classe responsável pela atualização do stock é a seguinte:
| package istia.st.articles.tests;
import istia.st.articles.dao.IArticlesDao;
public class ThreadMajStock extends Thread {
/**
* nom du thread
*/
private String name;
/**
* objet d'accès aux données
*/
private IArticlesDao articlesDao;
/**
*
* @param name
* le nom du thread afin de l'identifier
* @param articlesDao
* l'objet d'accès aux données du sgbd
*/
public ThreadMajStock(String name, IArticlesDao articlesDao) {
this.name = name;
this.articlesDao = articlesDao;
}
/**
* décrémente le stock de l'article 3 d'une unité fait un suivi écran des
* opérations
*/
public void run() {
// follow-up
System.out.println(name + " lancé");
// modification stock article 3
articlesDao.changerStockArticle(3, -1);
// follow-up
System.out.println(name + " terminé");
}
}
|
- A classe acima diminui o stock do item n.º 3 em 1
3.3.6.2. [domínio] testes de camada
A classe de teste JUnit para a camada [domain] é a seguinte:
| package istia.st.articles.tests.domain;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.Article;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// test the ArticlesDaoSqlMap class
public class JunitModeleDomainArticles extends TestCase {
// an instance of the domain access class
private IArticlesDomain articlesDomain;
// an instance of the data access class
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// retrieves a domain access instance
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// retrieves a data access instance
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
// retrieve a specific item
public void testGetArticleById() {
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// reads the ARTICLES table
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDomain.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
// screen display
private void listArticles() {
// reads the ARTICLES table
List articles = articlesDomain.getAllArticles();
// display read articles
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
// article purchases
public void testAchatPanier(){
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// reads the ARTICLES table
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// create a basket with two purchases
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// checks
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// shopping cart validation
articlesDomain.acheter(panier);
// checks
assertEquals(0,articlesDomain.getErreurs().size());
assertEquals(0,panier.getAchats().size());
// search article n° 3
article3=articlesDomain.getArticleById(3);
assertEquals(20,article3.getStockActuel());
// search article n° 4
article4=articlesDomain.getArticleById(4);
assertEquals(30,article4.getStockActuel());
// new basket
panier.ajouter(new Achat(article3,100));
// shopping cart validation
articlesDomain.acheter(panier);
// checks - we bought too much
// we must have an error
assertEquals(1,articlesDomain.getErreurs().size());
// search article n° 3
article3=articlesDomain.getArticleById(3);
// its stock must not have changed
assertEquals(20,article3.getStockActuel());
}
// withdraw purchases
public void testRetirerAchats(){
// delete contents of ARTICLES
articlesDao.clearAllArticles();
// reads the ARTICLES table
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// insertion
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// reads the ARTICLES table
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// create a basket with two purchases
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// checks
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// add a previously purchased item
panier.ajouter(new Achat(article3,10));
// checks
// the total must be increased to 1000
assertEquals(1000.0,panier.getTotal(),1e-6);
// always 2 items in the basket
assertEquals(2,panier.getAchats().size());
// qty item 3 increased to 20
Achat achat=(Achat)panier.getAchats().get(0);
assertEquals(20,achat.getQte());
// article 3 is removed from the basket
panier.enlever(3);
// checks
// the total must be increased to 400
assertEquals(400.0,panier.getTotal(),1e-6);
// 1 item only in basket
assertEquals(1,panier.getAchats().size());
// this must be article no. 4
assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
}
}
|
Comentários:
- A classe de teste utiliza o seu método setUp para armazenar uma instância da classe em teste, bem como uma instância da classe de acesso aos dados. Este último ponto é controverso. Teoricamente, a classe de teste não deveria precisar de aceder à camada [DAO], da qual nem sequer deveria ter conhecimento. Aqui, ignorámos esta «ética», que, se fosse seguida, nos teria obrigado a criar novos métodos na nossa interface [IArticlesDomain].
| // une instance de la classe d'accès au domaine
private IArticlesDomain articlesDomain;
// une instance de la classe d'accès aux données
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// récupère une instance d'accès au domaine
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// récupère une instance d'accès aux données
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
|
- O objeto a ser testado é fornecido pelo [Spring]. Acima, solicitamos o bean do Spring denominado [articlesDomain]. Este bean está definido no ficheiro de configuração do 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>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Como mostrado acima, o bean [articlesDomain] é uma instância da classe [istia.st.articles.domain.AchatsArticles]. Esta classe possui um construtor que espera, como parâmetro, um objeto que forneça acesso à camada [dao] do tipo [IArticlesDao]. Aqui, o ficheiro de configuração especifica que este objeto é o bean denominado [articlesDao]. Isto obriga o Spring a instanciar este bean. A instanciação do bean [articlesDao] foi explicada anteriormente. Assim, em última análise, foram instanciados dois beans:
- [articlesDao] do tipo [istia.st.articles.dao.ArticlesDaoSqlMap]
- [articlesDomain] do tipo [istia.st.articles.domain.AchatsArticles]
Estas duas instanciações são desencadeadas pela primeira chamada ao Spring:
| // récupère une instance d'accès au domaine
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
|
O bean [articlesDomain] é então recuperado. Durante a segunda chamada ao Spring:
| // récupère une instance d'accès aux données
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
|
[Spring] simplesmente devolve uma referência ao bean [articlesDao] que já tinha sido criado durante a chamada anterior. Este é o princípio do singleton. Se solicitar um bean ao Spring, este instancí-lo-á caso ainda não exista; caso contrário, devolverá uma referência ao bean existente.
3.4. Aplicação web MVC de três camadas
A seguir, queremos construir a seguinte aplicação web de três camadas:
A aplicação terá uma arquitetura MVC. O modelo M já foi escrito e testado. É o modelo descrito anteriormente. É-nos fornecido em três arquivos [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Precisamos de escrever o controlador C e as vistas V.
Primeiro, vamos considerar uma abordagem clássica, em que:
- o controlador C é gerido por um único servlet
- as vistas V são geridas por páginas JSP
3.5. Arquitetura MVC baseada num servlet controlador e em páginas JSP
A arquitetura MVC da aplicação será a seguinte:
| Classes de negócios, classes de acesso a dados e a base de dados |
| Páginas JSP |
| o servlet que processa os pedidos do cliente |
3.5.1. O modelo
Foi apresentado anteriormente. Consiste nos arquivos Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.5.2. As vistas
As vistas correspondem às apresentadas no início deste documento:
| list.jsp | As vistas estão localizadas na pasta [vues] da aplicação  |
| info.jsp |
| cart.jsp |
| empty-cart.jsp |
| errors.jsp |
3.5.3. O Controlador
O controlador consistirá num único servlet denominado [WebArticles]. Este irá tratar de vários pedidos do cliente. Estes pedidos serão identificados pela presença de um parâmetro [action] no pedido HTTP do cliente:
| significado | ação do controlador | respostas possíveis |
| o cliente deseja a lista de itens | - solicita a lista de itens ao empresa | - [LISTA] - [ERROS] |
| O cliente solicita Informações sobre um dos itens apresentados na vista [LISTA] | - solicita o item à camada de negócios | - [INFO] - [ERROS] |
| O cliente compra um artigo | - solicita o artigo à camada de negócios e adiciona-o ao carrinho do cliente | - [INFO] se houver erro na quantidade - [LIST] se não houver erro |
| o cliente deseja remover uma compra do seu carrinho | - recuperar o carrinho da sessão e modificá-lo | - [CARRINHO] - [CARRINHO VAZIO] - [ERROS] |
| o cliente deseja visualizar o seu carrinho | - recupera o carrinho da sessão | - [CARRINHO DE COMPRAS] - [CARRINHO VAZIO] - [ERROS] |
| O cliente terminou as compras e avança para o checkout | - atualiza a base de dados com os níveis de stock dos artigos comprados - remove do carrinho do cliente os artigos cuja foram confirmados | - [LISTA] - [ERROS] |
3.5.4. Configuração da aplicação
O nosso objetivo será configurar a aplicação de forma a torná-la o mais flexível possível no que diz respeito a alterações como:
- alterações aos URLs das várias vistas
- alterações nas classes que implementam as interfaces [IArticlesDao] e [IArticlesDomain]
- alterações no SGBD, na base de dados ou na tabela de artigos
3.5.5. Alterações nas URLs
Os nomes dos URLs das vistas serão colocados no ficheiro de configuração [web.xml] da aplicação, juntamente com alguns outros 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>
|
No [web.xml]
- as URLs das várias vistas da aplicação
- o nome [springConfigFileName] do ficheiro de configuração Spring que permitirá a criação de objetos singleton para aceder às camadas de negócio e DAO
- a vista [/vues/index.jsp] que será apresentada quando o URL solicitado pelo cliente for /<context>, em que <context> é o contexto da aplicação
3.5.6. Alterar as classes que implementam as interfaces
De acordo com o princípio das arquiteturas de três camadas, as camadas devem estar isoladas umas das outras. Este isolamento é alcançado da seguinte forma:
- as camadas comunicam entre si através de interfaces, e não de classes concretas
- O código de uma camada nunca instancia a classe de outra camada para a utilizar. Simplesmente solicita uma instância da implementação da interface a uma ferramenta externa — neste caso, o [Spring] — para a camada que pretende utilizar. Para tal, sabemos que não precisa de conhecer o nome da classe de implementação, mas apenas o nome do bean do Spring para o qual pretende uma referência.
Na nossa aplicação, o ficheiro de configuração do Spring poderia ter o seguinte aspeto:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Para aceder à camada [business], uma classe na camada [Interface do Utilizador, UI] pode solicitar o bean [articlesDomain]. O Spring irá então instanciar um objeto do tipo [istia.st.articles.domain.AchatsArticles]. Para esta instanciação, é necessário um bean do tipo [articlesDao], ou seja, um objeto do tipo [istia.st.articles.dao.ArticlesDaoSqlMap]. O Spring irá então instanciar esse objeto. Esta instanciação basear-se-á nas informações contidas no ficheiro [sqlmap-config-firebird.xml], o ficheiro de configuração para acesso aos dados via SqlMap. No final da operação, a classe [UI] que solicitou o bean [articlesDomain] dispõe de toda a cadeia que a liga aos dados do SGBD:
A independência da aplicação web em relação a alterações relacionadas com o SGBD ou a base de dados é aqui assegurada pelos ficheiros de configuração do SqlMap. Existem dois:
- o ficheiro [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 ficheiro refere-se a uma base de dados Firebird. Basta alterar o nome do controlador JDBC para trabalhar com um SGBD diferente.
- O ficheiro [articles.xml], que contém as várias instruções SQL necessárias à aplicação:
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- an alias to the istia.st.articles.dao.Article class -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- mapping ORM : row table ARTICLES - instance class Article -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- query SQL to obtain all items -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- query SQL to delete all items -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- the SQL query to insert an article -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>
<!-- the SQL query to delete a given item -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- query SQL to modify a given item -->
<statement id="modifyArticle">
update ARTICLES set nom=#nom#,
prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
id=#id#
</statement>
<!-- the SQL query to obtain a given item -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- query SQL to modify the stock of a given item -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#mouvement#
where id=#id# and stockActuel+#mouvement#>=0
</statement>
</sqlMap>
|
Se os nomes da tabela de produtos ou das colunas fossem alterados, teríamos de reescrever as consultas neste ficheiro de configuração sem ter de alterar o código Java. O mesmo se aplicaria se uma consulta fosse substituída por um procedimento armazenado por motivos de desempenho.
3.5.8. A arquitetura geral da aplicação [webarticles]
Uma aplicação web Java é um puzzle com muitas peças. Atribuir-lhe uma arquitetura MVC geralmente aumenta o número dessas peças. A estrutura da aplicação [webarticles] no [Eclipse] é a seguinte:
estrutura geral - abaixo estão os
arquivos Java utilizados pelo
Eclipse.
spring: para Spring
ibatis: para SqlMap
log4j, commons-logging: para
do Spring e do SqlMap
firebird: para o SGBD Firebird
mysql: para o SGBD MySQL
jstl, standard: para o
Biblioteca de tags JSTL
|
a pasta de código-fonte Java: contém o código Java
, bem como os ficheiros de configuração
O Eclipse copia automaticamente
esses ficheiros
para [WEB-INF/classes].
É aqui que a aplicação os irá encontrar.
|
 |  |
a pasta [WEB-INF] da aplicação: contém o
descritor [web.xml] da aplicação, bem como os
ficheiros de definição da biblioteca JSTL
| |
 |  |
3.5.9. visualizações JSP
As visualizações JSP utilizam a biblioteca de tags JSTL.
Para garantir a consistência entre as diferentes vistas, estas partilharão o mesmo cabeçalho, que exibe o nome da aplicação juntamente com o menu:
O menu é dinâmico e definido pelo controlador. O controlador inclui um atributo-chave «actions» no pedido enviado para a página JSP, com um valor associado de uma matriz Hastable[]. Cada elemento desta matriz é um dicionário destinado a gerar uma opção de menu no cabeçalho. Cada dicionário tem duas chaves:
- href: o URL associado à opção do menu
- link: o texto do menu
As outras vistas da aplicação utilizarão o cabeçalho definido por [entete.jsp] utilizando a seguinte tag JSP:
<jsp:include page="entete.jsp"/>
Em tempo de execução, esta tag incluirá o código da página [entete.jsp] na página JSP que a contém. Uma vez que o URL da página é um URL relativo (sem / final), a página [entete.jsp] será procurada no mesmo diretório que a página que contém a tag <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. list.jsp
Esta vista apresenta a lista de artigos disponíveis para venda:
É apresentada após um pedido para /main?action=list ou /main?action=cartvalidation. Os parâmetros do pedido do controlador são os seguintes:
| Objeto Hashtable[] - a matriz de opções do menu |
| ArrayList de objetos do tipo [Item] |
| Objeto String - mensagem a exibir na parte inferior da página |
Cada link [Info] na tabela HTML de artigos tem um URL no formato [?action=infos&id=ID], em que ID é o campo id do artigo exibido.
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 página apresenta informações sobre um artigo e permite também a sua compra:

É apresentada na sequência de um pedido /main?action=infos&id=ID ou de um pedido /main?action=achat&id=ID quando a quantidade comprada está incorreta. Os parâmetros de pedido do controlador são os seguintes:
| Objeto Hashtable[] - a matriz de opções do menu |
| objeto do tipo [Artigo] - item a exibir |
| Objeto String - mensagem a exibir em caso de erro com a quantidade |
| Objeto String - valor a exibir no campo de entrada [Qty] |
Os campos [msg] e [qte] são utilizados em caso de erro de introdução de dados relativo à quantidade:

Esta página contém um formulário que é enviado através do botão [Comprar]. O URL de destino POST é [?action=purchase&id=ID], em que ID é o ID do artigo adquirido.
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. cart.jsp
Esta visualização apresenta o conteúdo do carrinho de compras:

É apresentada na sequência de um pedido para /main?action=cart ou /main?action=remove&id=ID. Os parâmetros de pedido do controlador são os seguintes:
| Objeto Hashtable[] - a matriz de opções do menu |
| objeto do tipo [Cart] - o carrinho a apresentar |
Cada link [Remover] na tabela do carrinho de compras em HTML tem um URL no formato [?action=removeitem&id=ID], em que ID é o campo [id] do item a ser removido do carrinho.
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. emptycart.jsp
Esta visualização apresenta informações que indicam que o carrinho está vazio:

É apresentada na sequência de um pedido para /main?action=cart ou /main?action=remove&id=ID. Os parâmetros de pedido do controlador são os seguintes:
| Objeto Hashtable[] - a matriz de opções do menu |
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. errors.jsp
Esta vista é apresentada em caso de erros:

É apresentada após qualquer pedido que resulte num erro, exceto no caso da ação de compra com uma quantidade incorreta, que é tratada pela vista [INFOS]. Os elementos do pedido do controlador são os seguintes:
| Objeto Hashtable[] - a matriz de opções do menu |
| ArrayList de objetos String que representam as mensagens de erro a exibir |
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 a página inicial da aplicação no ficheiro [web.xml] da aplicação:
| <?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>
|
A vista [index.jsp] redireciona simplesmente o cliente para o ponto de entrada da aplicação:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
|
3.5.10. O Controlador
Ainda precisamos de escrever o núcleo da nossa aplicação web, o controlador. A sua função é:
- recuperar o pedido do cliente,
- processar a ação solicitada pelo cliente utilizando classes de negócio,
- enviar a vista apropriada em resposta.
3.5.10.1. Inicialização do controlador
Quando a classe do controlador é carregada pelo servidor de servlets, o seu método [init] é executado. Isto acontecerá apenas uma vez. Uma vez carregado na memória, o controlador permanecerá lá e processará pedidos de vários clientes. Cada cliente é tratado por um segmento de execução separado, pelo que os métodos do controlador são executados simultaneamente por segmentos diferentes. Note-se que, por esta razão, o controlador não deve ter quaisquer campos que os seus métodos possam modificar. Os seus campos devem ser de leitura apenas. Estes são inicializados pelo método [init], que é a sua função principal. Este método tem a característica única de ser executado apenas uma vez por uma única thread. Portanto, não há problemas com o acesso simultâneo aos campos do controlador dentro deste método. O objetivo do método [init] é inicializar os objetos necessários à aplicação web, que serão partilhados em modo de leitura apenas por todas as threads do cliente. Estes objetos partilhados podem ser colocados em dois locais:
- os campos privados do controlador
- o contexto de execução da aplicação (ServletContext)
O método [init] da aplicação [webarticles] irá realizar as seguintes ações:
- verificar o ficheiro [web.xml] para obter os parâmetros necessários para que a aplicação funcione corretamente. Estes foram descritos na secção 3.5.5.
- inicializar um campo privado [ArrayList errors] contendo uma lista de eventuais erros. Esta lista estará vazia se não houver erros, mas existirá independentemente disso.
- Se ocorrerem erros, o método [init] pára aí. Caso contrário, cria um objeto do tipo [IArticlesDomain], que será o objeto de negócio que o controlador utiliza para as suas necessidades. Conforme explicado em 3.5.6, o controlador solicitará o bean de que necessita à estrutura Spring. Esta operação de instanciação pode resultar em vários erros. Se assim for, estes serão, mais uma vez, armazenados no campo [errors] do controlador.
3.5.10.2. Métodos doGet, doPost
Estes dois métodos tratam de pedidos HTTP GET e POST dos clientes. Serão tratados de forma intercambiável. O método [doPost] pode, assim, redirecionar para o método [doGet] ou vice-versa. O pedido do cliente será processado da seguinte forma:
- O campo [errors] será verificado. Se não estiver vazio, isso significa que ocorreram erros durante a inicialização da aplicação e que esta não pode ser executada. A vista [ERRORS] será então enviada como resposta.
- O parâmetro [action] do pedido será recuperado e verificado. Se não corresponder a uma ação conhecida, a vista [ERRORS] é enviada com uma mensagem de erro apropriada.
- Se o parâmetro [action] for válido, a solicitação do cliente é encaminhada para um procedimento específico da ação para processamento. O procedimento que lida com a ação [uneAction] terá a seguinte assinatura:
| /**
* @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. Tratamento de diferentes ações
Os métodos que tratam das várias ações possíveis da aplicação são os seguintes:
método | solicitação | processamento | respostas possíveis |
| GET /main?action=list | - solicitar a lista de itens da classe de negócios - exibi-la | [LIST] ou [ERRORS] |
| GET /main?action=info&id=ID | - recuperar o item com id=ID da da classe de negócios - exibi-lo | [INFO] ou [ERRORS] |
| POST /main?action=purchase&id=ID - A quantidade comprada está incluída nos parâmetros enviados | - solicitar o item com id=ID da classe de negócios - Adicione-o ao carrinho na sessão do cliente | [LIST] ou [INFO] ou [ERRORS] |
| GET /main?action=removePurchase&id=ID | - remover o item com id=ID da lista de compras na sessão do cliente | [CART] |
| GET /main?action=cart | - Exibir a sessão do cliente | [CART] ou [EMPTY_CART] |
| GET /main?action=cartvalidation | - Reduzir os níveis de stock de todos os artigos na [LIST] ou [ERRORS] | [LISTA] ou [ERROS] |
3.5.10.4. O 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 {
// private fields
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String URL_MAIN = "urlMain";
private final String URL_ERREURS = "urlErreurs";
private final String URL_LISTE = "urlListe";
private final String URL_INFOS = "urlInfos";
private final String URL_PANIER = "urlPanier";
private final String URL_PANIER_VIDE = "urlPanierVide";
private final String URL_DEBUG = "urlDebug";
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters =
{
URL_MAIN,
URL_ERREURS,
URL_LISTE,
URL_INFOS,
URL_PANIER,
URL_PANIER_VIDE,
URL_DEBUG,
SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste";
private final String ACTION_PANIER = "panier";
private final String ACTION_ACHAT = "achat";
private final String ACTION_INFOS = "infos";
private final String ACTION_RETIRER_ACHAT = "retirerachat";
private final String ACTION_VALIDATION_PANIER = "validationpanier";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
public void init() {
// retrieve servlet initialization parameters
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// we memorize the error
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// mistakes?
if (erreurs.size() != 0) {
return;
}
// create a IArticlesDomain business layer access object
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// we memorize the error
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// memorize certain application urls
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// it's over
return;
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// check how the initialization of the servelet went
if (erreurs.size() != 0) {
// do we have the url of the error page?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
// action is processed
String action = request.getParameter("action");
if (action == null) {
// list of items
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// list of items
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// article info
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// purchase an item
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// basket display
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// remove an item from the basket
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// shopping cart validation
doValidationPanier(request, response);
return;
}
// unknown share
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// the error page is displayed
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// end
return;
}
private void doValidationPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// the buyer has confirmed his basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
// validate this basket
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// error recovery
ArrayList erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionPanier });
afficheErreurs(request, response, erreurs);
return;
}
// displays the list of items
request.setAttribute("message", "Votre panier a été validé");
doListe(request, response);
// end
return;
}
private void doRetirerAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// remove a purchase from the basket
try {
Panier panier =
(Panier) request.getSession().getAttribute("panier");
String strIdAchat = request.getParameter("id");
panier.enlever(Integer.parseInt(strIdAchat));
} catch (NumberFormatException ignored) {
} catch (NullPointerException ignored) {
}
// the basket is displayed
doPanier(request, response);
}
private void doPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// the basket is displayed
Panier panier = (Panier) request.getSession().getAttribute("panier");
// empty basket?
if (panier == null || panier.getAchats().size() == 0) {
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
.forward(request, response);
// end
return;
}
// there's something in the basket
request.setAttribute("panier", panier);
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionValidationPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER))
.forward(request, response);
// end
return;
}
private void doAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// purchase an item
// we recover the quantity
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// wrong qty
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
String url =
config.getInitParameter(URL_MAIN)
+ "?action=infos&id="
+ request.getParameter("id");
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// end
return;
}
// retrieve the client session
HttpSession session = request.getSession();
// we create the purchase
Article article = (Article) session.getAttribute("article");
Achat achat = new Achat(article, qté);
// the purchase is added to the customer's basket
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// we return to the list of items
String url = config.getInitParameter(URL_MAIN) + "?action=liste";
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// end
return;
}
private void afficheDebugInfos(
HttpServletRequest request,
HttpServletResponse response,
ArrayList infos)
throws ServletException, IOException {
// displays the list of items
request.setAttribute("infos", infos);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_DEBUG))
.forward(request, response);
// end
return;
}
public void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// idem get
doGet(request, response);
}
private void doInfos(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// error list
ArrayList erreurs = new ArrayList();
// retrieve the requested id
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// the key item id is requested
Article article = null;
try {
article=articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
if (article == null) {
// not normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// put the article in the session
request.getSession().setAttribute("article", article);
// the info page is displayed
request.setAttribute("actions", new Hashtable[] { hActionListe });
// request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_INFOS))
.forward(request, response);
// end
return;
}
private void afficheErreurs(
HttpServletRequest request,
HttpServletResponse response,
ArrayList erreurs)
throws ServletException, IOException {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// list of errors
ArrayList erreurs = new ArrayList();
// the list of items is requested
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// we memorize the error
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
// displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// end
return;
}
/**
* suivi console pour débogage
* @param message : le message à afficher
*/
private void affiche(String message) {
System.out.println(message);
}
}
|
Deixaremos que o leitor dedique algum tempo a ler e compreender este código. Esperamos que os comentários ajudem.
3.5.10.5. Teste de aplicações
Vejamos algumas capturas de ecrã dos testes. Primeiro, a página inicial da aplicação:

O URL solicitado era, na verdade, [http://localhost:8080/webarticles]. O leitor verá que, no ficheiro [web.xml], definimos uma página inicial para a aplicação:
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
A vista [index.jsp] é definida da seguinte forma:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
Houve, portanto, um redirecionamento para o URL [http://localhost:8080/webarticles/main?action=liste], conforme mostrado pelo URL do navegador na captura de ecrã. O URL [/main?action=liste] foi, assim, solicitado. Ainda no [web.xml], o URL /main está associado ao servlet [webarticles]:
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
Também no [web.xml], o servlet [webarticles] está associado ao servlet [istia.st.articles.web.WebArticles]:
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
O servlet [istia.st.articles.web.WebArticles] é, portanto, carregado pelo contentor de servlets Tomcat, caso ainda não o tenha sido, e o seu método [init] é executado:
| public void init() {
// retrieve servlet initialization parameters
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// we memorize the error
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// mistakes?
if (erreurs.size() != 0) {
return;
}
// create a IArticlesDomain business layer access object
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// we memorize the error
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// memorize certain application urls
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// it's over
return;
}
|
Comentários: O método [init]
- verifica a presença de determinados parâmetros de configuração
- instancia um serviço para aceder ao domínio da aplicação utilizando o Spring
- define uma lista de erros para indicar quaisquer erros de inicialização
- vários campos privados:
- errors: a lista de erros detetados por [init]
- [hActionListe, hActionPanier, hActionValidationPanier]: dicionários. Cada um contém as informações necessárias para exibir uma opção no menu principal apresentado pela vista [entete.jsp]
- acticlesDomain: o serviço para aceder ao modelo da aplicação
O método [init] é executado apenas uma vez, no carregamento inicial do servlet. Depois disso, um dos métodos [doGet, doPost] é executado, dependendo do tipo [GET, POST] do pedido do cliente. Aqui, ambos os métodos fazem a mesma coisa, e o código foi colocado em [doGet]:
| public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// check how the initialization of the servelet went
if (erreurs.size() != 0) {
// do we have the url of the error page?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
// action is processed
String action = request.getParameter("action");
if (action == null) {
// list of items
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// list of items
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// article info
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// purchase an item
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// basket display
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// remove an item from the basket
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// shopping cart validation
doValidationPanier(request, response);
return;
}
// unknown share
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// the error page is displayed
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// end
return;
}
|
- O método [doGet] começa por verificar se ocorreram erros de inicialização após o método [init]. Se for o caso, apresenta a vista [ERRORS] e o processo termina.
- Caso contrário, recupera o parâmetro [action] da solicitação do cliente. Lembre-se de que a aplicação foi construída para responder a solicitações que devem incluir um parâmetro [action].
- Executa o método associado à ação. Aqui, esse será o método [doListe].
O método [doListe] é o seguinte:
| private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// error list
ArrayList erreurs = new ArrayList();
// the list of items is requested
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// we memorize the error
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
// displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// end
return;
}
|
- Lembre-se de que o método [init] armazenou o serviço para aceder ao modelo da aplicação (camada de domínio) num campo privado do servlet:
// champs privés
private IArticlesDomain articlesDomain = null;
- Usando este serviço de acesso, podemos solicitar a lista de artigos:
| // la liste des erreurs
ArrayList erreurs = new ArrayList();
// on demande la liste des articles
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// on mémorise l'erreur
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- Se ocorrerem erros, é enviada a visualização [ERRORS]:
| // mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// end
return;
}
|
- caso contrário, a vista [LIST] é enviada:
| // displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
|
No nosso exemplo, tudo correu bem e obtivemos com sucesso a vista [LIST]. Convidamos o leitor a rever o código da vista [LIST] para verificar se os parâmetros dinâmicos esperados por esta vista são, de facto, fornecidos acima pelo controlador. O mesmo tipo de verificação deve ser realizado para cada vista:
- identifique os parâmetros dinâmicos da vista
- garanta que o controlador os coloca nos atributos de solicitação passados para a vista
Vamos agora simplesmente delinear o fluxo de ecrãs com que se depara um utilizador da aplicação. Convidamos o leitor a seguir uma linha de raciocínio semelhante à anterior em cada ocasião:
A partir da lista de itens, o utilizador pode selecionar um item:
O comprador pode adquirir o artigo n.º 3 aqui. Vamos introduzir um erro na quantidade:
O erro foi sinalizado. Agora, vamos comprar alguns itens:
A compra foi registada e a lista de itens foi novamente apresentada. Vamos verificar o carrinho de compras:
A compra está, de facto, no carrinho de compras. Vamos removê-la:
A compra foi removida do carrinho de compras e o carrinho foi recarregado. Agora está vazio.
Vamos comprar 100 unidades do artigo n.º 3 e 2 unidades do artigo n.º 4:
A compra do artigo n.º 3 não foi possível porque queríamos comprar 100, mas havia apenas 30 em stock. Esta compra permaneceu no carrinho:
O item n.º 4, no entanto, foi comprado, como mostra o seu novo nível de stock de 39 (40-1):
3.6.1. Arquitetura Geral de Aplicações
Vamos rever a arquitetura MVC da aplicação:
Na versão anterior:
- o controlador era gerido por um servlet
- as visualizações eram geridas por páginas JSP
- o modelo era gerido por um conjunto de três ficheiros .jar
Na versão Struts:
- o controlador será gerido por um servlet derivado do [ActionServlet] genérico do Struts
- As vistas serão geridas pelas mesmas páginas JSP de antes, com algumas pequenas diferenças
- O modelo será gerido pelos mesmos três arquivos
Veremos que a migração da aplicação anterior para o Struts envolve as seguintes tarefas:
- as ações que eram anteriormente tratadas em métodos específicos do servlet/controlador são agora tratadas por instâncias de classes derivadas da classe [Action] do Struts
- Escrever os ficheiros de configuração [web.xml] e [struts-config.xml]
- Fazer algumas alterações nas páginas JSP
Vamos rever a arquitetura MVC genérica utilizada pelo Struts:
| classes de negócios, classes de acesso a dados e a base de dados |
| páginas JSP |
| o servlet que processa os pedidos do cliente, os objetos [Action] e os beans [ActionForm] associados aos formulários. |
- O controlador é o coração da aplicação. Todas as solicitações do cliente passam por ele. É um servlet genérico fornecido pelo STRUTS. Em alguns casos, poderá ser necessário estendê-lo. Para casos simples, isso não é necessário. Este servlet genérico recupera as informações de que necessita de um ficheiro geralmente chamado struts-config.xml.
- Se a solicitação do cliente contiver parâmetros de formulário, o controlador coloca-os num objeto Bean. Os objetos Bean criados ao longo do tempo são armazenados na sessão ou na solicitação do cliente. Este comportamento é configurável. Não precisam de ser recriados se já tiverem sido criados.
- No ficheiro de configuração struts-config.xml, cada URL a ser processada programaticamente (e, portanto, não correspondente a uma vista JSP que possa ser solicitada diretamente) está associada a determinadas informações:
- o nome da classe Action responsável pelo processamento da solicitação. Aqui, mais uma vez, o objeto Action instanciado pode ser armazenado na sessão ou na solicitação.
- Se a URL solicitada for parametrizada (como quando um formulário é enviado ao controlador), é especificado o nome do bean responsável por armazenar os dados do formulário.
- Com base nas informações fornecidas pelo seu ficheiro de configuração, ao receber um pedido de URL de um cliente, o controlador consegue determinar se é necessário criar um bean e qual deles. Uma vez instanciado, o bean pode verificar se os dados que armazenou — provenientes do formulário — são válidos ou não. Um método do bean chamado `validate` é chamado automaticamente pelo controlador. O bean é criado pelo programador. O programador, portanto, coloca o código que verifica a validade dos dados do formulário dentro do método validate. Se os dados forem considerados inválidos, o controlador não prosseguirá. Ele passará o controlo para uma vista cujo nome encontra no seu ficheiro de configuração. A interação fica então concluída. Note-se que o programador pode optar por não verificar a validade do formulário. Isto também é feito no ficheiro struts-config.xml. Neste caso, o controlador não chama o método validate do bean.
- Se os dados do bean estiverem corretos, ou se não houver validação, ou se não houver bean, o controlador passa o controlo para o objeto Action associado à URL. Faz isso chamando o método execute desse objeto, passando-lhe a referência ao bean que ele possa ter construído. É aqui que o programador faz o que precisa ser feito: pode ser necessário chamar classes de negócio ou classes de acesso a dados. No final do processamento, o objeto Action devolve ao controlador o nome da vista que este deve enviar em resposta ao cliente.
- No seu ficheiro de configuração, o controlador irá encontrar a URL associada ao nome da vista que lhe foi pedido para apresentar. Em seguida, envia a vista. A interação com o cliente está concluída.
Na nossa aplicação, não utilizaremos objetos [Bean] como objetos de buffer entre o cliente e as classes [Action]. O objeto [Action] irá recuperar diretamente os parâmetros do pedido do cliente a partir do objeto [HttpServletRequest] que recebe. Isto facilita a portabilidade da nossa aplicação inicial. A arquitetura final da nossa aplicação será, portanto, a seguinte:
| classes de negócios, classes de acesso a dados e a base de dados |
| as páginas JSP |
| o servlet para processar os pedidos do cliente, objetos [Action] |
3.6.2. O modelo
Foi apresentado anteriormente. Consiste nos arquivos Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.6.3. Configuração da aplicação
3.6.3.1. Arquitetura geral
A arquitetura geral do projeto Eclipse é a seguinte:

3.6.3.2. Configuração do acesso aos dados
Uma vez que a interface de acesso aos dados permanece inalterada, os ficheiros de configuração associados são os mesmos da versão anterior. Estão definidos em [WEB-INF/src]:

Na captura de ecrã acima, os ficheiros [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] são os da versão anterior.
3.6.3.3. O diretório de arquivos
Em [WEB-INF/lib], encontrará as mesmas bibliotecas da versão anterior, além daquela exigida pelo Struts:

3.6.3.4. Configuração da aplicação
A aplicação é configurada utilizando dois ficheiros: [web.xml, struts-config.xml] na pasta [WEB-INF]:

O ficheiro [web.xml] é o seguinte:
| <?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>
|
O que diz este ficheiro?
- A página inicial da aplicação é [views/index.jsp] (welcome-file)
- Os pedidos de URL do tipo *.do serão redirecionados para o servlet [strutswebarticles] (servlet-mapping)
- O servlet [strutswebarticles] é uma instância da classe [istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
- Este servlet aceita dois parâmetros de inicialização
- o nome do ficheiro de configuração do Struts (config)
- O nome do ficheiro de configuração do Spring (springConfigFileName)
O ficheiro [struts-config.xml] é o seguinte:
| <?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>
|
O que diz este ficheiro de configuração?
- que o nosso controlador irá tratar das seguintes URLs:
| para exibir a lista de artigos |
| para exibir a lista de artigos |
| para exibir informações sobre um item específico |
| para comprar um item específico |
| para visualizar o carrinho de compras |
| para remover uma compra do carrinho de compras |
| para confirmar o carrinho de compras |
- As ações listadas acima correspondem, uma a uma, às ações tratadas pelo servlet na versão anterior. Para cada uma delas, são fornecidas as seguintes informações:
- o nome da classe responsável pelo tratamento desta ação
- as respostas possíveis (= visualizações) após o processamento da ação. Apenas uma delas será selecionada pelo controlador.
- o nome de um ficheiro de mensagens para a aplicação (message-resources). Aqui, o ficheiro existirá, mas estará vazio. Não será utilizado. Deve ser colocado no [ClassPath] da aplicação. Aqui, será colocado em [WEB-INF/classes]. No Eclipse, isto é conseguido colocando-o em [WEB-INF/src]:

3.6.4. As Visualizações JSP
As vistas JSP utilizadas aqui são também as da versão anterior. Foram feitas muito poucas alterações: as URLs do formato [?action=XX?id=YY& ...] foram alteradas para [/XX.do?id=YY&....]. Estamos a repetir explicações já dadas aqui para evitar que o utilizador tenha de voltar atrás. É importante compreender que a informação passada para a vista pelo controlador é exatamente a mesma em ambas as versões. Nada mudou a este respeito.
3.6.4.1. entete.jsp
Para garantir a consistência entre as diferentes vistas, estas partilharão o mesmo cabeçalho, que exibe o nome da aplicação juntamente com o menu:
O menu é dinâmico e definido pelo controlador. O controlador inclui na solicitação enviada à página JSP um atributo-chave "actions" com um valor associado de uma matriz Hastable[]. Cada elemento desta matriz é um dicionário destinado a gerar uma opção de menu no cabeçalho. Cada dicionário tem duas chaves:
- href: o URL associado à opção do menu
- link: o texto do menu
As outras vistas da aplicação utilizarão o cabeçalho definido por [entete.jsp] utilizando a seguinte tag JSP:
<jsp:include page="entete.jsp"/>
Quando executada, esta tag incluirá o código da página [entete.jsp] na página JSP que a contém. Uma vez que o URL da página é um URL relativo (sem / no final), a página [entete.jsp] será procurada no mesmo diretório que a página que contém a tag <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>
|
Comentários: sem alterações em relação à versão anterior
3.6.4.2. list.jsp
Esta vista apresenta a lista de artigos disponíveis para venda:
É apresentada após um pedido para /main.do ou /validerpanier.do. Os parâmetros de pedido do controlador são os seguintes:
| Objeto Hashtable[] - a matriz de opções do menu |
| ArrayList de objetos do tipo [Item] |
| Objeto String - mensagem a exibir na parte inferior da página |
Cada link [Info] na tabela HTML de artigos tem um URL no formato [/infos.do?id=ID], em que ID é o campo id do artigo exibido.
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>
|
Comentários: uma alteração (destacada acima)
3.6.4.3. infos.jsp
Esta página apresenta informações sobre um artigo e permite também a sua compra:

É apresentada na sequência de um pedido para /infos.do?id=ID ou de um pedido para /achat.do?id=ID quando a quantidade comprada está incorreta. Os elementos do pedido do controlador são os seguintes:
| object Hashtable[] - a matriz de opções do menu |
| objeto do tipo [Artigo] - item a exibir |
| Objeto String - mensagem a exibir em caso de erro com a quantidade |
| Objeto String - valor a exibir no campo de entrada [Qty] |
Os campos [msg] e [qte] são utilizados em caso de erro de introdução de dados relativo à quantidade:

Esta página contém um formulário que é enviado através do botão [Comprar]. O URL de destino POST é [/achat.do?id=ID], em que ID é o ID do artigo adquirido.
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>
|
Comentários: uma alteração (destacada acima)
3.6.4.4. cart.jsp
Esta vista apresenta o conteúdo do carrinho de compras:

É apresentada na sequência de um pedido para /panier.do ou /retirerachat.do?id=ID. Os parâmetros de pedido do controlador são os seguintes:
| Objeto Hashtable[] - a matriz de opções do menu |
| objeto do tipo [ShoppingCart] - o carrinho de compras a apresentar |
Cada link [Remove] na matriz do carrinho de compras em HTML tem um URL no formato [removeitem.do?id=ID], em que ID é o campo [id] do item a ser removido do carrinho.
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>
|
Comentários: uma alteração (destacada acima)
3.6.4.5. emptycart.jsp
Esta visualização apresenta informações que indicam que o carrinho está vazio:

É apresentada na sequência de um pedido para /panier.do ou /retirerachat.do?id=ID. Os parâmetros de pedido do controlador são os seguintes:
| Objeto Hashtable[] - a matriz de opções do menu |
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>
|
Comentários: sem alterações.
3.6.4.6. errors.jsp
Esta vista é apresentada em caso de erros:

É apresentada após qualquer pedido que resulte num erro, exceto no caso da ação de compra com uma quantidade incorreta, que é tratada pela vista [INFOS]. Os elementos do pedido do controlador são os seguintes:
| Objeto Hashtable[] - a matriz de opções do menu |
| ArrayList de objetos String que representam as mensagens de erro a exibir |
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>
|
Comentários: sem alterações.
3.6.4.7. index.jsp
Esta página está definida como a página inicial da aplicação no ficheiro [web.xml] da aplicação:
| <?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>
|
A vista [index.jsp] redireciona simplesmente o cliente para o ponto de entrada da aplicação:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
Comentários: uma alteração (destacada acima)
3.6.5. O Controlador Struts
O Struts possui um controlador genérico chamado [ActionServlet]. Sabemos que um servlet possui um método [init] que permite que a aplicação seja inicializada quando é iniciada. Se utilizarmos o controlador genérico do Struts [ActionServlet], não temos acesso ao seu método [init]. Aqui, temos tarefas a realizar quando a aplicação é iniciada, principalmente instanciar um objeto de acesso ao modelo. Portanto, precisamos de um método [init]. Assim, estendemos a classe [ActionServlet] na seguinte classe [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 {
// private fields
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters = { SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_ACHAT = "achat.do";
private final String ACTION_INFOS = "infos.do";
private final String ACTION_RETIRER_ACHAT = "retirerachat.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// getters - setters
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public ArrayList getErreurs() {
return erreurs;
}
public void setErreurs(ArrayList erreurs) {
this.erreurs = erreurs;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public void setHActionListe(Hashtable actionListe) {
hActionListe = actionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public void setHActionPanier(Hashtable actionPanier) {
hActionPanier = actionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
public void setHActionValidationPanier(Hashtable actionValidationPanier) {
hActionValidationPanier = actionValidationPanier;
}
public void init() throws ServletException{
// init parent class
super.init();
// retrieve servlet initialization parameters
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// we memorize the error
erreurs.add("Paramètre [" + parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// mistakes?
if (erreurs.size() != 0) {
return;
}
// create a IArticlesDomain business layer access object
try {
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource((String) config
.getInitParameter(SPRING_CONFIG_FILENAME))))
.getBean("articlesDomain");
} catch (Exception ex) {
// we memorize the error
erreurs.add("Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// memorize certain application urls
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// it's over
return;
}
}
|
Comentários:
- O valor da classe reside no seu método [init] e nos seus campos privados
- O método [init] faz o mesmo que o método [init] no controlador da versão anterior:
- verifica a presença de determinados parâmetros de configuração
- Ele instancia um serviço para aceder ao domínio da aplicação utilizando o Spring
- configurar uma lista de erros para indicar quaisquer erros de inicialização
- Antes de iniciar o trabalho, o método [init] chama o método [init] da classe pai [ActionServlet]. Este método irá processar o ficheiro de configuração do Struts [struts-config.xml].
- São definidos vários campos privados com os seus acessores:
- errors: a lista de erros detetados pelo [init]
- [hActionListe, hActionPanier, hActionValidationPanier]: dicionários. Cada um contém as informações necessárias para exibir uma opção no menu principal apresentado pela vista [entete.jsp]
- acticlesDomain: o serviço que fornece acesso ao modelo da aplicação
- O controlador de uma aplicação Struts é acessível às classes [Action] responsáveis por lidar com as várias ações possíveis. Estas classes terão acesso aos campos privados anteriores, uma vez que lhes são fornecidos acessores públicos.
Este controlador é instanciado pelo ficheiro [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>
|
Qualquer URL que termine em .do será tratada por uma instância da classe [istia.st.articles.web.struts.MainServlet]
3.6.6. Ações da aplicação Struts
3.6.6.1. Introdução
Cada ação Struts será implementada como uma classe. Na versão anterior, cada ação era implementada como um método no controlador da aplicação. Escrever a classe [Action] envolve normalmente:
- copiar e colar o método utilizado na versão anterior
- adaptar o código às convenções do Struts
3.6.6.2. main.do, list.do
Estas duas ações são idênticas e estão definidas no [struts-config.xml] da seguinte forma:
| <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>
|
Quando uma destas ações [main.do, liste.do] é executada num navegador, obtém-se o seguinte resultado:

O código da classe [ListeArticlesAction] é o seguinte:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST - ISTIA
*
*/
public class ListeArticlesAction extends Action {
/**
* affichage de la liste des articles - s'appuie sur la couche [domain]
*
* @param mapping :
* configuration de l'action dans struts-config.xml
* @param form :
* le formulaire passé à l'action - ici aucun
* @param request :
* la requête HTTP du client
* @param response :
* la réponse HTTP au client
*/
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// list of errors
ArrayList erreurs = new ArrayList();
// the list of items is requested
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// we memorize the error
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
}
}
|
Comentários:
- Escrever o código para uma classe [Action] consiste essencialmente em escrever o código para o seu método [execute]
- Foi armazenada uma certa quantidade de informação na instância do controlador. Recuperamos uma referência a ela utilizando:
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
- Recuperamos a lista de erros de inicialização armazenada pelo controlador. Se esta lista não estiver vazia, a vista [ERRORS] é enviada ao cliente:
| // initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
|
A vista que será efetivamente enviada ao cliente é fornecida pelo [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>
Esta é a vista [/vues/erreurs.jsp]. O leitor é convidado a verificar o que esta vista espera. Esta informação é fornecida aqui pela ação no objeto [request] como atributos.
- Mais uma vez, graças ao controlador, a ação pode recuperar o objeto que fornece acesso ao modelo da aplicação (camada de domínio):
// domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
- Depois de fazer isto, podemos solicitar a lista de artigos:
| // la liste des erreurs
ArrayList erreurs = new ArrayList();
// on demande la liste des articles
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// on mémorise l'erreur
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- Se ocorrerem erros, é enviada a visualização [ERRORS]:
| // mistakes?
if (erreurs.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- caso contrário, a vista [LIST] é enviada:
| // displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
|
A vista que será efetivamente enviada ao cliente é fornecida por [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>
Esta é a vista [/vues/liste.jsp]. O leitor é convidado a verificar o que é esperado por esta vista. Esta informação é fornecida aqui pela ação no objeto [request] como atributos.
3.6.6.3. infos.do
Esta ação é utilizada para fornecer informações sobre um dos itens apresentados na vista [LIST]:
Esta ação é configurada da seguinte forma no ficheiro [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>
O código para a classe [InfosArticleAction] é o seguinte:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class InfosArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// list of errors
ArrayList erreurs = new ArrayList();
// retrieve the requested id
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// the key item id is requested
Article article = null;
try {
article = articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
if (article == null) {
// not normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// put the article in the session
request.getSession().setAttribute("article", article);
// the info page is displayed
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
}
|
Comentários:
- O início do método [execute] é idêntico ao discutido anteriormente. O mesmo se aplica às outras ações.
- O método recupera o parâmetro [id], que normalmente deve estar presente na URL. A URL deve, de facto, ter o formato [/infos.do?id=X]. São realizadas várias verificações para confirmar a presença e a validade do parâmetro [id]. Se houver algum problema, a vista [ERRORS] é apresentada.
- Se [id] for válido, o item correspondente é solicitado à camada [domain]. Se isto lançar uma exceção ou se o item não for encontrado, a vista [ERRORS] é enviada novamente.
- Se tudo correr bem, o item recuperado é armazenado na sessão. Este é um ponto discutível. Aqui, assumimos que o cliente pode comprar este item. Se o fizer, iremos recuperá-lo da sessão em vez de o solicitar novamente à camada [domain].
- Por fim, a vista [INFOS] é apresentada. A vista que será efetivamente enviada ao cliente é fornecida pelo [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>
Esta é a vista [/vues/infos.jsp]. O leitor é convidado a verificar o que é esperado por esta vista. Esta informação é fornecida aqui pela ação no objeto [request] como atributos.
3.6.6.4. purchase.do
Esta ação é utilizada para comprar o artigo apresentado pela vista [INFOS] anterior:
Assim que o item é comprado, a vista [LIST] é exibida novamente (vista à direita). Se analisarmos o código HTML da vista à esquerda acima, vemos que a tag <form> está definida da seguinte forma:
| <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>
|
Podemos ver que o formulário é enviado para o controlador com a ação [achat.do].
Esta ação está configurada da seguinte forma no [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>
O código da classe [AchatArticleAction] é o seguinte:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST
*
*/
public class AchatArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// the quantity purchased is recovered
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// wrong qty
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
// retrieve the client session
HttpSession session = request.getSession();
// we retrieve the article placed in session
Article article = (Article) session.getAttribute("article");
// session expired?
if(article==null){
// the error page is displayed
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// create the new purchase
Achat achat = new Achat(article, qté);
// the purchase is added to the customer's basket
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// we return to the list of items
return mapping.findForward("afficherListeArticles");
}
}
|
Comentários:
- O início do método [execute] é idêntico aos estudados anteriormente.
- Recordemos o formato do formulário enviado ao 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>
|
- Existem dois parâmetros na solicitação: [id]: número do item, [qte]: quantidade comprada.
- A presença e a validade do parâmetro [qte] são verificadas. Se este parâmetro for considerado incorreto, a vista [INFOS] é devolvida ao utilizador juntamente com uma mensagem de erro:
| // the list of errors on this action
ArrayList erreurs = new ArrayList();
// the quantity purchased is recovered
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// wrong qty
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
|
- O item adquirido é recuperado da sessão. A sessão pode ter expirado. Neste caso, é apresentada a vista [ERRORS]:
| // retrieve the client session
HttpSession session = request.getSession();
// we retrieve the article placed in session
Article article = (Article) session.getAttribute("article");
// session expired?
if(article==null){
// the error page is displayed
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- Se a sessão não tiver expirado, o item é adicionado ao carrinho, que também é recuperado da sessão:
| // retrieve the client session
HttpSession session = request.getSession();
// we retrieve the article placed in session
Article article = (Article) session.getAttribute("article");
// session expired?
if(article==null){
// the error page is displayed
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// create the new purchase
Achat achat = new Achat(article, qté);
// the purchase is added to the customer's basket
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- Por fim, enviamos a vista [LIST]:
// on revient à la liste des articles
return mapping.findForward("afficherListeArticles");
- A vista que será efetivamente enviada ao cliente é fornecida pelo [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>
Esta é a vista [/main.do]. Esta vista não é uma vista, mas sim uma ação. A ação [/main.do] descrita acima será, portanto, executada e exibirá a lista de itens.
3.6.6.5. cart.do
Esta ação é utilizada para apresentar todas as compras do cliente. Está disponível através da opção de menu [Ver Carrinho]:
O código HTML associado ao link [Ver carrinho] é o seguinte:
<a href="panier.do">Voir le panier</a>
A ação [panier.do] está configurada da seguinte forma no ficheiro [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>
O código para a classe [VoirPanierAction] é o seguinte:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class VoirPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// the basket is displayed
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// empty basket
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
} else {
// there's something in the basket
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(), mainServlet.getHActionValidationPanier() });
return mapping.findForward("afficherPanier");
}
}
}
|
Comentários:
- O início do método [execute] é idêntico aos discutidos anteriormente.
- O carrinho de compras é recuperado da sessão onde normalmente se encontra. A sessão pode ter expirado, caso em que não existe carrinho de compras. Não tratamos isto como um erro, mas simplesmente assumimos que o carrinho de compras está vazio.
- Se o carrinho estiver vazio, é apresentada a vista [EMPTY CART]
- caso contrário, é apresentada a vista [PANIER]
As vistas efetivamente enviadas ao cliente são definidas pela ação:
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
3.6.6.6. checkout.do
Esta ação remove um item do carrinho de compras:
Após a ação [retirerachat.do], o carrinho de compras é exibido novamente (visualização à direita, acima). Se analisarmos o código HTML do link [Confirmar Carrinho] na visualização à esquerda, acima, vemos o seguinte:
<a href="retirerachat.do?id=3">Retirer</a>
A ação [retirerachat.do] recebe, portanto, como parâmetro, o ID do item a ser removido do carrinho. Esta ação está configurada da seguinte forma no [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>
O código para a classe [RetirerAchatAction] é o seguinte:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class RetirerAchatAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// we pick up the basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
}
// retrieve the id of the item to be removed
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// we remove the purchase
panier.enlever(id);
// the basket is displayed again
return mapping.findForward("afficherPanier");
}
}
|
Comentários:
- O início do método [execute] é idêntico aos estudados anteriormente.
- O código a seguir verifica a presença e a validade do parâmetro [id]. Se estiver incorreto, é enviada a visualização [ERRORS].
- Caso contrário, a compra é removida do carrinho:
// on enlève l'achat
panier.enlever(id);
- depois, o carrinho é exibido novamente:
// on affiche de nouveau le panier
return mapping.findForward("afficherPanier");
A vista efetivamente enviada ao cliente é definida pela ação:
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Podemos ver que, em termos de visualizações, é a ação [/cart.do] que será acionada. Isto já foi descrito. Ela exibirá a visualização [CART] ou [EMPTY CART], dependendo do estado do carrinho.
3.6.6.7. confirmCart.do
Esta ação é utilizada para confirmar as compras do cliente. Na prática, isto envolve uma única ação: os níveis de stock dos artigos comprados são reduzidos nas quantidades adquiridas na base de dados. Esta ação provém do seguinte menu:
O código HTML para o link [Confirmar carrinho] é o seguinte:
<a href="validerpanier.do">Valider le panier</a>
Quando este link é clicado, os níveis de stock são atualizados e a lista de artigos é apresentada novamente.
Esta ação está configurada da seguinte forma no [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>
O código para a classe [ValiderPanierAction] é o seguinte:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class ValiderPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// the control servlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialization errors?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// the error page is displayed
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// domain access object
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// the buyer has confirmed his basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// validate basket
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// recover any errors
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
// everything looks OK - the item list is displayed
return mapping.findForward("afficherListeArticles");
}
}
|
Comentários:
- O início do método [execute] é idêntico aos estudados anteriormente.
- Recuperamos o carrinho de compras da sessão. Se a sessão tiver expirado, exibimos a vista [ERRORS]:
| // the list of errors on this action
ArrayList erreurs = new ArrayList();
// the buyer has confirmed his basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- Processamos as compras no carrinho. Podem ocorrer erros se os níveis de stock forem insuficientes para satisfazer as compras. Neste caso, apresentamos a vista [ERRORS]:
| // validate basket
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// recover any errors
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
|
- Se tudo correu bem, exibimos novamente a lista de itens:
// tout semble OK - on affiche la liste des articles
return mapping.findForward("afficherListeArticles");
A vista efetivamente enviada ao cliente é definida pela ação:
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Podemos ver que, em termos da vista, é a ação [/main.do] que será acionada. Isto já foi descrito. Ela irá apresentar a vista [LIST].
3.7.1. Arquitetura geral da aplicação
Vamos rever a arquitetura MVC da aplicação:
Na primeira versão:
- o controlador era gerido por um servlet
- as visualizações eram geridas por páginas JSP
- o modelo era gerido por um conjunto de três ficheiros .jar
Na versão Struts:
- o controlador era gerido por um servlet derivado do [ActionServlet] genérico do Struts
- as vistas eram geridas pelas mesmas páginas JSP que na versão [Struts]
- O modelo era gerido pelos mesmos três arquivos
Na versão Spring:
- o controlador será tratado por um servlet fornecido pelo Spring [DispatcherServlet]
- As vistas serão geridas pelas mesmas páginas JSP de antes, com algumas pequenas diferenças
- O modelo será gerido pelos mesmos três ficheiros
Veremos que a migração da nossa aplicação do Struts para o Spring é simples, se estivermos dispostos a abdicar da utilização de todos os elementos recomendados para uma arquitetura Spring MVC padrão. As principais alterações são as seguintes:
- as ações que anteriormente eram tratadas em métodos específicos do servlet/controlador, ou por instâncias de classes derivadas da classe [Action] no Struts, são agora tratadas por instâncias de classes que implementam a interface [Controller] do Spring
- Os ficheiros de configuração necessários são os seguintes:
- [web.xml], uma vez que se trata de uma aplicação web. Este ficheiro contém um ouvinte que, quando a aplicação for inicializada, utilizará o [applicationContext.xml]
- [applicationContext.xml] para criar os beans necessários à aplicação, em particular o bean do serviço de acesso ao modelo
- As vistas JSP serão idênticas às do Struts. Teremos de criar uma nova.
Recordemos a arquitetura MVC do STRUTS utilizada na versão anterior:
| classes de negócios, classes de acesso a dados e a base de dados |
| as páginas JSP |
| o servlet para processar os pedidos do cliente, objetos [Action] |
Com o Spring, usamos a mesma arquitetura:
| classes de negócios, classes de acesso a dados e a base de dados |
| as páginas JSP |
| o servlet que processa os pedidos dos clientes, objetos que implementam a interface [Controller] |
- O controlador é o coração da aplicação. Todas as solicitações do cliente passam por ele. É um servlet genérico fornecido pelo SPRING. É do tipo [DispatcherServlet]. A partir de agora, nos referiremos a este controlador como o controlador [Spring].
- O controlador [Spring] encaminhará a solicitação do cliente para uma das instâncias [Controller]. Haverá uma instância por ação a ser processada. Isso é definido na URL solicitada, tal como no Struts. Assim, saberemos que a ação solicitada é a ação [list] porque a URL solicitada é [list.do]
- Se C for o contexto da aplicação, o controlador [Spring] utiliza um ficheiro [C-servlet.xml] que desempenha o mesmo papel que o ficheiro de configuração struts-config.xml na versão Struts. Para cada ação a ser processada pela aplicação, associamos o nome da classe do tipo Controller responsável por tratar a solicitação.
- O controlador passa o controlo para o objeto do tipo Controller associado à ação. Faz isso chamando o método handleRequest desse objeto e passando-lhe a solicitação do cliente. É aqui que o programador executa as tarefas necessárias: pode ser necessário chamar classes de lógica de negócio ou classes de acesso a dados. No final do processamento, o objeto Controller devolve ao controlador o nome da vista que este deve enviar em resposta ao cliente.
- No seu ficheiro de configuração, o controlador irá encontrar o URL associado ao nome da vista que lhe foi solicitado para apresentar. Em seguida, envia a vista. A interação com o cliente está concluída.
3.7.2. O Modelo
É o mesmo que nas duas versões anteriores. Consiste nos arquivos Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.7.3. Configuração da aplicação
3.7.3.1. Arquitetura geral
A arquitetura geral do projeto Eclipse é a seguinte:

3.7.3.2. Configuração do acesso aos dados
Uma vez que a interface de acesso aos dados permanece inalterada, os ficheiros de configuração associados são os mesmos da versão anterior. Estão definidos em [WEB-INF/src]:

Na captura de ecrã acima, os ficheiros [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] são os das versões anteriores.
3.7.3.3. O diretório de arquivos
Em [WEB-INF/lib], encontrará as mesmas bibliotecas da versão anterior, exceto as do Struts, que já não são necessárias:

3.7.3.4. Configuração da aplicação
A aplicação é configurada através de três ficheiros: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] na pasta [WEB-INF]:

O ficheiro [web.xml] é o seguinte:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- application spring context loader -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- the servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- url mapping -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- entry document -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
O que diz este ficheiro?
- A página inicial da aplicação é [/vues/index.jsp] (welcome-file)
- Os pedidos de URL do tipo *.do serão redirecionados para o servlet [springwebarticles] (servlet-mapping)
- O servlet [springwebarticles] é uma instância da classe [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class) fornecida pelo Spring.
- O ouvinte [org.springframework.web.context.ContextLoaderListener] será iniciado quando a aplicação for iniciada. A sua principal função será instanciar os beans Spring definidos no ficheiro [applicationContext.xml]
O ficheiro [applicationContext.xml] é o seguinte:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- web application configuration-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
Alguns elementos são familiares, enquanto outros o são menos. Três beans serão instanciados durante a inicialização da aplicação:
- articlesDao: serviço que fornece acesso à camada [dao]
- articlesDomain: serviço que fornece acesso ao modelo
- config: um bean no qual reuniremos as informações que todos os clientes devem partilhar. Este bean desempenhará o papel tradicionalmente desempenhado pelo contexto da aplicação, mas com informações tipadas em vez de não tipadas.
O último ficheiro [springwebarticles.xml] define as ações aceites pela aplicação de uma forma muito semelhante à utilizada pelo ficheiro Struts [struts-config.xml]. O seu conteúdo é o seguinte:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- stock managers = controllers -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- application mapping-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- view manager -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- message file -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
O que diz este ficheiro de configuração?
- que o nosso controlador irá tratar das seguintes URLs:
| para exibir a lista de artigos |
| para exibir a lista de artigos |
| para exibir informações sobre um item específico |
| para comprar um item específico |
| para visualizar o carrinho de compras |
| para remover uma compra do carrinho de compras |
| para confirmar o carrinho de compras |
- As ações listadas acima correspondem, uma a uma, às ações tratadas pelo controlador nas versões anteriores. Para cada uma delas, é especificado o nome da classe responsável pelo seu tratamento. Tomemos o exemplo da ação [/panier.do]:
- ela deve ser tratada pelo bean [VoirPanierController]. Este nome é arbitrário. É simplesmente uma chave.
<prop key="/panier.do">VoirPanierController</prop>
- A chave [VoirPanierController] é o nome de um bean definido no mesmo ficheiro de configuração:
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
- O bean [VoirPanierController] define:
- a classe a instanciar [istia.st.articles.web.spring.VoirPanierController] para tratar da ação
- como instanciá-la. Aqui, o bean [config] definido por [applicationContext.xml] e instanciado no arranque da aplicação é fornecido como parâmetro. Isto será feito para todas as ações [Controller]. Assim, cada uma delas terá, num campo privado, o objeto [config] no qual encontrará todas as informações partilhadas entre todos os clientes.
- Como os nomes das visualizações devem ser resolvidos:
<!-- gestionnaire de vues -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
Tal como no Struts, a instância [Controller] que processa uma ação irá devolver, após o processamento, uma chave ao controlador Spring para indicar qual a vista que deve apresentar. Com base nesta chave, podem existir várias estratégias para gerar a vista associada à chave. A estratégia utilizada é aquela definida pelo bean [viewResolver]. Aqui, este bean está associado à classe [org.springframework.web.servlet.view.InternalResourceViewResolver] com vários parâmetros de inicialização. Sem entrar em detalhes, o bean [viewResolver] especifica aqui que, se a chave da vista for "XX", então a vista gerada será [/views/XX.jsp]. O tipo de vistas enviadas ao cliente pode ser alterado de várias formas:
- alterando a classe de implementação do bean [viewResolver]
- alterando os parâmetros de inicialização da classe de implementação
Assim, pode mudar de uma vista HTML para uma vista XML simplesmente alterando o valor do bean [viewResolver]
- o nome de um ficheiro de mensagens para a aplicação (messageSource). Aqui, o ficheiro existirá, mas estará vazio. Não será utilizado. Deve ser colocado no [ClassPath] da aplicação. Aqui, será colocado em [WEB-INF/classes]. No Eclipse, isto é conseguido colocando-o em [WEB-INF/src]:

3.7.4. As vistas JSP
As vistas JSP utilizadas serão as utilizadas pelo Struts. Nenhuma é modificada:

É criada uma única vista nova: redirpanier.jsp. É utilizada exclusivamente para redirecionar o cliente para a ação [/panier.do]. O seu código é o seguinte:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
Recomenda-se aos leitores que consultem as definições das várias vistas na versão Struts.
3.7.5. Processamento de ações
As classes necessárias para o processamento das várias ações foram agrupadas no pacote [istia.st.articles.web.spring]:

Vamos rever como funciona a aplicação Spring utilizando um exemplo:
- O utilizador solicita a URL [http://localhost:8080/springwebarticles/main.do]

O que aconteceu?
- Foi consultado o ficheiro [web.xml] da aplicação [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>
<!-- application spring context loader -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- the servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- url mapping -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- entry document -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
- Se esta fosse a primeira solicitação à aplicação, várias coisas seriam acionadas:
- o ouvinte [org.springframework.web.context.ContextLoaderListener] foi carregado
- ele analisou o ficheiro de configuração [applicationContext.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- web application configuration-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
- Os beans acima foram criados no contexto da aplicação
- O ficheiro [springwebarticles-servlet.xml] foi então utilizado:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- stock managers = controllers -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- application mapping-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- view manager -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- message file -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
- Os beans [Controller] definidos por este ficheiro também foram criados
- Tudo está agora pronto para processar o pedido do cliente. O pedido foi: [http://localhost:8080/springwebarticles]. Aqui, não estamos a solicitar um URL do contexto, mas sim o próprio contexto. Por isso, é a secção [welcome-file] do ficheiro [web.xml] que é utilizada.
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
- A vista [index.jsp] é a seguinte:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
- O cliente é, portanto, solicitado a redirecionar para o URL [http://localhost:8080/springwebarticles/main.do]. E assim o faz.
- O controlador Spring recebe então um novo pedido. Utiliza o seu ficheiro [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>
<!-- stock managers = controllers -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
....
</bean>
<!-- application mapping-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
</beans>
|
- Este ficheiro indica que a ação [/main.do] deve ser tratada pelo bean [ListController].
- O pedido do cliente é passado para o método [handleRequest] do bean [ListController]. Este método executa a sua tarefa e devolve a chave da vista a ser apresentada ao controlador. Aqui, se tudo correr bem, esta chave será [list].
- O controlador Spring utiliza o bean [viewResolver] do ficheiro de configuração [springwebarticles-servlet.xml] para determinar a vista associada a esta chave. Neste caso, será a vista [/vues/liste.jsp]
- A vista [/vues/liste.jsp] é enviada ao cliente
3.7.6. Inicialização da aplicação Spring
Mencionámos que, quando a aplicação é iniciada, os beans no ficheiro [applicationContext.xml] são instanciados:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- data access class -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- the business class -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- web application configuration-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
Estamos familiarizados com os beans [articlesDao, articlesDomain], mas não com o bean [config]. Este bean é definido pela seguinte classe Java:
| package istia.st.articles.web.spring;
import java.util.Hashtable;
import istia.st.articles.domain.IArticlesDomain;
/**
* @author ST - ISTIA
*/
public class Config {
// private fields
private IArticlesDomain articlesDomain = null;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private final String lienActionListe = "Liste des articles";
private final String lienActionPanier = "Voir le panier";
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// getters-setters
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
// init web application
public void init() {
// memorize certain application urls
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// it's over
return;
}
}
|
Esta classe desempenha a mesma função que o método [init] de um servlet de aplicação web. Inicializa a aplicação. Aqui, isso é feito da seguinte forma:
- porque o bean [config] está definido da seguinte forma em [applicationContext.xml]:
<!-- la configuration de l'application web-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
Quando é criado, o seu campo privado [articlesDomain] é inicializado
- depois, devido ao atributo [init-method="init"] do bean acima, o método [init] da classe associada ao bean é executado. Aqui, ele inicializa os três dicionários [hActionListe, hActionPanier, hActionValidationPanier] utilizados para gerar os três possíveis links de menu oferecidos ao utilizador.
- São criados acessores públicos para tornar estes campos privados acessíveis a instâncias do tipo [Controller] que irão tratar das ações.
3.7.7. As ações [Controller] da aplicação Spring
3.7.7.1. Introdução
Cada ação Spring será objeto de uma classe do tipo [Controller]. Na versão Struts, cada ação era objeto de uma classe do tipo [Action]. A escrita da classe [Controller] consiste, na maioria das vezes, em:
- copiar e colar a classe [Action] que foi utilizada na versão Struts
- adaptar o código às convenções do Spring
3.7.7.2. main.do, liste.do
Estas duas ações são idênticas e estão definidas no [springwebarticles-servlet.xml] da seguinte forma:
| <!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
...
</props>
</property>
</bean>
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Estão associadas à classe [istia.st.articles.web.spring.ListController], que iremos discutir em detalhe em breve. Quando uma destas ações é executada num navegador, obtém-se o seguinte resultado:

O código da classe [istia.st.articles.web.spring.ListController] é o seguinte:
| package istia.st.articles.web.spring;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ListController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the list of items is requested
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// we memorize the error
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// the error page is displayed
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// send error view
return new ModelAndView("erreurs");
}
// displays the list of items
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// send view
return new ModelAndView("liste");
}
}
|
Comentários:
- A classe tem um campo privado [config]. Este campo foi inicializado pelo Spring quando o bean [ListController] foi instanciado:
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
Como mostrado acima, o campo [config] do [ListController] é inicializado com o bean [config]. O que é isto? É o bean [config] definido em [applicationContext.xml], ou seja, uma instância de [istia.st.articles.web.spring.Config] descrita acima.
- Escrever o código para uma classe [Controller] envolve essencialmente escrever o código para o seu método [handleRequest]
- Solicitamos a lista de artigos ao modelo. Esta é acessível através de [config.getArticlesDomain()]. Se ocorrer uma exceção, a vista [ERRORS] é renderizada. O resultado devolvido por [handleRequest] deve ser do tipo [ModelAndView]. Esta classe pode ser instanciada de várias formas. Aqui, e isto será sempre o caso, criamos uma instância de [ModelAndView] passando-lhe a chave da vista a ser exibida. Recorde-se que, com base na configuração do bean [viewResolver], solicitar a vista com a chave XX resultará no envio da vista [/vues/XX.jsp].
| // on demande la liste des articles
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// on mémorise l'erreur
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// on affiche la page des erreurs
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// envoyer la vue erreurs
return new ModelAndView("erreurs");
}
|
- Se não houver erros, a vista [LIST] é enviada:
| // on affiche la liste des articles
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// envoyer la vue
return new ModelAndView("liste");
|
3.7.7.3. infos.do
Esta ação é utilizada para fornecer informações sobre um dos itens apresentados na vista [LIST]:
Esta ação é definida em [springwebarticles-servlet.xml] da seguinte forma:
| <!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/infos.do">InfosController</prop>
...
</props>
</property>
</bean>
...
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
O código para a classe [InfosController] é o seguinte:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class InfosController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// error list
ArrayList erreurs = new ArrayList();
// retrieve the requested id
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// the id key item is requested
Article article = null;
try {
article = config.getArticlesDomain().getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
if (article == null) {
// not normal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// put the article in the session
request.getSession().setAttribute("article", article);
// the info page is displayed
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("infos");
}
}
|
Comentários:
- O método [handleRequest] recupera o parâmetro [id], que normalmente deve estar presente na URL. A URL deve, de facto, ter o formato [/infos.do?id=X]. São realizadas várias verificações para confirmar a presença e a validade do parâmetro [id]. Se houver algum problema, é apresentada a vista [ERRORS].
- Se [id] for válido, o item correspondente é solicitado à camada [domain]. Se isto lançar uma exceção ou se o item não for encontrado, a vista [ERROR] é enviada novamente.
- Se tudo correr bem, o item recuperado é armazenado na sessão. Este é um ponto discutível. Aqui, assumimos que o cliente poderá comprar este item. Se o fizer, iremos recuperá-lo da sessão em vez de o solicitar novamente à camada [domain].
- Por fim, a vista [INFO] é apresentada.
3.7.7.4. purchase.do
Esta ação é utilizada para comprar o item apresentado pela vista [INFOS] anterior:

Se analisarmos o código HTML desta vista, vemos que a tag <form> está definida da seguinte forma:
<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>
Podemos ver que o formulário é enviado para o controlador com a ação [achat.do].
Esta ação está configurada da seguinte forma no ficheiro [springwebarticles-servlet.xml]:
<!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/achat.do">AchatController</prop>
...
</props>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
O código para a classe [AchatController] é o seguinte:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class AchatController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// the quantity purchased is recovered
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// wrong qty
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
// retrieve the client session
HttpSession session = request.getSession();
// we retrieve the session item
Article article = (Article) session.getAttribute("article");
// session expired?
if (article == null) {
// the error page is displayed
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// create the new purchase
Achat achat = new Achat(article, qté);
// the purchase is added to the customer's basket
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// we return to the list of items
return new ModelAndView("index");
}
}
|
Comentários:
- Vamos rever o formato do formulário enviado ao 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>
- Existem dois parâmetros no pedido: [id]: número do artigo, [qte]: quantidade adquirida.
- A presença e a validade do parâmetro [qte] são verificadas. Se este parâmetro for considerado incorreto, a vista [INFOS] é devolvida ao utilizador juntamente com uma mensagem de erro:
| // la liste des erreurs sur cette action
ArrayList erreurs = new ArrayList();
// on récupère la quantité achetée
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// qté erronée
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
|
- O item adquirido é recuperado da sessão. A sessão pode ter expirado. Neste caso, é enviada a vista [ERRORS]:
| // on récupère la session du client
HttpSession session = request.getSession();
// on récupère l'article mis en session
Article article = (Article) session.getAttribute("article");
// session expirée ?
if (article == null) {
// on affiche la page des erreurs
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
|
- Se a sessão não tiver expirado, o item é adicionado ao carrinho, que também é recuperado da sessão:
| // on crée le nouvel achat
Achat achat = new Achat(article, qté);
// on ajoute l'achat au panier du client
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- Por fim, enviamos a vista [LIST]:
// on revient à la liste des articles
return new ModelAndView("index");
- Acima, enviamos a vista [/views/index.jsp]. Sabemos que esta vista instrui o navegador do cliente a redirecionar para a URL [/main.do]. É este redirecionamento que irá apresentar a lista de itens.
3.7.7.5. cart.do
Esta ação é utilizada para apresentar todas as compras do cliente. Está disponível através do menu:

O código HTML associado ao link acima é o seguinte:
<a href="panier.do">Voir le panier</a>
A página apresentada por este link é a seguinte:

Esta ação está configurada da seguinte forma em [springwebarticles-servlet.xml]:
| <!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/panier.do">VoirPanierController</prop>
...
</props>
</property>
</bean>
...
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
O código para a classe [VoirPanierController] é o seguinte:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class VoirPanierController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the basket is displayed
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// empty basket
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("paniervide");
} else {
// there's something in the basket
request.setAttribute("actions", new Hashtable[] {
config.getHActionListe(), config.getHActionValidationPanier() });
return new ModelAndView("panier");
}
}
}
|
Comentários:
- O carrinho de compras é recuperado da sessão onde normalmente é armazenado. A sessão pode ter expirado; nesse caso, não há carrinho de compras. Não tratamos isto como um erro, mas simplesmente assumimos que o carrinho de compras está vazio.
- Se o carrinho estiver vazio, é apresentada a vista [CARRINHO VAZIO]
- caso contrário, é apresentada a vista [CARRINHO]
3.7.7.6. removePurchase.do
Esta ação é utilizada para remover uma compra do carrinho:

Se analisarmos o código HTML do link acima, vemos o seguinte:
<a href="retirerachat.do?id=3">Retirer</a>
A ação [retirerachat.do] recebe, portanto, como parâmetro, o ID do artigo a ser removido do carrinho. Esta ação está configurada da seguinte forma no ficheiro [springwebarticles-servlet.xml]:
| <!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/retirerachat.do">RetirerAchatController</prop>
</props>
</property>
</bean>
...
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
O código da classe [RetirerAchatController] é o seguinte:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class RetirerAchatController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// we pick up the basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// retrieve the id of the item to be removed
String strId = request.getParameter("id");
// anything?
if (strId == null) {
// not normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// transform strId into an integer
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// not normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// we remove the purchase
panier.enlever(id);
// the basket is displayed again
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
}
}
|
Comentários:
- O código verifica a presença e a validade do parâmetro [id]. Se estiver incorreto, é enviada a visualização [ERRORS].
- Caso contrário, o artigo é removido do carrinho:
// on enlève l'achat
panier.enlever(id);
- depois, o carrinho é recarregado:
// on affiche de nouveau le panier
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
Vamos rever o código da vista [/vues/redirpanier.jsp]:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
Podemos ver que o cliente será redirecionado para a ação [/panier.do]. Isto já foi descrito. Apresentará a vista [PANIER] ou [PANIERVIDE], dependendo do estado do carrinho de compras.
3.7.7.7. confirmcart.do
Esta ação é utilizada para confirmar as compras do cliente. Na prática, isto envolve uma única ação: os níveis de stock dos artigos comprados são reduzidos nas quantidades adquiridas na base de dados. Esta ação provém do seguinte menu:

O código HTML para o link [Confirmar Carrinho] é o seguinte:
<a href="validerpanier.do">Valider le panier</a>
Quando este link é clicado, os níveis de stock são atualizados e a lista de artigos é apresentada novamente.
Esta ação está configurada da seguinte forma no ficheiro [springwebarticles-servlet.xml]:
| <!-- le mapping de l'application-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
...
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
O código para a classe [ValiderPanierController] é o seguinte:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ValiderPanierController implements Controller {
// web app configuration
Config config;
public void setConfig(Config config) {
this.config = config;
}
// query processing
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// the list of errors on this action
ArrayList erreurs = new ArrayList();
// the buyer has confirmed his basket
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expired
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// validate basket
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// not normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// recover any errors
erreurs = config.getArticlesDomain().getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe(), config.getHActionPanier() });
return new ModelAndView("erreurs");
}
// everything looks OK - the item list is displayed
return new ModelAndView("index");
}
}
|
Comentários:
- Recuperamos o carrinho de compras da sessão. Se a sessão tiver expirado, exibimos a vista [ERRORS]:
| // la liste des erreurs sur cette action
ArrayList erreurs = new ArrayList();
// l'acheteur a confirmé son panier
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// session expirée
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
|
- Processamos as compras no carrinho. Podem ocorrer erros se os níveis de stock forem insuficientes para satisfazer as compras. Neste caso, apresentamos a vista [ERRORS]:
| // on valide le panier
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// pas normal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// on récupère les éventuelles erreurs
erreurs = config.getArticlesDomain().getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe(), config.getHActionPanier() });
return new ModelAndView("erreurs");
}
|
- Se tudo correu bem, voltamos a apresentar a lista de itens:
// tout semble OK - on affiche la liste des articles
return new ModelAndView("index");
Sabemos que a vista [/vues/index.jsp] redireciona o cliente para a ação [/main.do]. Esta ação irá exibir a vista [LISTE].