3. Artigo 2.º - Exemplos de arquiteturas web de três camadas
Objetivos do artigo:
- arquiteturas de três camadas
- arquitetura web básica MVC
- 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 administrar graficamente o SGBD Firebird.
- Tomcat: http://jakarta.apache.org/tomcat/
- Plugin do 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 adquiridos em documentos que escrevi. Nesse caso, faço referência aos mesmos. É evidente que se trata apenas de uma sugestão e que o leitor pode utilizar os seus documentos 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 deste documento têm origem num livro lido durante o verão de 2004, uma magnífica obra de Rod Johnson: J2EE «Development without EJB», publicado pela editora Wrox.
3.1. A aplicação webarticles
Pretendemos apresentar aqui alguns elementos de uma aplicação web de comércio eletrónico. Esta permitirá aos clientes da web
- consultar uma lista de artigos provenientes de uma base de dados
- de colocar alguns deles num cesto de compras eletrónico
- e de confirmar o cesto. Esta confirmação terá como único efeito atualizar, na base de dados, os stocks dos artigos comprados.
As diferentes vistas apresentadas ao utilizador serão as seguintes:
- a vista [LISTE], que apresenta uma lista dos artigos à venda

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

- a vista [PANIER], que apresenta o conteúdo do cesto de compras do cliente

- a vista [PANIERVIDE], para o caso de o cesto de compras do cliente estar vazio

- a vista [ERREURS], que sinaliza qualquer erro da aplicação

3.2. Arquitetura geral da aplicação
Pretende-se construir uma aplicação com a seguinte estrutura de três camadas:
- as três camadas são tornadas independentes graças à utilização de interfaces Java
- A integração das diferentes camadas é realizada pelo Spring
- cada camada é constituída por pacotes separados: web (camada de interface do utilizador), domain (camada de negócio) 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 pretendemos construir de várias formas:
- utilizando uma tecnologia clássica de servlet controlador - páginas JSP
- utilizando a tecnologia Struts MVC
- utilizando a tecnologia Spring MVC
Em todos os casos, a aplicação seguirá uma arquitetura MVC (Modelo - Vista - Controlador). Se retomarmos o esquema em camadas acima, a arquitetura MVC integra-se nele da seguinte forma:
O processamento de um pedido de um cliente decorre de acordo com as seguintes etapas:
- o cliente envia um pedido ao controlador. Este controlador é um servlet que recebe todos os pedidos dos clientes. É a porta de entrada da aplicação. É o C de MVC.
- o controlador processa essa solicitação. Para tal, pode necessitar da ajuda da camada de negócio, a que se chama modelo M na estrutura MVC.
- O controlador recebe uma resposta da camada de negócio. A solicitação do cliente foi processada. Esta pode dar origem a várias respostas possíveis. Um exemplo clássico é
- uma página de erros, caso a solicitação não tenha podido ser processada corretamente
- uma página de confirmação, caso contrário
- o controlador escolhe a resposta (= vista) a enviar ao cliente. Esta é, na maioria das vezes, uma página que contém elementos dinâmicos. O controlador fornece esses elementos à vista.
- A vista é enviada ao cliente. É o V de MVC.
3.3. O modelo
Analisamos aqui o M de MVC. O modelo é aqui constituído pelos seguintes elementos:
- as classes de negócio
- as classes de acesso aos dados
- a base de dados
3.3.1. A base de dados
A base de dados contém apenas uma tabela denominada ARTICLES. Esta foi gerada com 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
);
/* restrições */
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<>'');
/* chave primária */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
|
| chave primária que identifica um artigo de forma única |
| nome do artigo |
| o seu preço |
| stock atual |
| o nível de stock abaixo do qual deve ser efetuada uma encomenda de reabastecimento |
Nos testes que se seguem, foi utilizada uma base de dados [Firebird]. [Firebird] é uma versão «open source» de SGBD. O controlador JDBC [firebirdsql-full.jar] está localizado na pasta [WEB-INF/lib] da aplicação web.
3.3.2. Os pacotes do modelo
O modelo M é aqui fornecido sob a forma de três arquivos:
- istia.st.articles.dao: contém as classes de acesso aos dados da 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]
archive | contenu | rôle |
istia.st.articles.dao | - contém o pacote [istia.st.articles.dao], que, por sua vez, contém os seguintes elementos: - [IArticlesDao]: a interface de acesso à camada DAO. É a única interface que a camada [domain] vê. Não vê nenhuma outra. - [Article]: classe que define um artigo - [ArticlesDaoSqlMap]: classe de implementação da interface [IArticlesDao] com a ferramenta SqlMap | camada de acesso aos dados – encontra-se inteiramente na 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 de acesso à camada [domain]. É a única interface que a camada web vê. Não vê nenhuma outra. - [AchatsArticles]: uma classe que implementa [IArticlesDomain] - [Achat]: classe que representa a compra de um cliente - [Panier]: classe que representa o conjunto de compras de um cliente | representa o modelo de compras na Web — encontra-se inteiramente na camada [domain] da arquitetura de três 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 do tipo [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) {
// inicialização dos atributos de instância
setId(id);
setNom(nom);
setPrix(prix);
setStockActuel(stockActuel);
setStockMinimum(stockMinimum);
}
// getters - setters
public int getId() {
return id;
}
public void setId(int id) {
// ID válido?
if (id < 0)
throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
// nome válido?
if(nom==null || nom.trim().equals("")){
throw new UncheckedAccessArticlesException("Le nom est [null] ou vide");
}
this.nom = nom;
}
public double getPrix() {
return prix;
}
public void setPrix(double prix) {
// preço válido?
if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
this.prix = prix;
}
public int getStockActuel() {
return stockActuel;
}
public void setStockActuel(int stockActuel) {
// stock válido?
if (stockActuel < 0)
throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
this.stockActuel = stockActuel;
}
public int getStockMinimum() {
return stockMinimum;
}
public void setStockMinimum(int stockMinimum) {
// stock válido?
if (stockMinimum < 0)
throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
this.stockMinimum = stockMinimum;
}
public String toString() {
return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
+ stockMinimum + "]";
}
}
|
Esta classe oferece:
- um construtor que permite definir as 5 informações de um artigo
- métodos de acesso, frequentemente denominados getters/setters, que servem para ler e escrever as 5 informações. Os nomes destes métodos seguem a norma JavaBean. É comum utilizar objetos JavaBean na camada DAO para fazer a interface com os dados do SGBD.
- uma verificação dos dados inseridos no artigo. Em caso de dados errados, é lançada uma exceção.
- um método toString que permite obter o valor de um artigo sob a forma de uma cadeia de caracteres. Isto é frequentemente útil para a depuração de 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);
}
|
A função dos diferentes métodos da interface é a seguinte:
| retorna todos os artigos da tabela ARTICLES numa lista de objetos [Article] |
| esvazia a tabela ARTICLES |
| retorna o objeto [Article] identificado pela sua chave primária |
| permite adicionar um artigo à tabela ARTICLES |
| permite modificar um artigo da tabela [ARTICLES] |
| permite eliminar um artigo da tabela [ARTICLES] |
| permite alterar o stock de um artigo da tabela [ARTICLES] |
A interface disponibiliza aos programas clientes um certo número de métodos definidos apenas pelas suas assinaturas. Não se ocupa da forma como esses métodos serão efetivamente implementados. Isto confere flexibilidade a uma aplicação. O programa cliente efetua as suas chamadas a uma interface e não a uma implementação específica da mesma.
A escolha de uma implementação específica será feita através de um ficheiro de configuração do Spring. Propomos aqui implementar a interface IArticlesDao utilizando um produto de código aberto denominado 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;
// Importações
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;
public class ArticlesDaoSqlMap implements IArticlesDao {
// Campos
private SqlMapClient sqlMap;
// Construtores
public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }
// Métodos
public SqlMapClient getSqlMap() {}
public void setSqlMap(SqlMapClient sqlMap) { }
public synchronized List getAllArticles() {}
public synchronized int ajouteArticle(Article unArticle) {}
public synchronized int supprimeArticle(int idArticle) {}
public synchronized int modifieArticle(Article unArticle) {}
public synchronized Article getArticleById(int idArticle) {}
public synchronized void clearAllArticles() { }
public synchronized int changerStockArticle(int idArticle, int mouvement) {}
}
|
Todos os métodos de acesso aos dados foram sincronizados para evitar problemas de acesso simultâneo à fonte de dados. Em qualquer momento, apenas um thread tem acesso a um determinado método.
A classe [ArticlesDaoSqlMap] utiliza a ferramenta [Ibatis SqlMap]. O objetivo desta ferramenta é permitir extrair o código SQL de acesso aos dados do código Java. Este é então colocado num ficheiro de configuração. Teremos oportunidade de voltar a este assunto. Para ser criada, a classe [ArticlesDaoSqlMap] necessita de 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, no qual se encontram os artigos
- gerir um conjunto de ligações
- gerir as transações
No nosso exemplo, chamar-se-á [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] acima referido permite definir como criar uma instância da classe [istia.st.articles.dao.Article] a partir de uma linha da tabela [ARTICLES] do SGBD. Define igualmente as consultas SQL que permitirão à camada [dao] obter os 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">
<!-- um alias para a classe istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- o mapeamento O RM: linha da tabela ARTICLES - instância da classe Artigo -->
<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>
<!-- a consulta SQL para obter todos os artigos -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- a consulta SQL para eliminar todos os artigos -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- a consulta SQL para inserir um artigo -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#(id#,#nome#,#preço#,#stockatual#,#stockmínimo#)
</statement>
<!-- a consulta SQL para eliminar um artigo específico -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- a consulta SQL para alterar um determinado artigo -->
<statement id="modifyArticle">
update ARTICLES set nom=#nome#,
prix=#preço#,stockatual=#stockatual#,stockmínimo=#stockmínimo# onde
id=#id#
</statement>
<!-- a consulta SQL para obter um determinado artigo -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- a consulta SQL para alterar o stock de um determinado artigo -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#movimento#
where id=#id# e stockActuel+#movimento#>=0
</statement>
</sqlMap>
|
O código do pacote [dao] encontra-se em anexo.
3.3.4. O pacote [istia.st.articles.domain]
A interface [IArticlesDomain] separa a camada [métier] da camada [web]. Esta última acede à camada [métier/domain] através desta interface, sem se preocupar com a classe que a implementa efetivamente. A interface define as seguintes ações para o acesso à camada de negócio:
| package istia.st.articles.domain;
// Importações
import java.util.ArrayList;
import java.util.List;
public abstract interface IArticlesDomain {
// Métodos
void acheter(Panier panier);
List getAllArticles();
Article getArticleById(int idArticle);
ArrayList getErreurs();
}
|
| retorna a lista de objetos [Article] a apresentar ao cliente |
Article getArticleById(int idArticle)
| retorna o objeto [Article] identificado por [idArticle] |
void acheter(Panier panier)
| valida o cesto de compras do cliente, deduzindo dos stocks dos artigos comprados a 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 [AchatsArticles]:
| package istia.st.articles.domain;
// Importações
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.List;
public class AchatsArticles implements IArticlesDomain {
// Campos
private IArticlesDao articlesDao;
private ArrayList erreurs;
// Construtores
public AchatsArticles(IArticlesDao articlesDao) { }
// Métodos
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 aos dados fornecido pela camada de acesso aos dados |
| a lista de eventuais erros |
Para criar uma instância da classe, é necessário fornecer o objeto que permite o acesso aos dados do SGBD:
public AchatsArticles(IArticlesDao articlesDao)
| construtor |
A classe [Achat] representa uma compra do cliente:
| package istia.st.articles.domain;
public class Achat {
// Campos
private Article article;
private int qte;
// Construtores
public Achat(Article article, int qte) { }
// Métodos
public double getTotal() {}
public Article getArticle() {}
public void setArticle(Article article) { }
public int getQte() {}
public void setQte() { }
public String toString() {}
}
|
A classe [Achat] é uma JavaBean com os seguintes campos e métodos:
| o artigo adquirido |
| a quantidade adquirida |
| indica o valor da compra |
| cadeia de identificação do objeto |
A classe [Panier] representa o conjunto de compras do cliente:
| package istia.st.articles.domain;
// Importações
import java.util.ArrayList;
public class Panier {
// Campos
private ArrayList achats;
// Construtores
public Panier() { }
// Métodos
public ArrayList getAchats() {}
public void ajouter(Achat unAchat) { }
public void enlever(int idAchat) { }
public double getTotal() {}
public String toString() { }
}
|
A classe [Panier] é uma JavaBean com os seguintes campos e métodos:
| a lista de compras do cliente — lista de objetos do tipo [Achat] |
void ajouter(Achat unAchat)
| adiciona uma compra à lista de compras |
void enlever(int idArticle)
| retira a compra do artigo idArticle |
| calcula o valor total das compras |
| retorna a cadeia de identificação do carrinho |
| retorna a lista de compras |
O código do pacote [domain] encontra-se em anexo.
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 de acesso à 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. Testes do modelo
O modelo M foi testado no Eclipse com a seguinte configuração:

Comentários:
- no [WEB-INF/lib] encontram-se:
- os ficheiros necessários para a ferramenta [ibatis SqlMap], responsável pelo acesso ao SGBD Firebird: ibatis-*.jar
- o arquivo necessário para a ferramenta [spring]: spring.jar
- o controlador JDBC do SGBD [Firebird]: firebirdsql-full.jar
- os ficheiros necessários para os registos: log4-*.jar, commons-logging.jar
- os três ficheiros do modelo testado: istia.st.articles.*.jar
- o ficheiro necessário para a ferramenta de teste [junit]
- no [WEB-INF/src] encontram-se os ficheiros de configuração que serão automaticamente copiados para o [WEB-INF/classes] pelo Eclipse:
- os ficheiros de configuração da ferramenta [sqlmap]: sqlmap-config-firebird.xml, articles.xml
- os da ferramenta [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
- o ficheiro de configuração da ferramenta [log4j]: log4j.properties
- no pacote [istia.st.articles.tests], encontram-se as classes de teste do modelo
3.3.6.1. Testes da camada [dao]
A classe de teste JUnit da camada [dao] é a seguinte. A sua leitura permite compreender como são utilizados os métodos da interface [IArticlesDao]:
| package istia.st.articles.tests.dao;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.dao.Article;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// teste da classe ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {
// uma instância da classe testada
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// obtém uma instância de acesso aos dados
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
public void testGetAllArticles() {
// exibe os artigos
listArticles();
}
public void testClearAllArticles() {
// esvazia a tabela de artigos
articlesDao.clearAllArticles();
// lê a tabela ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
}
public void testAjouteArticle() {
// elimina o conteúdo de ARTICLES
articlesDao.clearAllArticles();
// lê a tabela ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// inserção
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// lê a tabela ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
//exibe-a
listArticles();
}
public void testSupprimeArticle() {
// eliminação do conteúdo de ARTICLES
articlesDao.clearAllArticles();
// lê a tabela ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// inserção
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// lê a tabela ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// eliminação
articlesDao.supprimeArticle(4);
// lê a tabela ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(1, articles.size());
// exibe a tabela
listArticles();
}
public void testModifieArticle() {
// eliminação do conteúdo de ARTICLES
articlesDao.clearAllArticles();
// lê a tabela ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// inserção
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// lê a tabela ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
// alteração
articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
// getById
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getPrix(), 44, 1e-6);
// exibe a tabela
listArticles();
}
public void testGetArticleById() {
// eliminação do conteúdo de ARTICLES
articlesDao.clearAllArticles();
// lê a tabela ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// inserção
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// lê a tabela ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
private void listArticles() {
// lê a tabela ARTICLES
List articles = articlesDao.getAllArticles();
// são apresentados os artigos lidos
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
public void testChangerStockArticle() throws InterruptedException {
// eliminação do conteúdo de ARTICLES
articlesDao.clearAllArticles();
// inserção
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);
// criação de 100 threads para atualização do stock do artigo 3
Thread[] taches = new Thread[100];
for (int i = 0; i < taches.length; i++) {
taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
taches[i].start();
}
// aguarda-se a conclusão dos threads
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// recuperar o artigo 3 e verificar o seu stock
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
assertEquals(1, unArticle.getStockActuel());
// alteração do stock do artigo 4
boolean erreur = false;
int nbLignes = articlesDao.changerStockArticle(4, -100);
assertEquals(0, nbLignes);
// exibe a tabela
listArticles();
}
}
|
Comentários:
- a classe de teste armazena, através do seu método setUp, uma instância da classe a testar:
| // uma instância da classe testada
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// recupera uma instância de acesso aos dados
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
|
- O objeto a testar é fornecido por [Spring]. Acima, solicita-se o bean 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>
<!-- a classe de acesso aos dados -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
</beans>
|
Como se pode ver acima, o bean [articlesDao] é uma instância da classe [istia.st.articles.dao.ArticlesDaoSqlMap]. Esta classe possui um construtor que espera, como parâmetro, o nome do ficheiro de configuração da ferramenta [SqlMap]. Esse nome é aqui fornecido. Trata-se de [sqlmap-config-firebird.xml]. Este último já foi descrito. Fornece todas as informações necessárias para aceder aos dados do SGBD.
O método [testChangerStockArticle] cria 100 threads encarregadas de diminuir o stock de um determinado artigo. Trata-se, neste caso, de testar os acessos simultâneos 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 por atualizar o 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() {
// acompanhamento
System.out.println(name + " lancé");
// alteração do stock do artigo 3
articlesDao.changerStockArticle(3, -1);
// acompanhamento
System.out.println(name + " terminé");
}
}
|
- a classe acima diminui em 1 o stock do artigo n.º 3
3.3.6.2. Testes da camada [domain]
A classe de teste JUnit da 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;
// teste da classe ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {
// uma instância da classe de acesso ao domínio
private IArticlesDomain articlesDomain;
// uma instância da classe de acesso aos dados
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// recupera uma instância de acesso ao domínio
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// recupera uma instância de acesso aos dados
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
// recuperação de um artigo específico
public void testGetArticleById() {
// eliminação do conteúdo de ARTICLES
articlesDao.clearAllArticles();
// lê a tabela ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// inserção
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// lê a tabela ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDomain.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
// exibição no ecrã
private void listArticles() {
// lê a tabela ARTICLES
List articles = articlesDomain.getAllArticles();
// são apresentados os artigos lidos
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
// compras de artigos
public void testAchatPanier(){
// eliminação do conteúdo de ARTICLES
articlesDao.clearAllArticles();
// lê a tabela ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// inserção
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// lê a tabela ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// criação de um cesto com duas compras
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// verificações
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// validação do cesto de compras
articlesDomain.acheter(panier);
// verificações
assertEquals(0,articlesDomain.getErreurs().size());
assertEquals(0,panier.getAchats().size());
// procurar o artigo n.º 3
article3=articlesDomain.getArticleById(3);
assertEquals(20,article3.getStockActuel());
// procurar o artigo n.º 4
article4=articlesDomain.getArticleById(4);
assertEquals(30,article4.getStockActuel());
// novo cesto de compras
panier.ajouter(new Achat(article3,100));
// confirmação do carrinho
articlesDomain.acheter(panier);
// verificações - comprámos em excesso
// deve ter havido um erro
assertEquals(1,articlesDomain.getErreurs().size());
// procurar artigo n.º 3
article3=articlesDomain.getArticleById(3);
// o stock não deve ter mudado
assertEquals(20,article3.getStockActuel());
}
// remover compras
public void testRetirerAchats(){
// eliminação do conteúdo de ARTICLES
articlesDao.clearAllArticles();
// lê a tabela ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// inserção
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// lê a tabela ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// criação de um cesto com duas compras
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// verificações
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// adicionar um artigo já comprado
panier.ajouter(new Achat(article3,10));
// verificações
// o total deve ser alterado para 1000
assertEquals(1000.0,panier.getTotal(),1e-6);
// continuam a ser 2 artigos no cesto
assertEquals(2,panier.getAchats().size());
// a quantidade do artigo 3 deve ter passado para 20
Achat achat=(Achat)panier.getAchats().get(0);
assertEquals(20,achat.getQte());
// retira-se o artigo 3 do carrinho
panier.enlever(3);
// verificações
// o total deve ter passado para 400
assertEquals(400.0,panier.getTotal(),1e-6);
// apenas 1 artigo no cesto
assertEquals(1,panier.getAchats().size());
// deve ser o artigo n.º 4
assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
}
}
|
Comentários:
- a classe de teste armazena, através do seu método setUp, uma instância da classe a testar, 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 ter acesso à camada [dao], que nem sequer deveria conhecer. Neste caso, ignorámos esta «ética» que, para ser respeitada, nos teria obrigado a criar novos métodos na nossa interface [IArticlesDomain].
| // uma instância da classe de acesso ao domínio
private IArticlesDomain articlesDomain;
// uma instância da classe de acesso aos dados
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// recupera uma instância de acesso ao domínio
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// recupera uma instância de acesso aos dados
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
|
- o objeto a testar é fornecido por [Spring]. Acima, solicitamos o bean 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>
<!-- a classe de acesso aos dados -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- a classe de negócio -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Como se pode ver 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 de acesso à camada [dao] do tipo [IArticlesDao]. Aqui, o ficheiro de configuração indica que esse objeto é o bean denominado [articlesDao]. Isto obriga o Spring a instanciar esse bean. A instanciação do bean [articlesDao] foi explicada anteriormente. Assim, no final, 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 provocadas pela primeira chamada ao Spring:
| // recupera uma instância de acesso ao domínio
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
|
Recupera-se então o bean [articlesDomain]. Na segunda chamada ao Spring:
| // recupera uma instância de acesso aos dados
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
|
o [Spring] limita-se a devolver uma referência ao bean [articlesDao], que já tinha sido criado na chamada anterior. Este é o princípio do singleton. Se solicitarmos 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, pretendemos 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 mesmo descrito anteriormente. É-nos fornecido em três arquivos [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Temos de escrever o controlador C e as vistas V.
Consideremos, em primeiro lugar, um método clássico, aquele em que:
- o controlador C é assegurado por um único servlet
- as vistas V são asseguradas 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:
| as classes de negócio, as classes de acesso aos dados e a base de dados |
| as páginas JSP |
| o servlet de processamento de pedidos dos clientes |
3.5.1. O modelo
Já foi apresentado anteriormente. É constituído pelos ficheiros Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.5.2. As vistas
As vistas correspondem às que foram apresentadas no início do documento:
| liste.jsp | As vistas estão reunidas na pasta [vues] da aplicação  |
| infos.jsp |
| panier.jsp |
| paniervide.jsp |
| erreurs.jsp |
3.5.3. O controlador
O controlador será constituído por um único servlet denominado [WebArticles]. Este irá processar os diferentes pedidos dos clientes. Estes serão formalizados pela presença de um parâmetro [action] na solicitação HTTP do cliente:
| significado | ação do controlador | respostas possíveis |
| o cliente pretende a lista dos artigos | - solicita a lista de artigos à camada de negócio | - [LISTE] - [ERREURS] |
| O cliente solicita informações sobre um dos artigos apresentados na vista [LISTE] | - solicita o artigo à camada de negócios | - [INFOS] - [ERREURS] |
| o cliente compra um artigo | - solicita o artigo à camada de negócio e adiciona-o ao cesto de compras do cliente | - [INFOS] se houver erro na quantidade - [LISTE] se não houver erro |
| o cliente pretende eliminar um compra do seu carrinho | - recupera o carrinho da sessão e altera-o | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| o cliente pretende visualizar o seu cesto | - recupera o carrinho da sessão | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| o cliente concluiu as suas compras e passa para a fase de pagamento | - atualiza na base de dados os stocks dos artigos adquiridos - esvazia o cesto de compras do cliente dos artigos cuja a compra já foi confirmada | - [LISTE] - [ERREURS] |
3.5.4. Configuração da aplicação
Procuraremos configurar a aplicação de forma a torná-la o mais flexível possível face a alterações como:
- a alteração dos URLs das diferentes vistas
- a alteração das classes que implementam as interfaces [IArticlesDao] e [IArticlesDomain]
- a alteração do SGBD, da base de dados e da tabela de artigos
3.5.5. Alterações nas URLs
Os nomes das URLs das vistas serão colocados no ficheiro de configuração da aplicação [web.xml], 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>
|
Encontram-se no ficheiro [web.xml]
- as URLs das diferentes vistas da aplicação
- o nome [springConfigFileName] do ficheiro de configuração do Spring que permitirá a criação dos objetos singleton de acesso às camadas de negócio e DAO
- a vista [/vues/index.jsp] que será apresentada quando a URL solicitada pelo cliente for /<context>, em que <context> é o contexto da aplicação
3.5.6. A alteração das classes de implementação das interfaces
De acordo com o princípio das arquiteturas de três camadas, as camadas devem ser independentes umas das outras. Esta independência é obtida 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 por si próprio a classe de outra camada para a utilizar. Limita-se a solicitar a uma ferramenta externa, neste caso [Spring], uma instância de implementação da interface da 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 Spring de que pretende uma referência.
Na nossa aplicação, o ficheiro de configuração do Spring poderia ser o seguinte:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- a classe de acesso aos dados -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- a classe de negócio -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Para aceder à camada [métier], uma classe da camada [Interface utilisateur, UI] poderá solicitar o bean [articlesDomain]. O Spring irá então instanciar um objeto do tipo [istia.st.articles.domain.AchatsArticles]. Para esta instanciação, necessita de um bean do tipo [articlesDao], ou seja, de um objeto do tipo [istia.st.articles.dao.ArticlesDaoSqlMap]. O Spring irá então instanciar esse objeto. Esta instanciação será feita a partir das informações contidas no ficheiro [sqlmap-config-firebird.xml], ficheiro de configuração de um acesso aos dados por 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 às alterações relacionadas com o SGBD ou com a base de dados é assegurada aqui 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 aqui a uma base de dados Firebird. Basta alterar o nome do controlador JDBC para trabalhar com outro, como o SGBD.
- o ficheiro [articles.xml], que reúne as diferentes 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">
<!-- um alias para a classe istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- o mapeamento O RM: linha da tabela ARTICLES - instância da classe Artigo -->
<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>
<!-- a consulta SQL para obter todos os artigos -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- a consulta SQL para eliminar todos os artigos -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- a consulta SQL para inserir um artigo -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#(id#,#nome#,#preço#,#stockatual#,#stockmínimo#)
</statement>
<!-- a consulta SQL para eliminar um artigo específico -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- a consulta SQL para alterar um determinado artigo -->
<statement id="modifyArticle">
update ARTICLES set nom=#nome#,
prix=#preço#,stockatual=#stockatual#,stockmínimo=#stockmínimo# onde
id=#id#
</statement>
<!-- a consulta SQL para obter um determinado artigo -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- a consulta SQL para alterar o stock de um determinado artigo -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#movimento#
where id=#id# e stockActuel+#movimento#>=0
</statement>
</sqlMap>
|
Se os nomes da tabela de artigos ou das colunas viessem a mudar, seria necessário reescrever as consultas neste ficheiro de configuração sem ter de alterar o código Java. O mesmo se aplicaria se uma consulta viesse a ser substituída por um procedimento armazenado por razões de desempenho.
3.5.8. A arquitetura global da aplicação [webarticles]
Uma aplicação web Java é um quebra-cabeças com inúmeros elementos. Dotá-la de uma arquitetura MVC aumenta, em geral, o número desses elementos. A estrutura da aplicação [webarticles] sob [eclipse] é a seguinte:
structure générale - on voit ci-dessous les
archives Java utilisées par le projet
Eclipse.
spring : pour Spring
ibatis : pour SqlMap
log4j, commons-logging : pour les logs
de Spring et Sqlmap
firebird : pour le SGBD firebird
mysql : pour le SGBD MySQL
jstl, standard : pour la bibliothèque
de balises JSTL
|
le dossier des sources java : contient le code Java
ainsi que les fichiers de configuration
spring et sqlmap. Eclipse recopie
automatiquement ces fichiers
dans [WEB-INF/classes].
C'est là que l'application les trouvera.
|
 |  |
le dossier [WEB-INF] de l'application : contient le
descripteur [web.xml] de l'application ainsi que le
fichiers de définition de la bibliothèques JSTL
| |
 |  |
3.5.9. As vistas JSP
As vistas JSP utilizam a biblioteca de tags JSTL.
3.5.9.1. entete.jsp
Para conferir uma certa uniformidade às diferentes vistas, estas partilharão um mesmo cabeçalho, aquele que exibe o nome da aplicação juntamente com o menu:
O menu é dinâmico e definido pelo controlador. Este insere na solicitação enviada à página JSP um atributo-chave «actions», cujo valor associado é um array do tipo Hastable[]. Cada elemento deste array é um dicionário destinado a gerar uma opção do menu do cabeçalho. Cada dicionário tem duas chaves:
- href: o URL associado à opção do menu
- lien: o texto do menu
As outras vistas da aplicação utilizarão o cabeçalho definido por [entete.jsp] através da seguinte baliza JSP:
<jsp:include page="entete.jsp"/>
Quando executada, esta baliza terá como efeito incluir, no código da página JSP que a contém, o código da página [entete.jsp]. Como o URL da página é relativo (sem /), a página [entete.jsp] será procurada na mesma pasta 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. liste.jsp
Esta vista apresenta a lista de artigos disponíveis para venda:
É apresentada na sequência de uma solicitação /main?action=liste ou /main?action=validationpanier. Os elementos da solicitação do controlador são os seguintes:
| objeto Hashtable[] — a matriz de opções do menu |
| ArrayList de objetos do tipo [Article] |
| objeto String - mensagem a apresentar na parte inferior da página |
Cada ligação [Infos] da tabela HTML dos artigos tem um URL com o formato [?action=infos&id=ID], em que ID é o campo id do artigo apresentado.
Código:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Liste des articles</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th>
</tr>
<c:forEach var="article" items="${listarticles}">
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><a href="<c:out value="?action=infos&id=${article.id}"/>">Infos</a></td>
</tr>
</c:forEach>
</table>
<p>
<c:out value="${message}"/>
</body>
</html>
|
3.5.9.3. infos.jsp
Esta vista apresenta informações sobre um artigo e permite também a sua compra:

É apresentada na sequência de uma solicitação /main?action=infos&id=ID ou de uma solicitação /main?action=achat&id=ID quando a quantidade comprada está errada. Os elementos da solicitação do controlador são os seguintes:
| objeto Hashtable[] — a tabela de opções do menu |
| objeto do tipo [Article] — artigo a apresentar |
| objeto String - mensagem a apresentar em caso de erro na quantidade |
| objeto String - valor a apresentar no campo de introdução de dados [Qte] |
Os campos [msg] e [qte] são utilizados em caso de erro de introdução de dados relativos à quantidade:

Esta página contém um formulário que é enviado através do botão [Acheter]. A URL de destino do POST é [?action=achat&id=ID], sendo 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. panier.jsp
Esta página apresenta o conteúdo do carrinho:

É apresentada na sequência de uma solicitação /main?action=cesto ou /main?action=retirarcompra&id=ID. Os elementos da solicitação do controlador são os seguintes:
| objeto Hashtable[] — a tabela de opções do menu |
| objeto do tipo [Panier] — o carrinho a apresentar |
Cada ligação [Retirer] da tabela HTML das compras do carrinho tem um URL com o formato [?action=retirerachat&id=ID], em que ID é o campo [id] doartigo que se pretende remover 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. paniervide.jsp
Esta vista apresenta a informação que indica que o carrinho está vazio:

É apresentada na sequência de uma solicitação /main?action=panier ou /main?action=retirerachat&id=ID. Os elementos da solicitação do controlador são os seguintes:
| objeto Hashtable[] — a tabela 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. erreurs.jsp
Esta vista é apresentada em caso de erros:

É apresentada na sequência de qualquer pedido que resulte num erro, exceto no caso da ação de compra com uma quantidade errada, que é tratada pela vista [INFOS]. Os elementos do pedido do controlador são os seguintes:
| objeto Hashtable[] — a tabela de opções do menu |
| ArrayList de objetos String que representam as mensagens de erro a apresentar |
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 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] limita-se a redirecionar 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
Resta escrever o núcleo da nossa aplicação web, o controlador. A sua função consiste em:
- recuperar o pedido do cliente,
- processar a ação solicitada pelo cliente com a ajuda das classes de negócio,
- enviar, em resposta, a vista adequada.
3.5.10.1. Inicialização do controlador
Quando a classe do controlador é carregada pelo servidor de servlets, o seu método [init] é executado. Esta será a única vez. Uma vez carregado na memória, o controlador permanecerá lá e processará as solicitações dos diferentes clientes. Cada cliente é alvo de um thread de execução e os métodos do controlador são, assim, executados simultaneamente por diferentes threads. Recorde-se que, por esta razão, o controlador não deve ter campos que os seus métodos possam alterar. Os seus campos devem ser de leitura única. São inicializados pelo método [init], cuja função principal é precisamente essa. Este método tem, de facto, a particularidade de ser executado uma única vez por um único thread. Não há, portanto, problemas de acesso concorrente aos campos do controlador neste método. O método [init] tem como objetivo inicializar os objetos necessários à aplicação web e que serão partilhados em modo de leitura apenas por todas as threads de cliente. Estes objetos partilhados podem ser colocados em dois locais:
- nos campos privados do controlador
- o contexto de execução da aplicação (ServletContext)
O método [init] da aplicação [webarticles] realizará as seguintes ações:
- verificará a presença, no ficheiro [web.xml], dos parâmetros necessários ao bom funcionamento da aplicação. Estes foram descritos no parágrafo 3.5.5.
- preencherá um campo privado [ArrayList erreurs] com a lista de eventuais erros. Esta lista estará vazia se não houver erros, mas existirá na mesma.
- Se tiverem ocorrido erros, o método [init] termina aqui. Caso contrário, cria um objeto do tipo [IArticlesDomain], que será o objeto de negócio que o controlador utilizará para as suas necessidades. Tal como explicado no ponto 3.5.6, o controlador solicitará ao framework Spring o bean de que necessita. Esta operação de instanciação pode dar origem a vários erros. Se for esse o caso, estes serão, mais uma vez, armazenados no campo [erreurs] do controlador.
3.5.10.2. Métodos doGet, doPost
Estes dois métodos processam os pedidos HTTP, GET e POST dos clientes. Estas serão processadas indistintamente. O método [doPost] poderá, assim, remeter para o método [doGet] ou vice-versa. A solicitação do cliente será processada da seguinte forma:
- o campo [erreurs] 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 funcionar. Nesse caso, será enviada, como resposta, a vista [ERREURS].
- o parâmetro [action] da solicitação será recuperado e verificado. Se não corresponder a uma ação conhecida, a vista [ERREURS] é enviada com uma mensagem de erro adequada.
- 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 processa 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. Processamento das diferentes ações
Os métodos que tratam as diferentes ações possíveis da aplicação são os seguintes:
método | pedido | processamento | respostas possíveis |
| GET /main?action=liste | - solicitar a lista de artigos à classe de negócio - exibi-la | [LISTE] ou [ERREURS] |
| GET /main?action=infos&id=ID | - solicitar o artigo com id=ID à classe de negócio - exibi-lo | [INFOS] ou [ERREURS] |
| POST /main?action=compra&id=ID - a quantidade comprada faz parte dos parâmetros enviados | - solicitar o artigo com id=ID à classe de negócio - incluí-lo no cesto de compras na a sessão do cliente | [LISTE] ou [INFOS] ou [ERREURS] |
| GET /main?action=retirerachat&id=ID | - retirar o artigo com id=ID da lista de compras do carrinho de sessão do cliente | [PANIER] |
| GET /main?action=cesto | - exibir o cesto de compras da sessão do cliente | [PANIER] ou [PANIERVIDE] |
| GET /main?action=validationpanier | - Reduzir na base de dados os stocks de todos os artigos presentes no carrinho de sessão do cliente | [LISTE] ou [ERREURS] |
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 {
// campos privados
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String URL_MAIN = "urlMain";
private final String URL_ERREURS = "urlErreurs";
private final String URL_LISTE = "urlListe";
private final String URL_INFOS = "urlInfos";
private final String URL_PANIER = "urlPanier";
private final String URL_PANIER_VIDE = "urlPanierVide";
private final String URL_DEBUG = "urlDebug";
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters =
{
URL_MAIN,
URL_ERREURS,
URL_LISTE,
URL_INFOS,
URL_PANIER,
URL_PANIER_VIDE,
URL_DEBUG,
SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste";
private final String ACTION_PANIER = "panier";
private final String ACTION_ACHAT = "achat";
private final String ACTION_INFOS = "infos";
private final String ACTION_RETIRER_ACHAT = "retirerachat";
private final String ACTION_VALIDATION_PANIER = "validationpanier";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
public void init() {
// recuperam-se os parâmetros de inicialização do servlet
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// regista-se o erro
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// erros?
if (erreurs.size() != 0) {
return;
}
// é criado um objeto IArticlesDomain para aceder à camada de negócios
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// regista-se o erro
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// guardam-se algumas URLs da aplicação
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);
// concluído
return;
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// verifica-se como decorreu a inicialização do servlet
if (erreurs.size() != 0) {
// temos o URL da página de erros?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// exibimos a página de erros
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fim
return;
}
// processa-se a ação
String action = request.getParameter("action");
if (action == null) {
// lista de artigos
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// lista de artigos
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// informações sobre um artigo
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// compra de um artigo
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// visualização do cesto de compras
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// remoção de um artigo do cesto
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// confirmação do carrinho
doValidationPanier(request, response);
return;
}
// ação desconhecida
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// é apresentada a página de erros
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// fim
return;
}
private void doValidationPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// o comprador confirmou o seu carrinho
Panier panier = (Panier) request.getSession().getAttribute("panier");
// este carrinho é validado
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// anormal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// recuperam-se os erros
ArrayList erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionPanier });
afficheErreurs(request, response, erreurs);
return;
}
// exibe-se a lista de artigos
request.setAttribute("message", "Votre panier a été validé");
doListe(request, response);
// fim
return;
}
private void doRetirerAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// retirar um artigo do carrinho
try {
Panier panier =
(Panier) request.getSession().getAttribute("panier");
String strIdAchat = request.getParameter("id");
panier.enlever(Integer.parseInt(strIdAchat));
} catch (NumberFormatException ignored) {
} catch (NullPointerException ignored) {
}
// exibe o cesto
doPanier(request, response);
}
private void doPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// exibe o cesto
Panier panier = (Panier) request.getSession().getAttribute("panier");
// cesto vazio?
if (panier == null || panier.getAchats().size() == 0) {
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
.forward(request, response);
// fim
return;
}
// há algo no carrinho
request.setAttribute("panier", panier);
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionValidationPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER))
.forward(request, response);
// fim
return;
}
private void doAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// compra de um artigo
// recuperar a quantidade
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// quantidade errada
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);
// fim
return;
}
// recuperação da sessão do cliente
HttpSession session = request.getSession();
// criação da compra
Article article = (Article) session.getAttribute("article");
Achat achat = new Achat(article, qté);
// adiciona-se a compra ao cesto do cliente
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// regressa-se à lista de artigos
String url = config.getInitParameter(URL_MAIN) + "?action=liste";
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// fim
return;
}
private void afficheDebugInfos(
HttpServletRequest request,
HttpServletResponse response,
ArrayList infos)
throws ServletException, IOException {
// exibe-se a lista de artigos
request.setAttribute("infos", infos);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_DEBUG))
.forward(request, response);
// fim
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 {
// a lista de erros
ArrayList erreurs = new ArrayList();
// recupera-se o ID solicitado
String strId = request.getParameter("id");
// alguma coisa?
if (strId == null) {
// não é normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// converte-se strId na íntegra
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// não é normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// solicita-se o artigo da chave id
Article article = null;
try {
article=articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// anormal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
if (article == null) {
// anormal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// o artigo é colocado na sessão
request.getSession().setAttribute("article", article);
// é apresentada a página de informações
request.setAttribute("actions", new Hashtable[] { hActionListe });
/ / request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_INFOS))
.forward(request, response);
// fim
return;
}
private void afficheErreurs(
HttpServletRequest request,
HttpServletResponse response,
ArrayList erreurs)
throws ServletException, IOException {
// é apresentada a página de erros
request.setAttribute("erreurs", erreurs);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fim
return;
}
private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// a lista de erros
ArrayList erreurs = new ArrayList();
// solicita-se a lista de artigos
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// regista-se o erro
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// há erros?
if (erreurs.size() != 0) {
// exibe-se a página de erros
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fim
return;
}
// exibe-se a lista de artigos
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// fim
return;
}
/**
* suivi console pour débogage
* @param message : le message à afficher
*/
private void affiche(String message) {
System.out.println(message);
}
}
|
Deixamos que o leitor dedique algum tempo a ler e a compreender este código. Esperamos que os comentários o ajudem nesse sentido.
3.5.10.5. Testes da aplicação
Vamos mostrar algumas capturas de ecrã dos testes. Em primeiro lugar, a página inicial da aplicação:

O URL solicitado era, na realidade, [http://localhost:8080/webarticles]. O leitor poderá verificar 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], como se pode ver no URL do navegador na captura de ecrã. A URL [/main?action=liste] foi, portanto, solicitada. Ainda em [web.xml], a URL /main está associada ao servlet [webarticles]:
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
Ainda 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>
A servlet [ istia.st.articles.web.WebArticles] é, portanto, carregada pelo contentor de servlets Tomcat, caso ainda não o tenha sido, e o seu método [init] é executado:
| public void init() {
// recuperam-se os parâmetros de inicialização do servlet
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// regista-se o erro
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// erros?
if (erreurs.size() != 0) {
return;
}
// cria-se um objeto IArticlesDomain para aceder à camada de negócios
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// armazena-se o erro
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// guardam-se algumas URLs da aplicação
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);
// concluído
return;
}
|
Comentários: o método [init]
- verifica a presença de determinados parâmetros de configuração
- instancia um serviço de acesso ao domínio da aplicação através do Spring
- define uma lista de erros para indicar eventuais erros de inicialização
- alguns campos privados:
- erros: a lista de erros detetados pelo [init]
- [hActionListe, hActionPanier, hActionValidationPanier]: dicionários. Cada um deles contém as informações necessárias para apresentar uma opção do menu principal exibido pela vista [entete.jsp]
- acticlesDomain: o serviço de acesso ao modelo da aplicação
O método [init] é executado apenas uma vez, no carregamento inicial do servlet. Posteriormente, um dos métodos [doGet, doPost] é executado de acordo com o tipo [GET, POST] do pedido do cliente. Neste caso, os dois métodos fazem o mesmo e o código foi colocado no [doGet]:
| public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// verifica-se como decorreu a inicialização do servlet
if (erreurs.size() != 0) {
// temos a URL da página de erros?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// exibimos a página de erros
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fim
return;
}
// processa-se a ação
String action = request.getParameter("action");
if (action == null) {
// lista de artigos
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// lista de artigos
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// informações sobre um artigo
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// compra de um artigo
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// visualização do cesto de compras
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// remoção de um artigo do cesto
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// confirmação do cesto de compras
doValidationPanier(request, response);
return;
}
// ação desconhecida
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// é apresentada a página de erros
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// fim
return;
}
|
- o método [doGet] começa por verificar se ocorreram erros de inicialização após a execução do método [init]. Se sim, exibe a vista [ERREURS] e o processo termina.
- Caso contrário, recupera o parâmetro [action] na solicitação do cliente. Recorde-se que a aplicação foi concebida para responder a solicitações que incluam obrigatoriamente um parâmetro [action].
- Ela executa o método associado à ação. Neste caso, será o método [doListe].
O método [doListe] é o seguinte:
| private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// lista de erros
ArrayList erreurs = new ArrayList();
// solicita-se a lista de artigos
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// o erro é registado
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// há erros?
if (erreurs.size() != 0) {
// exibe-se a página de erros
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fim
return;
}
// exibe-se a lista de artigos
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// fim
return;
}
|
- Recorde-se que o método [init] armazenou o serviço de acesso ao modelo da aplicação (camada de domínio) num campo privado do servlet:
// campos privados
private IArticlesDomain articlesDomain = null;
- A partir deste serviço de acesso, é possível solicitar a lista de artigos:
| // a lista de erros
ArrayList erreurs = new ArrayList();
// solicita-se a lista de artigos
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// regista-se o erro
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- Caso ocorram erros, é enviada a vista [ERREURS]:
| // erros?
if (erreurs.size() != 0) {
// exibe-se a página de erros
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// fim
return;
}
|
- caso contrário, é enviada a vista [LISTE]:
| // exibe-se a lista de artigos
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 a vista [LISTE]. Convidamos o leitor a reler o código da vista [LISTE] para verificar que 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 feito para cada vista:
- identificar os parâmetros dinâmicos da vista
- certificar-se de que o controlador os inclui efetivamente nos atributos da solicitação transmitida à vista
Apresentamos agora simplesmente o percurso pelas telas percorridas por um utilizador da aplicação. O leitor é convidado a repetir, em cada caso, um raciocínio semelhante ao anterior:
A partir da lista de artigos, o utilizador pode escolher um artigo:
O comprador pode adquirir aqui o artigo n.º 3. Vamos cometer um erro ao introduzir a quantidade:
O erro foi sinalizado. Agora, vamos comprar alguns artigos:
A compra foi registada e a lista de artigos foi atualizada. Vamos verificar o cesto:
A compra está, de facto, no cesto. Vamos removê-la:
O produto foi removido do cesto de compras e este foi atualizado. Aqui, está vazio.
Vamos comprar 100 artigos n.º 3 e 2 artigos n.º 4:
A compra do artigo n.º 3 revelou-se impossível, pois pretendíamos comprar 100 e havia apenas 30 em stock. Esta compra permaneceu no cesto:
O artigo n.º 4, por sua vez, foi comprado, como demonstra o seu novo stock igual a 39 (40-1):
3.6.1. Arquitetura geral da aplicação
Voltemos à arquitetura MVC da aplicação:
Na versão anterior:
- o controlo era assegurado por um servlet
- as vistas 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á assegurado por um servlet derivado do servlet genérico [ActionServlet] do Struts
- as vistas serão geradas pelas mesmas páginas JSP que anteriormente, com algumas pequenas diferenças
- o modelo será implementado pelos mesmos três ficheiros
Vamos descobrir que a migração da aplicação anterior para o Struts consiste nas seguintes tarefas:
- as ações que eram processadas em métodos específicos do servlet/controlador são agora processadas por instâncias de classes derivadas da classe [Action] do Struts
- escrever os ficheiros de configuração [web.xml] e [struts-config.xml]
- efetuar algumas alterações nas páginas JSP
Recorde-se a arquitetura genérica MVC utilizada pelo STRUTS:
| as classes de negócio, as classes de acesso aos dados e a base de dados |
| as páginas JSP |
| o servlet de processamento de pedidos dos clientes, os objetos [Action] e os beans [ActionForm] associados aos formulários. |
- O controlador é o coração da aplicação. Todos os pedidos do cliente passam por ele. Trata-se de um servlet genérico fornecido pelo STRUTS. Em certos casos, pode ser necessário derivá-la. Para casos simples, isso não é necessário. Esta servlet genérica obtém as informações de que necessita num ficheiro geralmente denominado struts-config.xml.
- Se a solicitação do cliente contiver parâmetros de formulário, estes são colocados pelo controlador num objeto Bean. Os objetos Bean assim criados ao longo do tempo são armazenados na sessão ou na solicitação do cliente. Este aspeto é configurável. Não é necessário recriá-los se já tiverem sido criados anteriormente.
- No ficheiro de configuração struts-config.xml, a cada URL que deva ser processado por programa (não correspondendo, portanto, a uma vista JSP que se possa solicitar diretamente) são associadas determinadas informações:
- o nome da classe do tipo Action responsável por processar o pedido. Também neste caso, o objeto Action instanciado pode ser mantido na sessão ou no pedido.
- Se a URL solicitada estiver configurada (caso do envio de um formulário ao controlador), é indicado o nome do bean responsável por memorizar as informações do formulário.
- Com estas informações fornecidas pelo seu ficheiro de configuração, ao receber um pedido de URL por parte de um cliente, o controlador é capaz de determinar se existe um bean a criar e qual. 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 denominado «validate» é chamado automaticamente pelo controlador. O bean é criado pelo programador. Este insere, assim, no método «validate» o código que verifica a validade dos dados do formulário. Se os dados se revelarem inválidos, o controlador não prosseguirá. Passará o controlo para uma vista cujo nome encontrará no seu ficheiro de configuração. A interação fica então concluída. Note-se que o programador pode solicitar que a validade do formulário não seja verificada. Faz isso também 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 verificação, ou se não houver nenhum bean, o controlador passa o controlo para o objeto do tipo Action associado ao URL. Faz-o solicitando a execução do método `execute` desse objeto, ao qual transmite a referência do bean que, eventualmente, tenha construído. É aqui que o programador realiza o que tem de fazer: poderá ter de recorrer a classes de negócio ou a classes de acesso aos dados. No final do processamento, o objeto `Action` devolve ao controlador o nome da vista que este deve enviar como resposta ao cliente.
- No seu ficheiro de configuração, o controlador encontrará o URL associado ao nome da vista que lhe foi solicitado para apresentar. Em seguida, envia essa vista. A interação com o cliente está concluída.
Na nossa aplicação, não utilizaremos objetos [Bean] como objetos tampão entre o cliente e as classes [Action]. O objeto [Action] irá buscar diretamente os parâmetros do pedido do cliente no objeto [HttpServletRequest] que receberá. Isto facilita a portabilidade da nossa aplicação inicial. A arquitetura final da nossa aplicação será, portanto, a seguinte:
| as classes de negócio, as classes de acesso aos dados e a base de dados |
| as páginas JSP |
| o servlet de processamento de pedidos dos clientes, os objetos [Action] |
3.6.2. O modelo
Já foi apresentado anteriormente. É constituído pelos ficheiros 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
Dado que a interface de acesso aos dados não sofre alterações, 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 dos arquivos
No [WEB-INF/lib], encontram-se os mesmos arquivos da versão anterior, além daquele necessário para o Struts:

3.6.3.4. Configuração da aplicação
A aplicação é configurada através de 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 é [vues/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)
- esta 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á processar as seguintes URLs:
| para apresentar a lista de artigos |
| para apresentar a lista de artigos |
| para visualizar informações sobre um artigo específico |
| para comprar um artigo específico |
| para visualizar o cesto de compras |
| para remover um artigo do cesto de compras |
| para confirmar o cesto de compras |
- As ações anteriores correspondem, uma a uma, às ações processadas pelo servlet na versão anterior. Para cada uma delas, são especificadas as seguintes informações:
- o nome da classe responsável pelo processamento dessa ação
- as respostas (= visualizações) possíveis após o processamento da ação. Apenas uma delas será escolhida pelo controlador.
- o nome de um ficheiro de mensagens para a aplicação (message-resources). Neste caso, o ficheiro existirá, mas estará vazio. Não será utilizado. Deve ser colocado no [ClassPath] da aplicação. Aqui, será colocado no [WEB-INF/classes]. No Eclipse, obtém-se este resultado colocando-o no [WEB-INF/src]:

3.6.4. As vistas JSP
As vistas JSP utilizadas são também as da versão anterior. Muito poucas alterações foram feitas: trata-se das URLs do formato [?action=XX?id=YY& ...], que passam a ser [/XX.do?id=YY&....]. Repetimos aqui as explicações já fornecidas para evitar que o utilizador tenha de voltar atrás. É importante compreender que as informações transmitidas à vista pelo controlador são exatamente as mesmas nas duas versões. Nada foi alterado neste aspeto.
3.6.4.1. entete.jsp
Para conferir uma certa homogeneidade às diferentes vistas, estas partilharão um mesmo cabeçalho, aquele que exibe o nome da aplicação juntamente com o menu:
O menu é dinâmico e definido pelo controlador. Este insere na solicitação enviada à página JSP um atributo de chave «actions», cujo valor associado é um array do tipo Hastable[]. Cada elemento deste array é um dicionário destinado a gerar uma opção do menu do cabeçalho. Cada dicionário tem duas chaves:
- href: o URL associado à opção do menu
- lien: o texto do menu
As outras vistas da aplicação utilizarão o cabeçalho definido por [entete.jsp] através da seguinte baliza JSP:
<jsp:include page="entete.jsp"/>
Na execução, esta baliza terá como efeito incluir no código da página JSP, que a contém, o código da página [entete.jsp]. Como o URL da página é relativo (sem /), a página [entete.jsp] será procurada na mesma pasta 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. liste.jsp
Esta vista apresenta a lista de artigos disponíveis para venda:
É apresentada na sequência de uma consulta /main.do ou /validerpanier.do. Os elementos da consulta do controlador são os seguintes:
| objeto Hashtable[] — a tabela de opções do menu |
| ArrayList de objetos do tipo [Article] |
| objeto String - mensagem a apresentar na parte inferior da página |
Cada ligação [Infos] da tabela HTML dos artigos tem um URL com o formato [/infos.do?id=ID], em que ID é o campo id do artigo apresentado.
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 uma consulta /infos.do?id=ID ou de uma consulta /achat.do?id=ID quando a quantidade comprada está errada. Os elementos da solicitação do controlador são os seguintes:
| objeto Hashtable[] — a tabela de opções do menu |
| objeto do tipo [Article] — artigo a apresentar |
| objeto String - mensagem a apresentar em caso de erro na quantidade |
| Objeto String - valor a apresentar no campo de introdução [Qte] |
Os campos [msg] e [qte] são utilizados em caso de erro de introdução da quantidade:

Esta página contém um formulário que é enviado através do botão [Acheter]. A URL de destino do POST é [/achat.do?id=ID], sendo 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. panier.jsp
Esta vista apresenta o conteúdo do carrinho:

É apresentada na sequência de uma solicitação /panier.do ou /retirerachat.do?id=ID. Os elementos da solicitação do controlador são os seguintes:
| objeto Hashtable[] — a tabela de opções do menu |
| objeto do tipo [Panier] — o carrinho a apresentar |
Cada ligação [Retirer] da tabela HTML das compras do carrinho tem um URL com o formato [retirerachat.do?id=ID], em que ID é o campo [id] doartigo que se pretende remover 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. paniervide.jsp
Esta vista apresenta a informação indicando que o cesto está vazio:

É apresentada na sequência de uma solicitação /panier.do ou /retirerachat.do?id=ID. Os elementos da solicitação do controlador são os seguintes:
| objeto Hashtable[] — a tabela 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. erreurs.jsp
Esta vista é apresentada em caso de erros:

É apresentada na sequência de qualquer pedido que resulte num erro, exceto no caso da ação de compra com uma quantidade errada, que é tratada pela vista [INFOS]. Os elementos do pedido do controlador são os seguintes:
| objeto Hashtable[] — a tabela de opções do menu |
| ArrayList de objetos String que representam as mensagens de erro a apresentar |
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 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] limita-se a redirecionar 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 dispõe de um controlador genérico denominado [ActionServlet]. Sabe-se que um servlet dispõe de um método [init] que permite inicializar a aplicação quando esta é iniciada. Se utilizarmos o controlador genérico do Struts [ActionServlet], não temos acesso ao seu método [init]. Neste caso, temos tarefas a realizar no arranque da aplicação, essencialmente instanciar um objeto de acesso ao modelo. Por isso, precisamos de um método [init]. Assim, derivamos 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 {
// campos privados
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters = { SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_ACHAT = "achat.do";
private final String ACTION_INFOS = "infos.do";
private final String ACTION_RETIRER_ACHAT = "retirerachat.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// getters - setters
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public ArrayList getErreurs() {
return erreurs;
}
public void setErreurs(ArrayList erreurs) {
this.erreurs = erreurs;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public void setHActionListe(Hashtable actionListe) {
hActionListe = actionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public void setHActionPanier(Hashtable actionPanier) {
hActionPanier = actionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
public void setHActionValidationPanier(Hashtable actionValidationPanier) {
hActionValidationPanier = actionValidationPanier;
}
public void init() throws ServletException{
// inicialização da classe pai
super.init();
// recuperam-se os parâmetros de inicialização do servlet
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// regista-se o erro
erreurs.add("Paramètre [" + parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// erros?
if (erreurs.size() != 0) {
return;
}
// cria-se um objeto IArticlesDomain para aceder à camada de negócios
try {
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource((String) config
.getInitParameter(SPRING_CONFIG_FILENAME))))
.getBean("articlesDomain");
} catch (Exception ex) {
// armazena-se o erro
erreurs.add("Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// guardam-se algumas URLs da aplicação
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);
// Concluído
return;
}
}
|
Comentários:
- o interesse desta classe reside no seu método [init] e nos seus campos privados
- o método [init] faz o mesmo que o método [init] do controlador da versão anterior:
- verifica a presença de determinados parâmetros de configuração
- instancia um serviço de acesso ao domínio da aplicação através do Spring
- define uma lista de erros para indicar eventuais erros de inicialização
- antes de começar a funcionar, o método [init] chama o método [init] da classe pai [ActionServlet]. É este método que irá utilizar o ficheiro de configuração do Struts [struts-config.xml].
- Estão definidos vários campos privados com os respetivos acessores:
- erros: a lista de erros detetados pelo [init]
- [hActionListe, hActionPanier, hActionValidationPanier]: dicionários. Cada um deles contém as informações necessárias para apresentar uma opção do menu principal exibido pela vista [entete.jsp]
- acticlesDomain: o serviço de acesso ao modelo da aplicação
- O controlador de uma aplicação Struts está acessível às classes [Action], responsáveis por processar as diferentes ações possíveis. Estas classes terão acesso aos campos privados anteriores, uma vez que estes dispõem de acessores públicos.
A instância deste controlador é assegurada 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á processado por uma instância da classe [istia.st.articles.web.struts.MainServlet]
3.6.6. As ações da aplicação Struts
3.6.6.1. Introdução
Cada ação Struts será objeto de uma classe. Na versão anterior, cada ação era objeto de um método no controlador da aplicação. A implementação da classe [Action] consiste, na maioria das vezes, em:
- fazer um copiar/colar do método que tinha sido utilizado na versão anterior
- adaptar o código às convenções do Struts
3.6.6.2. main.do, liste.do
Estas duas ações são idênticas e estão definidas em [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 {
// o servlet de controlo
MainServlet mainServlet = (MainServlet) this.getServlet();
// erros de inicialização?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// é apresentada a página de erros
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// o objeto de acesso ao domínio
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// a lista de erros
ArrayList erreurs = new ArrayList();
// solicita-se a lista de artigos
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// o erro é registado
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// erros?
if (erreurs.size() != 0) {
// exibe-se a página de erros
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// exibe-se a lista de artigos
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
}
}
|
Comentários:
- escrever o código de uma classe [Action] consiste essencialmente em escrever o código do seu método [execute]
- Foram armazenadas algumas informações na instância do controlador. Obtém-se uma referência a esta através de:
// o servlet de controlo
MainServlet mainServlet = (MainServlet) this.getServlet();
- Recupera-se a lista de erros de inicialização armazenados pelo controlador. Se essa lista não estiver vazia, a vista [ERREURS] é enviada ao cliente:
| // erros de inicialização?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// é apresentada a página de erros
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
|
A vista que será efetivamente enviada ao cliente é-nos 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>
Trata-se da vista [/vues/erreurs.jsp]. O leitor é convidado a verificar o que é esperado por esta vista. Estas informações são aqui fornecidas pela ação no objeto [request] como atributos.
- Ainda através do controlador, a ação pode recuperar o objeto de acesso ao modelo da aplicação (camada de domínio):
// o objeto de acesso ao domínio
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
- Feito isto, é possível solicitar a lista de artigos:
| // a lista de erros
ArrayList erreurs = new ArrayList();
// solicita-se a lista de artigos
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// o erro é registado
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- Se ocorrerem erros, é apresentada a vista [ERREURS]:
| // erros?
if (erreurs.size() != 0) {
// exibe-se a página de erros
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- caso contrário, é enviada a vista [LISTE]:
| // exibe-se a lista de artigos
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 é-nos 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>
Trata-se da vista [/vues/liste.jsp]. O leitor é convidado a verificar o que é esperado por esta vista. Estas informações são aqui fornecidas pela ação no objeto [request] como atributos.
3.6.6.3. infos.do
Esta ação serve para fornecer informações sobre um dos artigos apresentados na vista [LISTE]:
Esta ação está configurada da seguinte forma em [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 da 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 {
// o servlet de controlo
MainServlet mainServlet = (MainServlet) this.getServlet();
// erros de inicialização?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// é apresentada a página de erros
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// o objeto de acesso ao domínio
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// a lista de erros
ArrayList erreurs = new ArrayList();
// recuperar o ID solicitado
String strId = request.getParameter("id");
// alguma coisa?
if (strId == null) {
// não é normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// converte-se strId na íntegra
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// não é normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// solicita-se o artigo da chave id
Article article = null;
try {
article = articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// anormal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
if (article == null) {
// anormal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// o artigo é colocado na sessão
request.getSession().setAttribute("article", article);
// é apresentada a página de informações
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
}
|
Comentários:
- o início do método [execute] é idêntico ao analisado anteriormente. O mesmo se aplica às restantes ações.
- O método recupera o parâmetro [id], que normalmente deve constar no URL. Este deve, de facto, ter o formato [/infos.do?id=X]. São realizados vários testes para verificar a presença e a validade do parâmetro [id]. Em caso de problema, é enviada a vista [ERREURS].
- Se [id] for válido, o artigo correspondente é solicitado à camada [domain]. Se esta lançar uma exceção ou se o artigo não for encontrado, também neste caso é enviada a vista [ERREURS].
- Se tudo correr bem, o artigo obtido é colocado na sessão. Este é um ponto discutível. Aqui, parte-se do princípio de que o cliente poderá vir a comprar esse artigo. Se o fizer, iremos buscá-lo à sessão, em vez de o solicitar novamente à camada [domain].
- Por fim, a vista [INFOS] é apresentada. A vista que será efetivamente enviada ao cliente é-nos fornecida por [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>
Trata-se da vista [/vues/infos.jsp]. O leitor é convidado a verificar o que é esperado por esta vista. Estas informações são aqui fornecidas pela ação no objeto [request] como atributos.
3.6.6.4. achat.do
Esta ação serve para comprar o artigo apresentado pela vista [INFOS] anterior:
Depois de o artigo ter sido comprado, a vista [LISTE] é novamente apresentada (vista da direita). Se analisarmos o código HTML da vista da esquerda acima, verificamos que a baliza <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>
|
Vemos, portanto, que o formulário é enviado para o controlador com a ação [achat.do].
Esta ação está configurada da seguinte forma em [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 {
// o servlet de controlo
MainServlet mainServlet = (MainServlet) this.getServlet();
// erros de inicialização?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// exibe-se a página de erros
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// recuperar a quantidade comprada
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// quantidade incorreta
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
// recupera-se a sessão do cliente
HttpSession session = request.getSession();
// recuperar o artigo adicionado à sessão
Article article = (Article) session.getAttribute("article");
// sessão expirada?
if(article==null){
// exibe-se a página de erros
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// cria-se a nova compra
Achat achat = new Achat(article, qté);
// adiciona-se a compra ao cesto do cliente
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// volta-se à lista de artigos
return mapping.findForward("afficherListeArticles");
}
}
|
Comentários:
- o início do método [execute] é idêntico aos analisados anteriormente.
- Recorde-se 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 artigo adquirido; [qte]: quantidade adquirida.
- A presença e a validade do parâmetro [qte] são verificadas. Se este parâmetro for reconhecido como incorreto, a vista [INFOS] é devolvida ao utilizador acompanhada de uma mensagem de erro:
| // a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// recupera-se a quantidade comprada
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// quantidade incorreta
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
|
- o artigo adquirido é recuperado da sessão. Esta pode ter expirado. Nesse caso, é enviada a vista [ERREURS]:
| // recuperar a sessão do cliente
HttpSession session = request.getSession();
// recuperar o artigo adicionado à sessão
Article article = (Article) session.getAttribute("article");
// sessão expirada?
if(article==null){
// exibe-se a página de erros
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 artigo é colocado no cesto, também recuperado da sessão:
| // recupera-se a sessão do cliente
HttpSession session = request.getSession();
// recuperar o artigo adicionado à sessão
Article article = (Article) session.getAttribute("article");
// sessão expirada?
if(article==null){
// é apresentada a página de erros
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// cria-se a nova compra
Achat achat = new Achat(article, qté);
// adiciona-se a compra ao cesto do cliente
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- por fim, envia-se a vista [LISTE]:
// volta-se à lista de artigos
return mapping.findForward("afficherListeArticles");
- A vista que será efetivamente enviada ao cliente é-nos fornecida por [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>
Trata-se da vista [/main.do]. Esta vista não é uma vista, mas sim uma ação. A ação [/main.do] descrita anteriormente será, portanto, executada e apresentará a lista de artigos.
3.6.6.5. panier.do
Esta ação serve para apresentar todas as compras do cliente. Está disponível através da opção de menu [Voir le panier]:
O código HTML associado ao link [Voir le panier] é o seguinte:
<a href="panier.do">Voir le panier</a>
A ação [panier.do] está configurada da seguinte forma em [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 da 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 {
// o servlet de controlo
MainServlet mainServlet = (MainServlet) this.getServlet();
// erros de inicialização?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// é apresentada a página de erros
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// exibe o cesto
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// cesto vazio
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
} else {
// há algo no carrinho
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 analisados anteriormente.
- O cesto de compras é obtido na sessão em que se encontra normalmente. A sessão pode ter expirado e, nesse caso, não existe cesto de compras. Não se trata deste caso como um erro, mas considera-se simplesmente que o cesto de compras está vazio.
- Se o carrinho estiver vazio, é apresentada a vista [PANIERVIDE]
- 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. retirerachat.do
Esta ação serve para remover uma compra do carrinho:
Após a ação [retirerachat.do], o carrinho é novamente apresentado (imagem à direita acima). Se analisarmos o código HTML da ligação [Valider le panier] da imagem à esquerda acima, temos 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 retirar do cesto de compras. Esta ação está configurada da seguinte forma em [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 da 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 {
// o servlet de controlo
MainServlet mainServlet = (MainServlet) this.getServlet();
// erros de inicialização?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// é apresentada a página de erros
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// recuperar o cesto
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessão expirada
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
}
// recupera-se o ID do artigo a retirar
String strId = request.getParameter("id");
// alguma coisa?
if (strId == null) {
// não é normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// convertemos strId para o formato inteiro
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// não é normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// eliminamos a compra
panier.enlever(id);
// volta a apresentar o cesto
return mapping.findForward("afficherPanier");
}
}
|
Comentários:
- o início do método [execute] é idêntico aos analisados anteriormente.
- O código que se segue tem como objetivo verificar a presença e a validade do parâmetro [id]. Se este estiver incorreto, é enviada a vista [ERREURS].
- Caso contrário, a compra é removida do cesto:
// remover a compra
panier.enlever(id);
- em seguida, o cesto é novamente apresentado:
// voltar a apresentar o cesto
return mapping.findForward("afficherPanier");
A visualização 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>
Vê-se que, no que diz respeito à vista, é a ação [/panier.do] que será acionada. Esta já foi descrita. Irá apresentar a vista [PANIER] ou [PANIERVIDE], dependendo do estado do cesto de compras.
3.6.6.7. validerpanier.do
Esta ação serve para validar as compras efetuadas pelo cliente. Concretamente, isto traduz-se numa única ação: os stocks 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 da ligação [Valider le panier] é o seguinte:
<a href="validerpanier.do">Valider le panier</a>
Quando esta ligação é ativada, os stocks são reduzidos e a lista de artigos é novamente apresentada.
Esta ação está configurada da seguinte forma em [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 da 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 {
// o servlet de controlo
MainServlet mainServlet = (MainServlet) this.getServlet();
// erros de inicialização?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// exibir a página de erros
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// o objeto de acesso ao domínio
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// o comprador confirmou o seu carrinho
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessão expirada
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// o cesto de compras está a ser validado
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// situação anómala
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");
}
// recuperam-se os eventuais erros
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
// Tudo parece estar em ordem OK - Apresenta-se a lista de artigos
return mapping.findForward("afficherListeArticles");
}
}
|
Comentários:
- o início do método [execute] é idêntico aos analisados anteriormente.
- Recupera-se o cesto de compras da sessão. Se esta tiver expirado, é enviada a vista [ERREURS]:
| // a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// o comprador confirmou o seu carrinho
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessão expirada
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- Validam-se as compras do cesto de compras. Podem ocorrer erros se os stocks forem insuficientes para satisfazer as compras. Nesse caso, envia-se a vista [ERREURS]:
| // o cesto de compras é validado
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// situação anómala
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");
}
// recuperam-se os eventuais erros
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 correr bem, volta a ser apresentada a lista de artigos:
// Tudo parece estar bem OK - Apresenta-se a lista de artigos
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>
Vê-se que, no que diz respeito à vista, é a ação [/main.do] que será acionada. Esta já foi descrita. Irá apresentar a vista [LISTE].
3.7.1. Arquitetura geral da aplicação
Voltemos à arquitetura MVC da aplicação:
Na primeira versão:
- o controlo era assegurado por um servlet
- as vistas 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 assegurado por um servlet derivado do servlet genérico [ActionServlet] do Struts
- as vistas eram geradas pelas mesmas páginas JSP que na versão [Struts]
- o modelo era gerido pelos mesmos três ficheiros
Na versão Spring:
- o controlador será assegurado por uma servlet fornecida pelo Spring [DispatcherServlet]
- as vistas serão geradas pelas mesmas páginas JSP que anteriormente, com algumas pequenas diferenças
- o modelo será gerido pelos mesmos três ficheiros
Vamos descobrir que a transição da nossa aplicação do Struts para o Spring é simples, se aceitarmos não utilizar todos os elementos recomendados para uma arquitetura Spring MVC ortodoxa. As principais alterações são as seguintes:
- as ações que eram processadas em métodos específicos do servlet/controlador, ou por instâncias de classes derivadas da classe [Action] no Struts, são agora processadas por instâncias de classes que implementam a interface Spring [Controller]
- os ficheiros de configuração necessários passam a ser os seguintes:
- [web.xml], uma vez que se trata de uma aplicação web. Este ficheiro contém um Listener que, no momento da inicialização da aplicação, irá utilizar o ficheiro [applicationContext.xml]
- [applicationContext.xml], que permitirá criar os beans de que a aplicação necessita, nomeadamente o do serviço de acesso ao modelo
- as vistas JSP serão idênticas às do Struts. Teremos de criar uma nova.
Recorde-se a arquitetura MVC STRUTS utilizada na versão anterior:
| as classes de negócio, as classes de acesso aos dados e a base de dados |
| as páginas JSP |
| o servlet de processamento de pedidos dos clientes, os objetos [Action] |
Com o Spring, adotamos uma arquitetura idêntica:
| as classes de negócio, as classes de acesso aos dados e a base de dados |
| as páginas JSP |
| o servlet de processamento de pedidos dos clientes, os objetos que implementam a interface [Controller] |
- o controlador é o coração da aplicação. Todos os pedidos do cliente passam por ele. Trata-se de um servlet genérico fornecido pelo SPRING. É do tipo [DispatcherServlet]. Doravante, chamaremos a este controlador o controlador [Spring].
- O controlador [Spring] encaminhará a solicitação do cliente para uma das instâncias do tipo [Controller]. Haverá uma instância deste tipo por cada ação a ser processada. Esta é definida na URL solicitada, tal como no Struts. Assim, saberemos que a ação solicitada é a ação [liste] porque a URL solicitada é [liste.do]
- Se C for o contexto da aplicação, o controlador [Spring] utiliza um ficheiro [C-servlet.xml] que desempenha o papel do ficheiro de configuração struts-config.xml, na versão Struts. A cada ação a ser processada pela aplicação, associa-se o nome da classe do tipo Controller responsável por processar o pedido.
- O controlador passa o controlo ao objeto do tipo Controller associado à ação. Faz isso solicitando a execução do método handleRequest desse objeto, ao qual transmite o pedido do cliente. É aqui que o programador faz o que tem de fazer: poderá ter de recorrer a classes de negócio ou a classes de acesso aos dados. No final do processamento, o objeto Controller devolve ao controlador o nome da vista que este deve enviar como resposta ao cliente.
- No seu ficheiro de configuração, o controlador encontrará a URL associada ao nome da vista que lhe foi solicitado para apresentar. Em seguida, envia essa vista. A interação com o cliente está concluída.
3.7.2. O modelo
É o mesmo das duas versões anteriores. É constituído pelos ficheiros 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
Dado que a interface de acesso aos dados não sofre alterações, 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 dos arquivos
No [WEB-INF/lib], encontram-se os mesmos arquivos que na versão anterior, exceto os do Struts, que já não são necessários:

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>
<!-- o carregador do contexto Spring da aplicação -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- o servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- o mapeamento das URLs -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- o documento de entrada -->
<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 listener [org.springframework.web.context.ContextLoaderListener] será iniciado no arranque da aplicação. 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>
<!-- a classe de acesso aos dados -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- a classe de negócio -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- a configuração da aplicação web-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
Encontramos aqui elementos já conhecidos e outros menos familiares. Três beans serão instanciados durante a inicialização da aplicação:
- articlesDao: serviço de acesso à camada [dao]
- articlesDomain: serviço de acesso ao modelo
- config: um bean no qual iremos reunir as informações que todos os clientes devem partilhar. Este bean desempenhará o papel que 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>
<!-- os gestores de ações = controladores -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- o mapeamento da aplicação-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- gestor de vistas -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- o ficheiro de mensagens -->
<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á processar os seguintes URLs:
| para apresentar a lista de artigos |
| para apresentar a lista de artigos |
| para visualizar informações sobre um artigo específico |
| para comprar um artigo específico |
| para visualizar o cesto de compras |
| para remover um artigo do cesto de compras |
| para confirmar o cesto de compras |
- as ações anteriores correspondem, uma a uma, às ações processadas pelo controlador nas versões anteriores. Para cada uma delas, é especificado o nome da classe responsável pelo seu processamento. Tomemos como exemplo a ação [/panier.do]:
- esta deve ser processada pelo bean [VoirPanierController]. Este nome é livre. Trata-se de uma simples 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 processar a 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 disporá, num campo privado, do objeto [config], no qual encontrará todas as informações partilhadas entre todos os clientes.
- A forma como os nomes das vistas devem ser resolvidos:
<!-- gestor de vistas -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
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 lhe indicar qual a vista que deve apresentar. A partir dessa chave, podem existir várias estratégias para gerar a vista associada à chave. A estratégia escolhida é a definida pelo bean [viewResolver]. Aqui, este bean está associado à classe [org.springframework.web.servlet.view.InternalResourceViewResolver] com diferentes parâmetros de inicialização. Sem entrar em pormenores, o bean [viewResolver] indica aqui que, se a chave da vista for «XX», então a vista gerada será [/vues/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, é possível passar 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). Neste caso, o ficheiro existirá, mas estará vazio. Não será utilizado. Deve ser colocado no [ClassPath] da aplicação. Aqui, será colocado no [WEB-INF/classes]. No Eclipse, obtém-se este resultado colocando-o no [WEB-INF/src]:

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

É criada uma única nova vista: redirpanier.jsp. Esta serve apenas 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"/>
Sugere-se ao leitor que releia a definição das diferentes vistas na versão Struts.
3.7.5. O processamento das ações
As classes necessárias para o processamento das diferentes ações foram reunidas no pacote [istia.st.articles.web.spring]:

Recordemos o funcionamento da aplicação Spring através de um exemplo:
- o utilizador acede à 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>
<!-- o carregador do contexto Spring da aplicação -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- o servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- o mapeamento de URLs -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- o documento de entrada -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
- Se esta foi a primeira solicitação à aplicação, ocorreram várias ações:
- o listener [ org.springframework.web.context.ContextLoaderListener] foi carregado
- este processou 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>
<!-- a classe de acesso aos dados -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- a classe de negócio -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- a configuração da aplicação web-->
<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 referidos 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>
<!-- os gestores de ações = controladores -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- o mapeamento da aplicação-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- gestor de vistas -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- o ficheiro de mensagens -->
<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. Este era: [http://localhost:8080/springwebarticles]. Aqui, não se solicita um URL do contexto, mas sim o próprio contexto. É, portanto, 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, convidado a redirecionar-se para o URL [http://localhost:8080/springwebarticles/main.do]. E assim o faz.
- O controlador Spring recebe, assim, um novo pedido. Este 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>
<!-- os gestores de ações = controladores -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
....
</bean>
<!-- o mapeamento da aplicação-->
<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-lhe que a ação [/main.do] deve ser processada pelo bean [ListController].
- A solicitação do cliente é transmitida ao método [handleRequest] do bean [ListController]. Este executa a sua tarefa e devolve ao controlador a chave da vista a apresentar. Aqui, se tudo correr bem, essa chave será [liste].
- O controlador Spring utiliza o bean [viewResolver] do ficheiro de configuração [springwebarticles-servlet.xml] para determinar a vista associada a essa 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
Já referimos que, no arranque da aplicação, os beans do ficheiro [applicationContext.xml] eram instanciados:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- a classe de acesso aos dados -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- a classe de negócio -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- a configuração da aplicação web-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
Conhecemos os beans [articlesDao, articlesDomain], mas não o bean [config]. Este é 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 {
// campos privados
private IArticlesDomain articlesDomain = null;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private final String lienActionListe = "Liste des articles";
private final String lienActionPanier = "Voir le panier";
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// getters e 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;
}
// inicialização da aplicação web
public void init() {
// guardamos algumas URLs da aplicação
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);
// Está concluído
return;
}
}
|
Esta classe executa o que o método [init] de um servlet de aplicação web faz. Inicializa a aplicação. Aqui, isso ocorre da seguinte forma:
- porque o bean [config] está definido da seguinte forma em [applicationContext.xml]:
<!-- a configuração da aplicação web-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
quando foi criado, o seu campo privado [articlesDomain] foi inicializado
- depois, devido ao atributo [init-method="init"] do bean acima, o método [init] da classe associada ao bean é executado. Aqui, este método inicializa os três dicionários [hActionListe, hActionPanier, hActionValidationPanier] utilizados para gerar os três links possíveis do menu apresentado ao utilizador.
- São criados acessores públicos para tornar estes campos privados acessíveis às instâncias do tipo [Controller] que irão processar as 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 criação da classe [Controller] consiste, na maioria das vezes, em:
- fazer um copiar/colar da classe [Action] que tinha sido 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 na classe [springwebarticles-servlet.xml] da seguinte forma:
| <!-- o mapeamento da aplicação-->
<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 detalhar 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 {
// a configuração da aplicação web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processamento do pedido
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// solicita-se a lista de artigos
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// o erro é registado
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// é apresentada a página de erros
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// enviar a vista de erros
return new ModelAndView("erreurs");
}
// exibe a lista de artigos
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// enviar a visualização
return new ModelAndView("liste");
}
}
|
Comentários:
- a classe possui 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 se pode ver acima, o campo [config] de [ListController] é inicializado com o bean [config]. Do que se trata? Trata-se do bean [config] definido em [applicationContext.xml], c.a.d. Uma instância de [istia.st.articles.web.spring.Config] descrita acima.
- Escrever o código de uma classe [Controller] consiste essencialmente em escrever o código do seu método [handleRequest]
- solicita-se a lista de artigos ao modelo. Este está acessível através de [config.getArticlesDomain()]. Se ocorrer uma exceção, é enviada a vista [ERREURS]. O resultado devolvido por [handleRequest] deve ser do tipo [ModelAndView]. É possível instanciar esta classe de várias formas. Aqui, e tal será sempre o caso, constrói-se uma instância de [ModelAndView], passando-lhe a chave da vista a apresentar. Recorde-se que, devido à configuração do bean [viewResolver], solicitar a exibição da vista com a chave XX resultará no envio da vista [/vues/XX.jsp].
| // solicita-se a lista de artigos
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// regista-se o erro
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// exibe a página de erros
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// enviar a visualização de erros
return new ModelAndView("erreurs");
}
|
- Se não houver erros, é a vista [LISTE] que é enviada:
| // exibe-se a lista de artigos
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// enviar a visualização
return new ModelAndView("liste");
|
3.7.7.3. infos.do
Esta ação serve para fornecer informações sobre um dos artigos apresentados na vista [LISTE]:
Esta ação está definida em [springwebarticles-servlet.xml] por:
| <!-- o mapeamento da aplicação-->
<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 da 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 {
// a configuração da aplicação web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processamento do pedido
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// a lista de erros
ArrayList erreurs = new ArrayList();
// recuperamos o ID solicitado
String strId = request.getParameter("id");
// alguma coisa?
if (strId == null) {
// não é normal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// converte-se strId na íntegra
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// não é normal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// solicita-se o artigo da chave id
Article article = null;
try {
article = config.getArticlesDomain().getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// anormal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
if (article == null) {
// anormal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// o artigo é colocado na sessão
request.getSession().setAttribute("article", article);
// é apresentada a página de informações
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 constar no URL. Este deve, de facto, ter o formato [/infos.do?id=X]. São realizados vários testes para verificar a presença e a validade do parâmetro [id]. Em caso de problema, é enviada a vista [ERREURS].
- Se [id] for válido, o artigo correspondente é solicitado à camada [domain]. Se esta lançar uma exceção ou se o artigo não for encontrado, também neste caso é enviada a vista [ERREURS].
- Se tudo correr bem, o artigo obtido é colocado na sessão. Este é um ponto discutível. Aqui, parte-se do princípio de que o cliente poderá vir a comprar esse artigo. Se o fizer, irá-se buscá-lo à sessão, em vez de o solicitar novamente à camada [domain].
- Por fim, a vista [INFOS] é apresentada.
3.7.7.4. achat.do
Esta ação serve para comprar o artigo apresentado pela vista anterior [INFOS]:

Se analisarmos o código HTML desta vista, verificamos que a baliza <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>
Vemos, portanto, que o formulário é enviado para o controlador com a ação [achat.do].
Esta ação está configurada da seguinte forma em [springwebarticles-servlet.xml]:
<!-- o mapeamento da aplicação-->
<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 da 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 {
// a configuração da aplicação web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processamento do pedido
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// recuperação da quantidade comprada
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// quantidade incorreta
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
// recuperação da sessão do cliente
HttpSession session = request.getSession();
// recuperar o artigo adicionado à sessão
Article article = (Article) session.getAttribute("article");
// sessão expirada?
if (article == null) {
// exibe-se a página de erros
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// cria-se a nova compra
Achat achat = new Achat(article, qté);
// adiciona-se a compra ao cesto do cliente
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// volta-se à lista de artigos
return new ModelAndView("index");
}
}
|
Comentários:
- 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 consulta: [id]: número do artigo adquirido; [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 acompanhada de uma mensagem de erro:
| // a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// recupera-se a quantidade comprada
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// quantidade incorreta
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
|
- o artigo adquirido é recuperado da sessão. Esta pode ter expirado. Nesse caso, é enviada a vista [ERREURS]:
| // recuperar a sessão do cliente
HttpSession session = request.getSession();
// recuperar o artigo adicionado à sessão
Article article = (Article) session.getAttribute("article");
// sessão expirada?
if (article == null) {
// exibe-se a página de erros
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 artigo é colocado no cesto, também recuperado da sessão:
| // cria-se a nova compra
Achat achat = new Achat(article, qté);
// adiciona-se a compra ao cesto do cliente
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- por fim, envia-se a vista [LISTE]:
// volta-se à lista de artigos
return new ModelAndView("index");
- Acima, enviamos a vista [/vues/index.jsp]. Sabemos que esta solicita ao navegador do cliente que se redirecione para o URL [/main.do]. É este redirecionamento que irá apresentar a lista de artigos.
3.7.7.5. panier.do
Esta ação serve 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 a que este link remete é a seguinte:

Esta ação está configurada da seguinte forma em [springwebarticles-servlet.xml]:
| <!-- o mapeamento da aplicação-->
<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 da 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 {
// a configuração da aplicação web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processamento do pedido
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// exibir o carrinho
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// cesto vazio
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("paniervide");
} else {
// há algo no carrinho
request.setAttribute("actions", new Hashtable[] {
config.getHActionListe(), config.getHActionValidationPanier() });
return new ModelAndView("panier");
}
}
}
|
Comentários:
- o cesto de compras é obtido na sessão em que se encontra normalmente. A sessão pode ter expirado e, nesse caso, não existe cesto de compras. Não consideramos este caso como um erro, mas simplesmente assumimos que o cesto de compras está vazio.
- Se o carrinho estiver vazio, é apresentada a vista [PANIERVIDE]
- caso contrário, é apresentada a vista [PANIER]
3.7.7.6. retirerachat.do
Esta ação serve para remover um produto do cesto:

Se analisarmos o código HTML da ligação acima, temos 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 remover do cesto de compras. Esta ação está configurada da seguinte forma em [springwebarticles-servlet.xml]:
| <!-- o mapeamento da aplicação-->
<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 {
// a configuração da aplicação web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processamento do pedido
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// recuperação do carrinho
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessão expirada
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// recuperação do ID do artigo a retirar
String strId = request.getParameter("id");
// alguma coisa?
if (strId == null) {
// não é normal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// convertemos strId para o formato inteiro
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// não é normal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// eliminamos a compra
panier.enlever(id);
// volta a apresentar o cesto
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 este estiver incorreto, é enviada a vista [ERREURS].
- Caso contrário, a compra é removida do cesto:
// remover a compra
panier.enlever(id);
- em seguida, o cesto é novamente apresentado:
// voltar a apresentar o cesto
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
Recorde-se o código da vista [/vues/redirpanier.jsp]:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
Vemos que o cliente será redirecionado para a ação [/panier.do]. Esta já foi descrita. Irá apresentar a vista [PANIER] ou [PANIERVIDE], consoante o estado do cesto de compras.
3.7.7.7. validerpanier.do
Esta ação serve para validar as compras efetuadas pelo cliente. Concretamente, isto traduz-se numa única ação: os stocks 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 da ligação [Valider le panier] é o seguinte:
<a href="validerpanier.do">Valider le panier</a>
Quando esta ligação é ativada, os stocks são reduzidos e a lista de artigos é novamente apresentada.
Esta ação está configurada da seguinte forma em [springwebarticles-servlet.xml]:
| <!-- o mapeamento da aplicação-->
<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 da 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 {
// a configuração da aplicação web
Config config;
public void setConfig(Config config) {
this.config = config;
}
// processamento do pedido
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// o comprador confirmou o seu carrinho
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessão expirada
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// o cesto de compras está a ser validado
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// situação anormal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// Recuperam-se os eventuais erros
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");
}
// Tudo parece estar em ordem OK - Apresenta-se a lista de artigos
return new ModelAndView("index");
}
}
|
Comentários:
- recupera-se o cesto de compras da sessão. Se esta tiver expirado, é enviada a vista [ERREURS]:
| // a lista de erros nesta ação
ArrayList erreurs = new ArrayList();
// o comprador confirmou o seu carrinho
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessão expirada
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
|
- Validam-se as compras do carrinho. Podem ocorrer erros se os stocks forem insuficientes para satisfazer as compras. Nesse caso, envia-se a vista [ERREURS]:
| // o cesto de compras é validado
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// situação anómala
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");
}
// Recuperam-se os eventuais erros
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 correr bem, volta a ser apresentada a lista de artigos:
// Tudo parece estar em ordem OK - Apresenta-se a lista de artigos
return new ModelAndView("index");
Sabemos que a vista [/vues/index.jsp] redireciona o cliente para a ação [/main.do]. Esta ação irá apresentar a vista [LISTE].