Skip to content

3. Artigo 2.º - Exemplos de arquiteturas web de três camadas

Objetivos deste artigo:

  • Arquiteturas de três camadas
  • arquitetura web MVC básica
  • Arquitetura Struts MVC
  • Arquitetura Spring MVC

Ferramentas utilizadas:

  • Spring: http://www.springframework.org/
  • Ibatis SqlMap: http://www.ibatis.com/
  • JUnit: http://www.junit.org/index.htm
  • Eclipse: http://www.eclipse.org/
  • Struts: http://struts.apache.org/
  • Firebird: http://firebird.sourceforge.net/: SGBD, controlador JDBC. Na verdade, qualquer fonte JDBC serve.
  • IBExpert, Edição Pessoal: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (março de 2005). O IBExpert permite-lhe administrar graficamente o SGBD Firebird.
  • Tomcat: http://jakarta.apache.org/tomcat/
  • Plugin Tomcat para o Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. Consulte também o documento https://tahe.developpez.com/java/eclipse/

A compreensão deste documento requer vários pré-requisitos. Alguns deles podem ser encontrados em documentos que escrevi. Nesses casos, faço referência aos mesmos. Obviamente, isto é apenas uma sugestão, e os leitores são livres de utilizar os seus recursos preferidos.

  • Linguagem Java: [https://tahe.developpez.com/java/cours]
  • Programação Web em Java: [https://tahe.developpez.com/java/web/]
  • Programação Web com Java, Eclipse e Tomcat: [https://tahe.developpez.com/java/eclipse/]
  • Programação Web com Struts: [https://tahe.developpez.com/java/struts/]
  • Utilização do aspeto IoC do Spring: [https://tahe.developpez.com/java/springioc]
  • Biblioteca de tags JSTL: [https://tahe.developpez.com/java/eclipse/] (em parte)
  • Documentação do Ibatis SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (março de 2005).

As ideias contidas neste documento têm origem num livro que li no verão de 2004, uma obra magnífica de Rod Johnson: J2EE Development without EJB, publicado pela Wrox.


3.1. A aplicação webarticles

Aqui, gostaríamos de apresentar alguns componentes de uma aplicação web de comércio eletrónico. Esta aplicação permitirá aos clientes web

  • visualizar uma lista de itens de uma base de dados
  • adicionar alguns deles a um carrinho de compras eletrónico
  • confirmar o carrinho. Esta confirmação irá simplesmente atualizar os níveis de inventário dos artigos adquiridos na base de dados.

As diferentes visualizações apresentadas ao utilizador serão as seguintes:

  • a vista [LISTA], que exibe uma lista de artigos à venda

Image

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

Image

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

Image

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

Image

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

Image

3.2. Arquitetura geral da aplicação

Queremos criar uma aplicação com a seguinte arquitetura de três camadas:

  • As três camadas são tornadas independentes através da utilização de interfaces Java
  • A integração das diferentes camadas é gerida pelo Spring
  • Cada camada está contida em pacotes separados: web (camada de interface do utilizador), domínio (camada de negócios) e DAO (camada de acesso aos dados).

Partiremos do princípio de que as camadas [domain] e [DAO] já estão implementadas. Iremos concentrar-nos apenas na camada [web], que propomos construir de várias formas:

  • utilizando a tecnologia clássica de controladores de servlet — páginas JSP
  • utilizando a tecnologia Struts MVC
  • utilizando a tecnologia Spring MVC

Em todos os casos, a aplicação seguirá uma arquitetura MVC (Model-View-Controller). Se nos referirmos ao diagrama em camadas acima, a arquitetura MVC encaixa-se nele da seguinte forma:

O processamento de um pedido do cliente segue estes passos:

  1. O cliente envia uma solicitação ao controlador. Este controlador é um servlet que lida com todas as solicitações do cliente. É o ponto de entrada da aplicação. É o C em MVC.
  2. O controlador processa esta solicitação. Para tal, pode necessitar da assistência da camada de negócios, conhecida como o «M» na estrutura MVC.
  3. O controlador recebe uma resposta da camada de negócios. A solicitação do cliente foi processada. Isso pode desencadear várias respostas possíveis. Um exemplo clássico é
    • uma página de erro, caso a solicitação não tenha sido processada corretamente
    • uma página de confirmação, caso contrário
  4. O controlador escolhe a resposta (= vista) a enviar ao cliente. Trata-se, na maioria das vezes, de uma página que contém elementos dinâmicos. O controlador fornece estes à vista.
  5. A vista é enviada ao cliente. Esta é a V em MVC.

3.3. O Modelo

Aqui, analisamos o M em MVC. O modelo é composto pelos seguintes elementos:

  1. classes de negócio
  2. classes de acesso a dados
  3. a base de dados

3.3.1. A base de dados

A base de dados contém apenas uma tabela chamada ARTICLES. Esta tabela foi gerada utilizando os seguintes comandos SQL:

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


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

/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
chave primária que identifica um item de forma única
nome
nome do item
preço
o seu preço
stock atual
stock atual
stock mínimo
o nível de stock abaixo do qual deve ser efetuada uma nova encomenda

Nos testes a seguir, foi utilizada uma base de dados [Firebird]. O [Firebird] é um SGBD de «código aberto». O controlador JDBC [firebirdsql-full.jar] está localizado na pasta [WEB-INF/lib] da aplicação web.

3.3.2. Os pacotes de modelos

O modelo M é fornecido aqui na forma de três arquivos:

  • istia.st.articles.dao: contém as classes de acesso aos dados para a camada [DAO]
  • istia.st.articles.exception: contém uma classe de exceção para esta gestão de artigos
  • istia.st.articles.domain: contém as classes de negócio da camada [domain]
arquivo
conteúdo
função
istia.st.articles.dao
- contém o pacote [istia.st.articles.dao], que por sua vez contém os seguintes elementos:
- [IArticlesDao]: a interface para aceder à camada Dao. Esta é a única interface visível para a camada [domain]. Não vê outras.
- [Article]: classe que define um artigo
- [ArticlesDaoSqlMap]: classe de implementação para a interface [IArticlesDao] utilizando a ferramenta SqlMap
camada de acesso a dados – está localizada inteiramente dentro da camada [dao] da arquitetura de três camadas da aplicação web
istia.st.articles.domain
- contém o pacote [istia.st.articles.domain], que por sua vez contém os seguintes elementos:
- [IArticlesDomain]: a interface para aceder à camada [domain]. Esta é a única interface visível para a camada web. Não vê outras.
- [ArticlePurchases]: uma classe que implementa [IArticlesDomain]
- [Purchase]: uma classe que representa a compra de um cliente
- [ShoppingCart]: uma classe que representa o total de compras de um cliente
representa o modelo de compra web - está localizada inteiramente na camada [domain] da arquitetura de 3 camadas da aplicação web
istia.st.articles.exception
- contém o pacote [istia.st.articles.exception], que por sua vez contém os seguintes elementos:
- [UncheckedAccessArticlesException]: classe que define uma exceção [RuntimeException]. Este tipo de exceção é lançado pela camada [dao] assim que ocorre um problema de acesso aos dados.
 

3.3.3. O pacote [istia.st.articles.dao]

A classe que define um artigo é a seguinte:

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

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

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

  public Article(int id, String nom, double prix, int stockActuel,
      int stockMinimum) {
     // init instance attributes
    setId(id);
    setNom(nom);
    setPrix(prix);
    setStockActuel(stockActuel);
    setStockMinimum(stockMinimum);
  }

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

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

  public String getNom() {
    return nom;
  }

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

  public double getPrix() {
    return prix;
  }

  public void setPrix(double prix) {
     // valid price?
    if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
    this.prix = prix;
  }

  public int getStockActuel() {
    return stockActuel;
  }

  public void setStockActuel(int stockActuel) {
     // valid stock?
    if (stockActuel < 0)
      throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
    this.stockActuel = stockActuel;
  }

  public int getStockMinimum() {
    return stockMinimum;
  }

  public void setStockMinimum(int stockMinimum) {
     // valid stock?
    if (stockMinimum < 0)
      throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
    this.stockMinimum = stockMinimum;
  }

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

Esta classe fornece:

  1. um construtor para definir as 5 informações de um item
  2. acessores, frequentemente chamados de getters/setters, utilizados para ler e escrever as 5 informações. Os nomes destes métodos seguem o padrão JavaBean. A utilização de objetos JavaBean na camada DAO para interagir com os dados do SGBD é uma prática padrão.
  3. validação dos dados introduzidos para o item. Se os dados forem inválidos, é lançada uma exceção.
  4. Um método toString que devolve o valor de um item como uma cadeia de caracteres. Isto é frequentemente útil para depurar uma aplicação.

A interface [IArticlesDao] é definida da seguinte forma:

package istia.st.articles.dao;

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

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

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

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

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

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

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

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

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

As funções dos vários métodos na interface são as seguintes:

getAllArticles
retorna todos os itens da tabela ARTICLES numa lista de objetos [Article]
clearAllArticles
limpa a tabela ARTICLES
getArticleById
retorna o objeto [Article] identificado pela sua chave primária
addArticle
permite adicionar um artigo à tabela ARTICLES
modifyArticle
permite modificar um item na tabela [ARTICLES]
deleteItem
permite eliminar um item da tabela [ARTICLES]
updateItemStock
permite-lhe modificar o stock de um artigo na tabela [ARTICLES]

A interface fornece aos programas clientes vários métodos definidos exclusivamente pelas suas assinaturas. Não se preocupa com a forma como esses métodos serão efetivamente implementados. Isto confere flexibilidade a uma aplicação. O programa cliente efetua chamadas a uma interface, em vez de a uma implementação específica dessa interface.

A escolha de uma implementação específica será feita através de um ficheiro de configuração do Spring. Aqui, propomos implementar a interface IArticlesDao utilizando um produto de código aberto chamado SqlMap. Isto permitir-nos-á remover todas as instruções SQL do código Java.

A classe de implementação [ArticlesDaoSqlMap] é definida da seguinte forma:

package istia.st.articles.dao;

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

public class ArticlesDaoSqlMap implements IArticlesDao {

  // Fields
  private SqlMapClient sqlMap;

   // Constructors
  public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }

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

Todos os métodos de acesso aos dados foram sincronizados para evitar problemas de acesso simultâneo à fonte de dados. Em qualquer momento, apenas um segmento de execução tem acesso a um determinado método.

A classe [ArticlesDaoSqlMap] utiliza a ferramenta [Ibatis SqlMap]. A vantagem desta ferramenta é que permite separar o código SQL para acesso aos dados do código Java. Este é então colocado num ficheiro de configuração. Teremos oportunidade de voltar a este assunto mais tarde. Para ser instanciada, a classe [ArticlesDaoSqlMap] requer um ficheiro de configuração cujo nome é passado como parâmetro ao construtor da classe. Este ficheiro de configuração define as informações necessárias para:

  • aceder ao SGBD que contém os artigos
  • gerir um conjunto de ligações
  • gerir transações

No nosso exemplo, será denominado [sqlmap-config-firebird.xml] e definirá o acesso a uma base de dados Firebird:

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

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

O ficheiro de configuração [articles.xml] referido acima define como construir uma instância da classe [istia.st.articles.dao.Article] a partir de uma linha na tabela [ARTICLES] do SGBD. Define também as consultas SQL que permitirão à camada [dao] recuperar dados da fonte de dados Firebird.

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

     <!-- an alias to the istia.st.articles.dao.Article class -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- mapping ORM :  row table ARTICLES - instance class Article -->
  <resultMap id="article" class="article">
    <result property="id" column="ID"/>
    <result property="nom" column="NOM"/>
    <result property="prix" column="PRIX"/>
    <result property="stockActuel" column="STOCKACTUEL"/>
    <result property="stockMinimum" column="STOCKMINIMUM"/>
  </resultMap>

     <!-- query SQL to obtain all items -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- query SQL to delete all items -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- the SQL query to insert an article -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>

     <!-- the SQL query to delete a given item -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- query SQL to modify a given item -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nom#,
    prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
    id=#id#
</statement>

     <!-- query SQL to obtain a given item -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- query SQL to modify the stock of a given item -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#mouvement#
    where id=#id# and stockActuel+#mouvement#&gt;=0
</statement>
</sqlMap>

O código do pacote [dao] pode ser encontrado no apêndice.

3.3.4. O pacote [istia.st.articles.domain]

A interface [IArticlesDomain] desacopla a camada [business] da camada [web]. Esta última acede à camada [business/domain] através desta interface sem se preocupar com a classe que a implementa efetivamente. A interface define as seguintes ações para aceder à camada de negócios:

 package istia.st.articles.domain;

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

public abstract interface IArticlesDomain {

   // Methods
  void acheter(Panier panier);
  List getAllArticles();
  Article getArticleById(int idArticle);
  ArrayList getErreurs();
}
List getAllArticles()
retorna a lista de objetos [Article] a serem exibidos ao cliente
Artigo getArticleById(int idArtigo)
retorna o objeto [Article] identificado por [idArticle]
void comprar(Carrinho carrinho)
processa o carrinho do cliente, diminuindo o stock dos itens comprados pela quantidade adquirida — pode falhar se o stock for insuficiente
ArrayList getErrors()
retorna a lista de erros que ocorreram — vazia se não houver erros

Aqui, a interface [IArticlesDomain] será implementada pela seguinte classe [PurchaseItems]:

package istia.st.articles.domain;

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

public class AchatsArticles implements IArticlesDomain {

   // Fields
  private IArticlesDao articlesDao;
  private ArrayList erreurs;

   // Manufacturers
  public AchatsArticles(IArticlesDao articlesDao) { }

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

Esta classe implementa os quatro métodos da interface [IArticlesDomain]. Possui dois campos privados:

IArticlesDao articlesDao
o objeto de acesso a dados fornecido pela camada de acesso a dados
ArrayList errors
a lista de eventuais erros

Para criar uma instância da classe, deve fornecer o objeto que permite o acesso aos dados do SGBD:

public PurchasesItems(IArticlesDao articlesDao)
construtor

A classe [Purchase] representa uma compra de um cliente:

package istia.st.articles.domain;

public class Achat {

   // Fields
  private Article article;
  private int qte;

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

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

A classe [Purchase] é um JavaBean com os seguintes campos e métodos:

item
o artigo adquirido
quantidade
a quantidade comprada
duplicar getTotal()
retorna o valor da compra
String toString()
representação do objeto como string

A classe [Cart] representa o total das compras do cliente:

package istia.st.articles.domain;

// Imports
import java.util.ArrayList;

public class Panier {

  // Fields
  private ArrayList achats;

   // Manufacturers
  public Panier() { }

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

A classe [Cart] é um JavaBean com os seguintes campos e métodos:

compras
a lista de compras do cliente - uma lista de objetos do tipo [Purchase]
void add(Purchase compra)
adiciona uma compra à lista de compras
void remove(int itemId)
remove a compra correspondente ao ID do item idArticle
double getTotal()
retorna o valor total das compras
String toString()
retorna a representação em string do carrinho de compras
ArrayList getPurchases()
retorna a lista de compras

O código do pacote [domain] pode ser encontrado no apêndice.

3.3.5. O pacote [istia.st.articles.exception]

Este pacote contém a classe que define a exceção lançada pela camada [dao] quando esta encontra um problema ao aceder à fonte de dados:

package istia.st.articles.exception;

public class UncheckedAccessArticlesException
    extends RuntimeException {

  public UncheckedAccessArticlesException() {
    super();
  }

  public UncheckedAccessArticlesException(String mesg) {
    super(mesg);
  }

  public UncheckedAccessArticlesException(String mesg, Throwable th) {
    super(mesg, th);
  }
}

3.3.6. Teste do modelo

O modelo M foi testado no Eclipse com a seguinte configuração:

Image

Comentários:

  • Em [WEB-INF/lib] encontrará:
    • os arquivos necessários para a ferramenta [ibatis SqlMap], responsável pelo acesso ao SGBD Firebird: ibatis-*.jar
    • o ficheiro necessário para a ferramenta [Spring]: spring.jar
    • o controlador JDBC para o SGBD [Firebird]: firebirdsql-full.jar
    • os arquivos necessários para o registo: log4-*.jar, commons-logging.jar
    • os três arquivos para o modelo testado: istia.st.articles.*.jar
    • o arquivo necessário para a ferramenta de testes [junit]
  • Em [WEB-INF/src] encontram-se os ficheiros de configuração que serão automaticamente copiados para [WEB-INF/classes] pelo Eclipse:
    • os ficheiros de configuração para a ferramenta [sqlmap]: sqlmap-config-firebird.xml, articles.xml
    • os ficheiros de configuração da ferramenta [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
    • o ficheiro de configuração para a ferramenta [log4j]: log4j.properties
  • No pacote [istia.st.articles.tests], encontrará as classes de teste do modelo

3.3.6.1. Testes para a camada [dao]

A classe de teste JUnit para a camada [dao] é a seguinte. A sua leitura ajuda a compreender como os métodos da interface [IArticlesDao] são utilizados:

package istia.st.articles.tests.dao;

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

// test the ArticlesDaoSqlMap class
public class JunitModeleDaoArticles extends TestCase {

     // an instance of the class under test
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // retrieves a data access instance
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }

    public void testGetAllArticles() {
         // displays articles
        listArticles();
    }

    public void testClearAllArticles() {
         // empties item table
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
    }

    public void testAjouteArticle() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         //the poster
        listArticles();
    }

    public void testSupprimeArticle() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // delete
        articlesDao.supprimeArticle(4);
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(1, articles.size());
         // displays the table
        listArticles();
    }

    public void testModifieArticle() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
         // modification
        articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
         // getById
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getPrix(), 44, 1e-6);
         // displays the table
        listArticles();
    }

    public void testGetArticleById() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

    private void listArticles() {
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
         // display read articles
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

    public void testChangerStockArticle() throws InterruptedException {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // insertion
        int nbArticles = articlesDao.ajouteArticle(new Article(3, "article3",
                30, 101, 3));
        assertEquals(nbArticles, 1);
        nbArticles = articlesDao.ajouteArticle(new Article(4, "article4", 40,
                40, 4));
        assertEquals(nbArticles, 1);
         // creation of 100 threads to update the stock of item 3
        Thread[] taches = new Thread[100];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
            taches[i].start();
        }
         // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // retrieve item 3 and check stock
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        assertEquals(1, unArticle.getStockActuel());
         // modification stock article 4
        boolean erreur = false;
        int nbLignes = articlesDao.changerStockArticle(4, -100);
        assertEquals(0, nbLignes);
         // displays the table
        listArticles();
    }
}

Comentários:

  • A classe de teste armazena, utilizando o seu método setUp, uma instância da classe em teste:
1
2
3
4
5
6
7
8
    // une instance de la classe testée
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }
  • O objeto a ser testado é fornecido pelo [Spring]. Acima, solicitamos o bean do Spring denominado [articlesDao]. Este bean está definido no ficheiro de configuração do Spring [spring-config-test-dao.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
</beans>

Como mostrado acima, o bean [articlesDao] é uma instância da classe [istia.st.articles.dao.ArticlesDaoSqlMap]. Esta classe possui um construtor que recebe o nome do ficheiro de configuração da ferramenta [SqlMap] como parâmetro. Esse nome é fornecido aqui. É o [sqlmap-config-firebird.xml]. Este ficheiro já foi descrito. Ele fornece todas as informações necessárias para aceder aos dados do SGBD.

O método [testChangerStockArticle] cria 100 threads responsáveis por diminuir o stock de um determinado artigo. O objetivo aqui é testar o acesso simultâneo ao SGBD. Como o método [changerStockArticle] da classe [istia.st.articles.dao.ArticlesDaoSqlMap] foi sincronizado, este teste é bem-sucedido. Se removemos a sincronização, o teste já não é bem-sucedido. A classe responsável pela atualização do stock é a seguinte:

package istia.st.articles.tests;

import istia.st.articles.dao.IArticlesDao;

public class ThreadMajStock extends Thread {

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

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

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

    /**
     * décrémente le stock de l'article 3 d'une unité fait un suivi écran des
     * opérations
     */
    public void run() {
         // follow-up
        System.out.println(name + " lancé");
         // modification stock article 3
        articlesDao.changerStockArticle(3, -1);
         // follow-up
        System.out.println(name + " terminé");
    }
}
  • A classe acima diminui o stock do item n.º 3 em 1

3.3.6.2. [domínio] testes de camada

A classe de teste JUnit para a camada [domain] é a seguinte:

package istia.st.articles.tests.domain;

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

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

// test the ArticlesDaoSqlMap class
public class JunitModeleDomainArticles extends TestCase {

     // an instance of the domain access class
    private IArticlesDomain articlesDomain;

     // an instance of the data access class
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // retrieves a domain access instance
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // retrieves a data access instance
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }

     // retrieve a specific item
    public void testGetArticleById() {
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // reads the ARTICLES table
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDomain.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

     // screen display
    private void listArticles() {
         // reads the ARTICLES table
        List articles = articlesDomain.getAllArticles();
         // display read articles
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

     // article purchases
    public void testAchatPanier(){
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // reads the ARTICLES table
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // create a basket with two purchases
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // checks
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // shopping cart validation
        articlesDomain.acheter(panier);
         // checks
        assertEquals(0,articlesDomain.getErreurs().size());
        assertEquals(0,panier.getAchats().size());
         // search article n° 3
        article3=articlesDomain.getArticleById(3);
        assertEquals(20,article3.getStockActuel());
         // search article n° 4
        article4=articlesDomain.getArticleById(4);
        assertEquals(30,article4.getStockActuel());
         // new basket
        panier.ajouter(new Achat(article3,100));
         // shopping cart validation
        articlesDomain.acheter(panier);
         // checks - we bought too much
         // we must have an error
        assertEquals(1,articlesDomain.getErreurs().size());
         // search article n° 3
        article3=articlesDomain.getArticleById(3);
         // its stock must not have changed
        assertEquals(20,article3.getStockActuel());    
    }

     // withdraw purchases
    public void testRetirerAchats(){
         // delete contents of ARTICLES
        articlesDao.clearAllArticles();
         // reads the ARTICLES table
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // insertion
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // reads the ARTICLES table
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // create a basket with two purchases
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // checks
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // add a previously purchased item
        panier.ajouter(new Achat(article3,10));
         // checks
         // the total must be increased to 1000
        assertEquals(1000.0,panier.getTotal(),1e-6);
         // always 2 items in the basket
        assertEquals(2,panier.getAchats().size());
         // qty item 3 increased to 20
        Achat achat=(Achat)panier.getAchats().get(0);
        assertEquals(20,achat.getQte());
         // article 3 is removed from the basket
        panier.enlever(3);
         // checks
         // the total must be increased to 400
        assertEquals(400.0,panier.getTotal(),1e-6);
         // 1 item only in basket
        assertEquals(1,panier.getAchats().size());
         // this must be article no. 4
        assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
    }
}

Comentários:

  • A classe de teste utiliza o seu método setUp para armazenar uma instância da classe em teste, bem como uma instância da classe de acesso aos dados. Este último ponto é controverso. Teoricamente, a classe de teste não deveria precisar de aceder à camada [DAO], da qual nem sequer deveria ter conhecimento. Aqui, ignorámos esta «ética», que, se fosse seguida, nos teria obrigado a criar novos métodos na nossa interface [IArticlesDomain].
    // une instance de la classe d'accès au domaine
    private IArticlesDomain articlesDomain;

    // une instance de la classe d'accès aux données
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
        // récupère une instance d'accès au domaine
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }
  • O objeto a ser testado é fornecido pelo [Spring]. Acima, solicitamos o bean do Spring denominado [articlesDomain]. Este bean está definido no ficheiro de configuração do Spring [spring-config-test-domain.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Como mostrado acima, o bean [articlesDomain] é uma instância da classe [istia.st.articles.domain.AchatsArticles]. Esta classe possui um construtor que espera, como parâmetro, um objeto que forneça acesso à camada [dao] do tipo [IArticlesDao]. Aqui, o ficheiro de configuração especifica que este objeto é o bean denominado [articlesDao]. Isto obriga o Spring a instanciar este bean. A instanciação do bean [articlesDao] foi explicada anteriormente. Assim, em última análise, foram instanciados dois beans:

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

Estas duas instanciações são desencadeadas pela primeira chamada ao Spring:

1
2
3
4
        // récupère une instance d'accès au domaine
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");

O bean [articlesDomain] é então recuperado. Durante a segunda chamada ao Spring:

1
2
3
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");

[Spring] simplesmente devolve uma referência ao bean [articlesDao] que já tinha sido criado durante a chamada anterior. Este é o princípio do singleton. Se solicitar um bean ao Spring, este instancí-lo-á caso ainda não exista; caso contrário, devolverá uma referência ao bean existente.

3.4. Aplicação web MVC de três camadas

A seguir, queremos construir a seguinte aplicação web de três camadas:

A aplicação terá uma arquitetura MVC. O modelo M já foi escrito e testado. É o modelo descrito anteriormente. É-nos fornecido em três arquivos [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Precisamos de escrever o controlador C e as vistas V.

Primeiro, vamos considerar uma abordagem clássica, em que:

  • o controlador C é gerido por um único servlet
  • as vistas V são geridas por páginas JSP

3.5. Arquitetura MVC baseada num servlet controlador e em páginas JSP

A arquitetura MVC da aplicação será a seguinte:

M = Modelo
Classes de negócios, classes de acesso a dados e a base de dados
V = Visualizações
Páginas JSP
C = Controlador
o servlet que processa os pedidos do cliente

3.5.1. O modelo

Foi apresentado anteriormente. Consiste nos arquivos Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.5.2. As vistas

As vistas correspondem às apresentadas no início deste documento:

LIST
list.jsp
As vistas estão localizadas na pasta [vues] da aplicação
INFORMAÇÃO
info.jsp
CARRINHO
cart.jsp
CARRINHO VAZIO
empty-cart.jsp
ERROS
errors.jsp

3.5.3. O Controlador

O controlador consistirá num único servlet denominado [WebArticles]. Este irá tratar de vários pedidos do cliente. Estes pedidos serão identificados pela presença de um parâmetro [action] no pedido HTTP do cliente:

solicitação
significado
ação do controlador
respostas possíveis
ação=lista
o cliente deseja a lista de
itens
- solicita a lista de itens ao
empresa
- [LISTA]
- [ERROS]
action=info
O cliente solicita
Informações sobre um dos itens apresentados na vista
[LISTA]
- solicita o item à camada de negócios
- [INFO]
- [ERROS]
ação=compra
O cliente compra um artigo
- solicita o artigo à camada de negócios e
adiciona-o ao carrinho do cliente
- [INFO] se houver erro na quantidade
- [LIST] se não houver erro
ação=remover compra
o cliente deseja remover uma
compra do seu carrinho
- recuperar o carrinho da sessão e modificá-lo
- [CARRINHO]
- [CARRINHO VAZIO]
- [ERROS]
ação=carrinho
o cliente deseja visualizar o seu
carrinho
- recupera o carrinho da sessão
- [CARRINHO DE COMPRAS]
- [CARRINHO VAZIO]
- [ERROS]
action=validate-cart
O cliente terminou as compras
e avança para o checkout
- atualiza a base de dados com os níveis de stock dos
artigos comprados
- remove do carrinho do cliente os artigos cuja
foram confirmados
- [LISTA]
- [ERROS]

3.5.4. Configuração da aplicação

O nosso objetivo será configurar a aplicação de forma a torná-la o mais flexível possível no que diz respeito a alterações como:

  1. alterações aos URLs das várias vistas
  2. alterações nas classes que implementam as interfaces [IArticlesDao] e [IArticlesDomain]
  3. alterações no SGBD, na base de dados ou na tabela de artigos

3.5.5. Alterações nas URLs

Os nomes dos URLs das vistas serão colocados no ficheiro de configuração [web.xml] da aplicação, juntamente com alguns outros parâmetros:

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

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

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

No [web.xml]

  • as URLs das várias vistas da aplicação
  • o nome [springConfigFileName] do ficheiro de configuração Spring que permitirá a criação de objetos singleton para aceder às camadas de negócio e DAO
  • a vista [/vues/index.jsp] que será apresentada quando o URL solicitado pelo cliente for /<context>, em que <context> é o contexto da aplicação

3.5.6. Alterar as classes que implementam as interfaces

De acordo com o princípio das arquiteturas de três camadas, as camadas devem estar isoladas umas das outras. Este isolamento é alcançado da seguinte forma:

  • as camadas comunicam entre si através de interfaces, e não de classes concretas
  • O código de uma camada nunca instancia a classe de outra camada para a utilizar. Simplesmente solicita uma instância da implementação da interface a uma ferramenta externa — neste caso, o [Spring] — para a camada que pretende utilizar. Para tal, sabemos que não precisa de conhecer o nome da classe de implementação, mas apenas o nome do bean do Spring para o qual pretende uma referência.

Na nossa aplicação, o ficheiro de configuração do Spring poderia ter o seguinte aspeto:

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

Para aceder à camada [business], uma classe na camada [Interface do Utilizador, UI] pode solicitar o bean [articlesDomain]. O Spring irá então instanciar um objeto do tipo [istia.st.articles.domain.AchatsArticles]. Para esta instanciação, é necessário um bean do tipo [articlesDao], ou seja, um objeto do tipo [istia.st.articles.dao.ArticlesDaoSqlMap]. O Spring irá então instanciar esse objeto. Esta instanciação basear-se-á nas informações contidas no ficheiro [sqlmap-config-firebird.xml], o ficheiro de configuração para acesso aos dados via SqlMap. No final da operação, a classe [UI] que solicitou o bean [articlesDomain] dispõe de toda a cadeia que a liga aos dados do SGBD:

3.5.7. Alterações relacionadas com o SGBD ou a base de dados

A independência da aplicação web em relação a alterações relacionadas com o SGBD ou a base de dados é aqui assegurada pelos ficheiros de configuração do SqlMap. Existem dois:

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

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

Este ficheiro refere-se a uma base de dados Firebird. Basta alterar o nome do controlador JDBC para trabalhar com um SGBD diferente.

  1. O ficheiro [articles.xml], que contém as várias instruções SQL necessárias à aplicação:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">

     <!-- an alias to the istia.st.articles.dao.Article class -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- mapping ORM :  row table ARTICLES - instance class Article -->
  <resultMap id="article" class="article">
    <result property="id" column="ID"/>
    <result property="nom" column="NOM"/>
    <result property="prix" column="PRIX"/>
    <result property="stockActuel" column="STOCKACTUEL"/>
    <result property="stockMinimum" column="STOCKMINIMUM"/>
  </resultMap>

     <!-- query SQL to obtain all items -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- query SQL to delete all items -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- the SQL query to insert an article -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#nom#,#prix#,#stockactuel#,#stockminimum#)
</statement>

     <!-- the SQL query to delete a given item -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- query SQL to modify a given item -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nom#,
    prix=#prix#,stockactuel=#stockactuel#,stockminimum=#stockminimum# where
    id=#id#
</statement>

     <!-- the SQL query to obtain a given item -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- query SQL to modify the stock of a given item -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#mouvement#
    where id=#id# and stockActuel+#mouvement#&gt;=0
</statement>
</sqlMap>

Se os nomes da tabela de produtos ou das colunas fossem alterados, teríamos de reescrever as consultas neste ficheiro de configuração sem ter de alterar o código Java. O mesmo se aplicaria se uma consulta fosse substituída por um procedimento armazenado por motivos de desempenho.

3.5.8. A arquitetura geral da aplicação [webarticles]

Uma aplicação web Java é um puzzle com muitas peças. Atribuir-lhe uma arquitetura MVC geralmente aumenta o número dessas peças. A estrutura da aplicação [webarticles] no [Eclipse] é a seguinte:

estrutura geral - abaixo estão os
 arquivos Java utilizados pelo
 Eclipse.
spring: para Spring
ibatis: para SqlMap
log4j, commons-logging: para
 do Spring e do SqlMap
firebird: para o SGBD Firebird
mysql: para o SGBD MySQL
jstl, standard: para o
 Biblioteca de tags JSTL
a pasta de código-fonte Java: contém o código Java
 , bem como os ficheiros de configuração
 O Eclipse copia automaticamente
 esses ficheiros
para [WEB-INF/classes].
É aqui que a aplicação os irá encontrar.
a pasta [WEB-INF] da aplicação: contém o
 descritor [web.xml] da aplicação, bem como os
 ficheiros de definição da biblioteca JSTL
as visualizações

3.5.9. visualizações JSP

As visualizações JSP utilizam a biblioteca de tags JSTL.

3.5.9.1. header.jsp

Para garantir a consistência entre as diferentes vistas, estas partilharão o mesmo cabeçalho, que exibe o nome da aplicação juntamente com o menu:

O menu é dinâmico e definido pelo controlador. O controlador inclui um atributo-chave «actions» no pedido enviado para a página JSP, com um valor associado de uma matriz Hastable[]. Cada elemento desta matriz é um dicionário destinado a gerar uma opção de menu no cabeçalho. Cada dicionário tem duas chaves:

  • href: o URL associado à opção do menu
  • link: o texto do menu

As outras vistas da aplicação utilizarão o cabeçalho definido por [entete.jsp] utilizando a seguinte tag JSP:

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

Em tempo de execução, esta tag incluirá o código da página [entete.jsp] na página JSP que a contém. Uma vez que o URL da página é um URL relativo (sem / final), a página [entete.jsp] será procurada no mesmo diretório que a página que contém a tag <jsp:include>.

Código:

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

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

3.5.9.2. list.jsp

Esta vista apresenta a lista de artigos disponíveis para venda:

É apresentada após um pedido para /main?action=list ou /main?action=cartvalidation. Os parâmetros do pedido do controlador são os seguintes:

ações
Objeto Hashtable[] - a matriz de opções do menu
listarticles
ArrayList de objetos do tipo [Item]
mensagem
Objeto String - mensagem a exibir na parte inferior da página

Cada link [Info] na tabela HTML de artigos tem um URL no formato [?action=infos&id=ID], em que ID é o campo id do artigo exibido.

Código:

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

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

3.5.9.3. infos.jsp

Esta página apresenta informações sobre um artigo e permite também a sua compra:

Image

É apresentada na sequência de um pedido /main?action=infos&id=ID ou de um pedido /main?action=achat&id=ID quando a quantidade comprada está incorreta. Os parâmetros de pedido do controlador são os seguintes:

ações
Objeto Hashtable[] - a matriz de opções do menu
item
objeto do tipo [Artigo] - item a exibir
msg
Objeto String - mensagem a exibir em caso de erro com a quantidade
qte
Objeto String - valor a exibir no campo de entrada [Qty]

Os campos [msg] e [qte] são utilizados em caso de erro de introdução de dados relativo à quantidade:

Image

Esta página contém um formulário que é enviado através do botão [Comprar]. O URL de destino POST é [?action=purchase&id=ID], em que ID é o ID do artigo adquirido.

Código:

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

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

3.5.9.4. cart.jsp

Esta visualização apresenta o conteúdo do carrinho de compras:

Image

É apresentada na sequência de um pedido para /main?action=cart ou /main?action=remove&id=ID. Os parâmetros de pedido do controlador são os seguintes:

ações
Objeto Hashtable[] - a matriz de opções do menu
cart
objeto do tipo [Cart] - o carrinho a apresentar

Cada link [Remover] na tabela do carrinho de compras em HTML tem um URL no formato [?action=removeitem&id=ID], em que ID é o campo [id] do item a ser removido do carrinho.

Código:

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

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

3.5.9.5. emptycart.jsp

Esta visualização apresenta informações que indicam que o carrinho está vazio:

Image

É apresentada na sequência de um pedido para /main?action=cart ou /main?action=remove&id=ID. Os parâmetros de pedido do controlador são os seguintes:

ações
Objeto Hashtable[] - a matriz de opções do menu

Código:

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

<jsp:include page="entete.jsp"/>
        <h2>Contenu de votre panier</h2>
        <p>
        Votre panier est vide.
    </body>
</html>

3.5.9.6. errors.jsp

Esta vista é apresentada em caso de erros:

Image

É apresentada após qualquer pedido que resulte num erro, exceto no caso da ação de compra com uma quantidade incorreta, que é tratada pela vista [INFOS]. Os elementos do pedido do controlador são os seguintes:

actions
Objeto Hashtable[] - a matriz de opções do menu
erros
ArrayList de objetos String que representam as mensagens de erro a exibir

Código:

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

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

3.5.9.7. index.jsp

Esta página está definida como a página inicial da aplicação no ficheiro [web.xml] da aplicação:

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

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

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

A vista [index.jsp] redireciona simplesmente o cliente para o ponto de entrada da aplicação:

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

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

3.5.10. O Controlador

Ainda precisamos de escrever o núcleo da nossa aplicação web, o controlador. A sua função é:

  • recuperar o pedido do cliente,
  • processar a ação solicitada pelo cliente utilizando classes de negócio,
  • enviar a vista apropriada em resposta.

3.5.10.1. Inicialização do controlador

Quando a classe do controlador é carregada pelo servidor de servlets, o seu método [init] é executado. Isto acontecerá apenas uma vez. Uma vez carregado na memória, o controlador permanecerá lá e processará pedidos de vários clientes. Cada cliente é tratado por um segmento de execução separado, pelo que os métodos do controlador são executados simultaneamente por segmentos diferentes. Note-se que, por esta razão, o controlador não deve ter quaisquer campos que os seus métodos possam modificar. Os seus campos devem ser de leitura apenas. Estes são inicializados pelo método [init], que é a sua função principal. Este método tem a característica única de ser executado apenas uma vez por uma única thread. Portanto, não há problemas com o acesso simultâneo aos campos do controlador dentro deste método. O objetivo do método [init] é inicializar os objetos necessários à aplicação web, que serão partilhados em modo de leitura apenas por todas as threads do cliente. Estes objetos partilhados podem ser colocados em dois locais:

  • os campos privados do controlador
  • o contexto de execução da aplicação (ServletContext)

O método [init] da aplicação [webarticles] irá realizar as seguintes ações:

  • verificar o ficheiro [web.xml] para obter os parâmetros necessários para que a aplicação funcione corretamente. Estes foram descritos na secção 3.5.5.
  • inicializar um campo privado [ArrayList errors] contendo uma lista de eventuais erros. Esta lista estará vazia se não houver erros, mas existirá independentemente disso.
  • Se ocorrerem erros, o método [init] pára aí. Caso contrário, cria um objeto do tipo [IArticlesDomain], que será o objeto de negócio que o controlador utiliza para as suas necessidades. Conforme explicado em 3.5.6, o controlador solicitará o bean de que necessita à estrutura Spring. Esta operação de instanciação pode resultar em vários erros. Se assim for, estes serão, mais uma vez, armazenados no campo [errors] do controlador.

3.5.10.2. Métodos doGet, doPost

Estes dois métodos tratam de pedidos HTTP GET e POST dos clientes. Serão tratados de forma intercambiável. O método [doPost] pode, assim, redirecionar para o método [doGet] ou vice-versa. O pedido do cliente será processado da seguinte forma:

  • O campo [errors] será verificado. Se não estiver vazio, isso significa que ocorreram erros durante a inicialização da aplicação e que esta não pode ser executada. A vista [ERRORS] será então enviada como resposta.
  • O parâmetro [action] do pedido será recuperado e verificado. Se não corresponder a uma ação conhecida, a vista [ERRORS] é enviada com uma mensagem de erro apropriada.
  • Se o parâmetro [action] for válido, a solicitação do cliente é encaminhada para um procedimento específico da ação para processamento. O procedimento que lida com a ação [uneAction] terá a seguinte assinatura:
1
2
3
4
5
6
7
8
/**
   * @param request la requête du client
   * @param response la réponse au client
   * @throws IOException
   * @throws ServletException
   */
  private void doUneAction(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException; 

3.5.10.3. Tratamento de diferentes ações

Os métodos que tratam das várias ações possíveis da aplicação são os seguintes:

método
solicitação
processamento
respostas possíveis
doList
GET /main?action=list
- solicitar a lista de itens
da classe de negócios
- exibi-la
[LIST] ou [ERRORS]
doInfo
GET /main?action=info&id=ID
- recuperar o item com id=ID da
da classe de negócios
- exibi-lo
[INFO] ou [ERRORS]
doPurchase
POST /main?action=purchase&id=ID
- A quantidade comprada está incluída nos parâmetros enviados
- solicitar o item com id=ID da
classe de negócios
- Adicione-o ao carrinho na
sessão do cliente
[LIST] ou [INFO]
ou [ERRORS]
doRemovePurchase
GET /main?action=removePurchase&id=ID
- remover o item com id=ID da
lista de compras na
sessão do cliente
[CART]
doCart
GET /main?action=cart
- Exibir a
sessão do cliente
[CART] ou [EMPTY_CART]
doCartValidation
GET /main?action=cartvalidation
- Reduzir os
níveis de stock de todos os artigos
na
[LIST] ou [ERRORS]
[LISTA] ou [ERROS]

3.5.10.4. O código

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

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import istia.st.articles.domain.Achat;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;

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

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

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

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

    public void init() {
         // retrieve servlet initialization parameters
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // we memorize the error
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // mistakes?
        if (erreurs.size() != 0) {
            return;
        }
         // create a IArticlesDomain business layer access object
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // we memorize the error
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // memorize certain application urls
        hActionListe.put("href", "?action=" + ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", "?action=" + ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put(
            "href",
            "?action=" + ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // it's over
        return;
    }


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

         // check how the initialization of the servelet went
        if (erreurs.size() != 0) {
             // do we have the url of the error page?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // action is processed
        String action = request.getParameter("action");
        if (action == null) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // article info
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // purchase an item
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // basket display
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // remove an item from the basket
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // shopping cart validation
            doValidationPanier(request, response);
            return;
        }
         // unknown share
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // the error page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // end
        return;
    }


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

         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // validate this basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // error recovery
        ArrayList erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
            request.setAttribute(
                "actions",
                new Hashtable[] { hActionListe, hActionPanier });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // displays the list of items
        request.setAttribute("message", "Votre panier a été validé");
        doListe(request, response);
         // end
        return;
    }


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

         // remove a purchase from the basket
        try {
            Panier panier =
                (Panier) request.getSession().getAttribute("panier");
            String strIdAchat = request.getParameter("id");
            panier.enlever(Integer.parseInt(strIdAchat));
        } catch (NumberFormatException ignored) {
        } catch (NullPointerException ignored) {
        }
         // the basket is displayed
        doPanier(request, response);
    }


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

         // the basket is displayed
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // empty basket?
        if (panier == null || panier.getAchats().size() == 0) {
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
                .forward(request, response);
             // end
            return;
        }
         // there's something in the basket
        request.setAttribute("panier", panier);
        request.setAttribute(
            "actions",
            new Hashtable[] { hActionListe, hActionValidationPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_PANIER))
            .forward(request, response);
         // end
        return;
    }


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

         // purchase an item
         // we recover the quantity
        int qté = 0;
        try {
            qté = Integer.parseInt(request.getParameter("qte"));
            if (qté <= 0)
                throw new NumberFormatException();
        } catch (NumberFormatException ex) {
             // wrong qty
            request.setAttribute("msg", "Quantité incorrecte");
            request.setAttribute("qte", request.getParameter("qte"));
            String url =
                config.getInitParameter(URL_MAIN)
                    + "?action=infos&id="
                    + request.getParameter("id");
            getServletContext().getRequestDispatcher(url).forward(
                request,
                response);
             // end
            return;
        }
         // retrieve the client session
        HttpSession session = request.getSession();
         // we create the purchase
        Article article = (Article) session.getAttribute("article");
        Achat achat = new Achat(article, qté);
         // the purchase is added to the customer's basket
        Panier panier = (Panier) session.getAttribute("panier");
        if (panier == null) {
            panier = new Panier();
            session.setAttribute("panier", panier);
        }
        panier.ajouter(achat);
         // we return to the list of items
        String url = config.getInitParameter(URL_MAIN) + "?action=liste";
        getServletContext().getRequestDispatcher(url).forward(
            request,
            response);
         // end
        return;
    }


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

         // displays the list of items
        request.setAttribute("infos", infos);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_DEBUG))
            .forward(request, response);
         // end
        return;
    }


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

         // idem get
        doGet(request, response);
    }


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

         // error list
        ArrayList erreurs = new ArrayList();
         // retrieve the requested id
        String strId = request.getParameter("id");
         // anything?
        if (strId == null) {
             // not normal
            erreurs.add("action incorrecte([infos,id=null]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // transform strId into an integer
        int id = 0;
        try {
            id = Integer.parseInt(strId);
        } catch (Exception ex) {
             // not normal
            erreurs.add("action incorrecte([infos,id=" + strId + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // the key item id is requested
        Article article = null;
        try {
            article=articlesDomain.getArticleById(id);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
        if (article == null) {
             // not normal
            erreurs.add("Article de clé [" + id + "] inexistant");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // put the article in the session
        request.getSession().setAttribute("article", article);
         // the info page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        //    request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_INFOS))
            .forward(request, response);
         // end
        return;
    }

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

         // the error page is displayed
        request.setAttribute("erreurs", erreurs);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
            .forward(request, response);
         // end
        return;
    }


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

         // list of errors
        ArrayList erreurs = new ArrayList();
         // the list of items is requested
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // end
        return;
    }

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

Deixaremos que o leitor dedique algum tempo a ler e compreender este código. Esperamos que os comentários ajudem.

3.5.10.5. Teste de aplicações

Vejamos algumas capturas de ecrã dos testes. Primeiro, a página inicial da aplicação:

Image

O URL solicitado era, na verdade, [http://localhost:8080/webarticles]. O leitor verá que, no ficheiro [web.xml], definimos uma página inicial para a aplicação:

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

A vista [index.jsp] é definida da seguinte forma:

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

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

Houve, portanto, um redirecionamento para o URL [http://localhost:8080/webarticles/main?action=liste], conforme mostrado pelo URL do navegador na captura de ecrã. O URL [/main?action=liste] foi, assim, solicitado. Ainda no [web.xml], o URL /main está associado ao servlet [webarticles]:

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

Também no [web.xml], o servlet [webarticles] está associado ao servlet [istia.st.articles.web.WebArticles]:

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

O servlet [istia.st.articles.web.WebArticles] é, portanto, carregado pelo contentor de servlets Tomcat, caso ainda não o tenha sido, e o seu método [init] é executado:

    public void init() {
         // retrieve servlet initialization parameters
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // we memorize the error
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // mistakes?
        if (erreurs.size() != 0) {
            return;
        }
         // create a IArticlesDomain business layer access object
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // we memorize the error
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // memorize certain application urls
        hActionListe.put("href", "?action=" + ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", "?action=" + ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put(
            "href",
            "?action=" + ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // it's over
        return;
    }

Comentários: O método [init]

  • verifica a presença de determinados parâmetros de configuração
  • instancia um serviço para aceder ao domínio da aplicação utilizando o Spring
  • define uma lista de erros para indicar quaisquer erros de inicialização
  • vários campos privados:
    • errors: a lista de erros detetados por [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: dicionários. Cada um contém as informações necessárias para exibir uma opção no menu principal apresentado pela vista [entete.jsp]
    • acticlesDomain: o serviço para aceder ao modelo da aplicação

O método [init] é executado apenas uma vez, no carregamento inicial do servlet. Depois disso, um dos métodos [doGet, doPost] é executado, dependendo do tipo [GET, POST] do pedido do cliente. Aqui, ambos os métodos fazem a mesma coisa, e o código foi colocado em [doGet]:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
         // check how the initialization of the servelet went
        if (erreurs.size() != 0) {
             // do we have the url of the error page?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // action is processed
        String action = request.getParameter("action");
        if (action == null) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // article info
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // purchase an item
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // basket display
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // remove an item from the basket
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // shopping cart validation
            doValidationPanier(request, response);
            return;
        }
         // unknown share
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // the error page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // end
        return;
    }
  • O método [doGet] começa por verificar se ocorreram erros de inicialização após o método [init]. Se for o caso, apresenta a vista [ERRORS] e o processo termina.
  • Caso contrário, recupera o parâmetro [action] da solicitação do cliente. Lembre-se de que a aplicação foi construída para responder a solicitações que devem incluir um parâmetro [action].
  • Executa o método associado à ação. Aqui, esse será o método [doListe].

O método [doListe] é o seguinte:

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

         // error list
        ArrayList erreurs = new ArrayList();
         // the list of items is requested
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // end
        return;
    }
  • Lembre-se de que o método [init] armazenou o serviço para aceder ao modelo da aplicação (camada de domínio) num campo privado do servlet:
    // champs privés
    private IArticlesDomain articlesDomain = null;
  • Usando este serviço de acesso, podemos solicitar a lista de artigos:
        // la liste des erreurs
        ArrayList erreurs = new ArrayList();
        // on demande la liste des articles
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
            // on mémorise l'erreur
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
  • Se ocorrerem erros, é enviada a visualização [ERRORS]:
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
  • caso contrário, a vista [LIST] é enviada:
1
2
3
4
5
6
7
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);

No nosso exemplo, tudo correu bem e obtivemos com sucesso a vista [LIST]. Convidamos o leitor a rever o código da vista [LIST] para verificar se os parâmetros dinâmicos esperados por esta vista são, de facto, fornecidos acima pelo controlador. O mesmo tipo de verificação deve ser realizado para cada vista:

  • identifique os parâmetros dinâmicos da vista
  • garanta que o controlador os coloca nos atributos de solicitação passados para a vista

Vamos agora simplesmente delinear o fluxo de ecrãs com que se depara um utilizador da aplicação. Convidamos o leitor a seguir uma linha de raciocínio semelhante à anterior em cada ocasião:

A partir da lista de itens, o utilizador pode selecionar um item:

O comprador pode adquirir o artigo n.º 3 aqui. Vamos introduzir um erro na quantidade:

O erro foi sinalizado. Agora, vamos comprar alguns itens:

A compra foi registada e a lista de itens foi novamente apresentada. Vamos verificar o carrinho de compras:

A compra está, de facto, no carrinho de compras. Vamos removê-la:

A compra foi removida do carrinho de compras e o carrinho foi recarregado. Agora está vazio.

Vamos comprar 100 unidades do artigo n.º 3 e 2 unidades do artigo n.º 4:

A compra do artigo n.º 3 não foi possível porque queríamos comprar 100, mas havia apenas 30 em stock. Esta compra permaneceu no carrinho:

O item n.º 4, no entanto, foi comprado, como mostra o seu novo nível de stock de 39 (40-1):

3.6. Arquitetura MVC com Struts

3.6.1. Arquitetura Geral de Aplicações

Vamos rever a arquitetura MVC da aplicação:

Na versão anterior:

  • o controlador era gerido por um servlet
  • as visualizações eram geridas por páginas JSP
  • o modelo era gerido por um conjunto de três ficheiros .jar

Na versão Struts:

  • o controlador será gerido por um servlet derivado do [ActionServlet] genérico do Struts
  • As vistas serão geridas pelas mesmas páginas JSP de antes, com algumas pequenas diferenças
  • O modelo será gerido pelos mesmos três arquivos

Veremos que a migração da aplicação anterior para o Struts envolve as seguintes tarefas:

  • as ações que eram anteriormente tratadas em métodos específicos do servlet/controlador são agora tratadas por instâncias de classes derivadas da classe [Action] do Struts
  • Escrever os ficheiros de configuração [web.xml] e [struts-config.xml]
  • Fazer algumas alterações nas páginas JSP

Vamos rever a arquitetura MVC genérica utilizada pelo Struts:

M = modelo
classes de negócios, classes de acesso a dados e a base de dados
V = vistas
páginas JSP
C = controlador
o servlet que processa os pedidos do cliente, os objetos [Action] e os beans [ActionForm] associados aos formulários.
  • O controlador é o coração da aplicação. Todas as solicitações do cliente passam por ele. É um servlet genérico fornecido pelo STRUTS. Em alguns casos, poderá ser necessário estendê-lo. Para casos simples, isso não é necessário. Este servlet genérico recupera as informações de que necessita de um ficheiro geralmente chamado struts-config.xml.
  • Se a solicitação do cliente contiver parâmetros de formulário, o controlador coloca-os num objeto Bean. Os objetos Bean criados ao longo do tempo são armazenados na sessão ou na solicitação do cliente. Este comportamento é configurável. Não precisam de ser recriados se já tiverem sido criados.
  • No ficheiro de configuração struts-config.xml, cada URL a ser processada programaticamente (e, portanto, não correspondente a uma vista JSP que possa ser solicitada diretamente) está associada a determinadas informações:
    • o nome da classe Action responsável pelo processamento da solicitação. Aqui, mais uma vez, o objeto Action instanciado pode ser armazenado na sessão ou na solicitação.
    • Se a URL solicitada for parametrizada (como quando um formulário é enviado ao controlador), é especificado o nome do bean responsável por armazenar os dados do formulário.
  • Com base nas informações fornecidas pelo seu ficheiro de configuração, ao receber um pedido de URL de um cliente, o controlador consegue determinar se é necessário criar um bean e qual deles. Uma vez instanciado, o bean pode verificar se os dados que armazenou — provenientes do formulário — são válidos ou não. Um método do bean chamado `validate` é chamado automaticamente pelo controlador. O bean é criado pelo programador. O programador, portanto, coloca o código que verifica a validade dos dados do formulário dentro do método validate. Se os dados forem considerados inválidos, o controlador não prosseguirá. Ele passará o controlo para uma vista cujo nome encontra no seu ficheiro de configuração. A interação fica então concluída. Note-se que o programador pode optar por não verificar a validade do formulário. Isto também é feito no ficheiro struts-config.xml. Neste caso, o controlador não chama o método validate do bean.
  • Se os dados do bean estiverem corretos, ou se não houver validação, ou se não houver bean, o controlador passa o controlo para o objeto Action associado à URL. Faz isso chamando o método execute desse objeto, passando-lhe a referência ao bean que ele possa ter construído. É aqui que o programador faz o que precisa ser feito: pode ser necessário chamar classes de negócio ou classes de acesso a dados. No final do processamento, o objeto Action devolve ao controlador o nome da vista que este deve enviar em resposta ao cliente.
  • No seu ficheiro de configuração, o controlador irá encontrar a URL associada ao nome da vista que lhe foi pedido para apresentar. Em seguida, envia a vista. A interação com o cliente está concluída.

Na nossa aplicação, não utilizaremos objetos [Bean] como objetos de buffer entre o cliente e as classes [Action]. O objeto [Action] irá recuperar diretamente os parâmetros do pedido do cliente a partir do objeto [HttpServletRequest] que recebe. Isto facilita a portabilidade da nossa aplicação inicial. A arquitetura final da nossa aplicação será, portanto, a seguinte:

M = modelo
classes de negócios, classes de acesso a dados e a base de dados
V = Visualizações
as páginas JSP
C = Controlador
o servlet para processar os pedidos do cliente, objetos [Action]

3.6.2. O modelo

Foi apresentado anteriormente. Consiste nos arquivos Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.6.3. Configuração da aplicação

3.6.3.1. Arquitetura geral

A arquitetura geral do projeto Eclipse é a seguinte:

Image

3.6.3.2. Configuração do acesso aos dados

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

Image

Na captura de ecrã acima, os ficheiros [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] são os da versão anterior.

3.6.3.3. O diretório de arquivos

Em [WEB-INF/lib], encontrará as mesmas bibliotecas da versão anterior, além daquela exigida pelo Struts:

Image

3.6.3.4. Configuração da aplicação

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

Image

O ficheiro [web.xml] é o seguinte:

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

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

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

O que diz este ficheiro?

  • A página inicial da aplicação é [views/index.jsp] (welcome-file)
  • Os pedidos de URL do tipo *.do serão redirecionados para o servlet [strutswebarticles] (servlet-mapping)
  • O servlet [strutswebarticles] é uma instância da classe [istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
  • Este servlet aceita dois parâmetros de inicialização
    • o nome do ficheiro de configuração do Struts (config)
    • O nome do ficheiro de configuração do Spring (springConfigFileName)

O ficheiro [struts-config.xml] é o seguinte:

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

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

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

O que diz este ficheiro de configuração?

  • que o nosso controlador irá tratar das seguintes URLs:
main.do
para exibir a lista de artigos
liste.do
para exibir a lista de artigos
info.do
para exibir informações sobre um item específico
purchase.do
para comprar um item específico
cart.do
para visualizar o carrinho de compras
remove-purchase.do
para remover uma compra do carrinho de compras
confirmcart.do
para confirmar o carrinho de compras
  • As ações listadas acima correspondem, uma a uma, às ações tratadas pelo servlet na versão anterior. Para cada uma delas, são fornecidas as seguintes informações:
  • o nome da classe responsável pelo tratamento desta ação
  • as respostas possíveis (= visualizações) após o processamento da ação. Apenas uma delas será selecionada pelo controlador.
  • o nome de um ficheiro de mensagens para a aplicação (message-resources). Aqui, o ficheiro existirá, mas estará vazio. Não será utilizado. Deve ser colocado no [ClassPath] da aplicação. Aqui, será colocado em [WEB-INF/classes]. No Eclipse, isto é conseguido colocando-o em [WEB-INF/src]:

Image

3.6.4. As Visualizações JSP

As vistas JSP utilizadas aqui são também as da versão anterior. Foram feitas muito poucas alterações: as URLs do formato [?action=XX?id=YY& ...] foram alteradas para [/XX.do?id=YY&....]. Estamos a repetir explicações já dadas aqui para evitar que o utilizador tenha de voltar atrás. É importante compreender que a informação passada para a vista pelo controlador é exatamente a mesma em ambas as versões. Nada mudou a este respeito.

3.6.4.1. entete.jsp

Para garantir a consistência entre as diferentes vistas, estas partilharão o mesmo cabeçalho, que exibe o nome da aplicação juntamente com o menu:

O menu é dinâmico e definido pelo controlador. O controlador inclui na solicitação enviada à página JSP um atributo-chave "actions" com um valor associado de uma matriz Hastable[]. Cada elemento desta matriz é um dicionário destinado a gerar uma opção de menu no cabeçalho. Cada dicionário tem duas chaves:

  • href: o URL associado à opção do menu
  • link: o texto do menu

As outras vistas da aplicação utilizarão o cabeçalho definido por [entete.jsp] utilizando a seguinte tag JSP:

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

Quando executada, esta tag incluirá o código da página [entete.jsp] na página JSP que a contém. Uma vez que o URL da página é um URL relativo (sem / no final), a página [entete.jsp] será procurada no mesmo diretório que a página que contém a tag <jsp:include>.

Código:

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

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

Comentários: sem alterações em relação à versão anterior

3.6.4.2. list.jsp

Esta vista apresenta a lista de artigos disponíveis para venda:

É apresentada após um pedido para /main.do ou /validerpanier.do. Os parâmetros de pedido do controlador são os seguintes:

ações
Objeto Hashtable[] - a matriz de opções do menu
listarticles
ArrayList de objetos do tipo [Item]
mensagem
Objeto String - mensagem a exibir na parte inferior da página

Cada link [Info] na tabela HTML de artigos tem um URL no formato [/infos.do?id=ID], em que ID é o campo id do artigo exibido.

Código:

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

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

Comentários: uma alteração (destacada acima)

3.6.4.3. infos.jsp

Esta página apresenta informações sobre um artigo e permite também a sua compra:

Image

É apresentada na sequência de um pedido para /infos.do?id=ID ou de um pedido para /achat.do?id=ID quando a quantidade comprada está incorreta. Os elementos do pedido do controlador são os seguintes:

ações
object Hashtable[] - a matriz de opções do menu
item
objeto do tipo [Artigo] - item a exibir
msg
Objeto String - mensagem a exibir em caso de erro com a quantidade
qte
Objeto String - valor a exibir no campo de entrada [Qty]

Os campos [msg] e [qte] são utilizados em caso de erro de introdução de dados relativo à quantidade:

Image

Esta página contém um formulário que é enviado através do botão [Comprar]. O URL de destino POST é [/achat.do?id=ID], em que ID é o ID do artigo adquirido.

Código:

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

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

Comentários: uma alteração (destacada acima)

3.6.4.4. cart.jsp

Esta vista apresenta o conteúdo do carrinho de compras:

Image

É apresentada na sequência de um pedido para /panier.do ou /retirerachat.do?id=ID. Os parâmetros de pedido do controlador são os seguintes:

ações
Objeto Hashtable[] - a matriz de opções do menu
cart
objeto do tipo [ShoppingCart] - o carrinho de compras a apresentar

Cada link [Remove] na matriz do carrinho de compras em HTML tem um URL no formato [removeitem.do?id=ID], em que ID é o campo [id] do item a ser removido do carrinho.

Código:

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

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

Comentários: uma alteração (destacada acima)

3.6.4.5. emptycart.jsp

Esta visualização apresenta informações que indicam que o carrinho está vazio:

Image

É apresentada na sequência de um pedido para /panier.do ou /retirerachat.do?id=ID. Os parâmetros de pedido do controlador são os seguintes:

ações
Objeto Hashtable[] - a matriz de opções do menu

Código:

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

<jsp:include page="entete.jsp"/>
        <h2>Contenu de votre panier</h2>
        <p>
        Votre panier est vide.
    </body>
</html>

Comentários: sem alterações.

3.6.4.6. errors.jsp

Esta vista é apresentada em caso de erros:

Image

É apresentada após qualquer pedido que resulte num erro, exceto no caso da ação de compra com uma quantidade incorreta, que é tratada pela vista [INFOS]. Os elementos do pedido do controlador são os seguintes:

ações
Objeto Hashtable[] - a matriz de opções do menu
erros
ArrayList de objetos String que representam as mensagens de erro a exibir

Código:

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

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

Comentários: sem alterações.

3.6.4.7. index.jsp

Esta página está definida como a página inicial da aplicação no ficheiro [web.xml] da aplicação:

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

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

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

A vista [index.jsp] redireciona simplesmente o cliente para o ponto de entrada da aplicação:

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

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

Comentários: uma alteração (destacada acima)

3.6.5. O Controlador Struts

O Struts possui um controlador genérico chamado [ActionServlet]. Sabemos que um servlet possui um método [init] que permite que a aplicação seja inicializada quando é iniciada. Se utilizarmos o controlador genérico do Struts [ActionServlet], não temos acesso ao seu método [init]. Aqui, temos tarefas a realizar quando a aplicação é iniciada, principalmente instanciar um objeto de acesso ao modelo. Portanto, precisamos de um método [init]. Assim, estendemos a classe [ActionServlet] na seguinte classe [MainServlet]:

package istia.st.articles.web.struts;

import istia.st.articles.domain.IArticlesDomain;

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

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

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

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

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

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

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

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

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

  public void init() throws ServletException{

     // init parent class
    super.init();
     // retrieve servlet initialization parameters
    config = getServletConfig();
    String param = null;
    for (int i = 0; i < parameters.length; i++) {
      param = config.getInitParameter(parameters[i]);
      if (param == null) {
         // we memorize the error
        erreurs.add("Paramètre [" + parameters[i]
            + "] absent dans le fichier [web.xml]");
      }
    }
     // mistakes?
    if (erreurs.size() != 0) {
      return;
    }
     // create a IArticlesDomain business layer access object
    try {
      articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
          new ClassPathResource((String) config
              .getInitParameter(SPRING_CONFIG_FILENAME))))
          .getBean("articlesDomain");
    } catch (Exception ex) {
       // we memorize the error
      erreurs.add("Erreur de configuration de l'accès aux données : "
          + ex.toString());
      return;
    }
     // memorize certain application urls
    hActionListe.put("href", ACTION_LISTE);
    hActionListe.put("lien", lienActionListe);
    hActionPanier.put("href", ACTION_PANIER);
    hActionPanier.put("lien", lienActionPanier);
    hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
    hActionValidationPanier.put("lien", lienActionValidationPanier);

     // it's over
    return;
  }
}

Comentários:

  • O valor da classe reside no seu método [init] e nos seus campos privados
  • O método [init] faz o mesmo que o método [init] no controlador da versão anterior:
    • verifica a presença de determinados parâmetros de configuração
    • Ele instancia um serviço para aceder ao domínio da aplicação utilizando o Spring
    • configurar uma lista de erros para indicar quaisquer erros de inicialização
  • Antes de iniciar o trabalho, o método [init] chama o método [init] da classe pai [ActionServlet]. Este método irá processar o ficheiro de configuração do Struts [struts-config.xml].
  • São definidos vários campos privados com os seus acessores:
    • errors: a lista de erros detetados pelo [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: dicionários. Cada um contém as informações necessárias para exibir uma opção no menu principal apresentado pela vista [entete.jsp]
    • acticlesDomain: o serviço que fornece acesso ao modelo da aplicação
  • O controlador de uma aplicação Struts é acessível às classes [Action] responsáveis por lidar com as várias ações possíveis. Estas classes terão acesso aos campos privados anteriores, uma vez que lhes são fornecidos acessores públicos.

Este controlador é instanciado pelo ficheiro [web.xml]:

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

Qualquer URL que termine em .do será tratada por uma instância da classe [istia.st.articles.web.struts.MainServlet]

3.6.6. Ações da aplicação Struts

3.6.6.1. Introdução

Cada ação Struts será implementada como uma classe. Na versão anterior, cada ação era implementada como um método no controlador da aplicação. Escrever a classe [Action] envolve normalmente:

  • copiar e colar o método utilizado na versão anterior
  • adaptar o código às convenções do Struts

3.6.6.2. main.do, list.do

Estas duas ações são idênticas e estão definidas no [struts-config.xml] da seguinte forma:

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

Quando uma destas ações [main.do, liste.do] é executada num navegador, obtém-se o seguinte resultado:

Image

O código da classe [ListeArticlesAction] é o seguinte:

package istia.st.articles.web.struts;

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

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

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

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

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

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

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

     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();

     // list of errors
    ArrayList erreurs = new ArrayList();
     // the list of items is requested
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // we memorize the error
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
     // mistakes?
    if (erreurs.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // displays the list of items
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");
  }
}

Comentários:

  • Escrever o código para uma classe [Action] consiste essencialmente em escrever o código para o seu método [execute]
  • Foi armazenada uma certa quantidade de informação na instância do controlador. Recuperamos uma referência a ela utilizando:
     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
  • Recuperamos a lista de erros de inicialização armazenada pelo controlador. Se esta lista não estiver vazia, a vista [ERRORS] é enviada ao cliente:
1
2
3
4
5
6
7
8
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

A vista que será efetivamente enviada ao cliente é fornecida pelo [struts-config.xml]:

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

Esta é a vista [/vues/erreurs.jsp]. O leitor é convidado a verificar o que esta vista espera. Esta informação é fornecida aqui pela ação no objeto [request] como atributos.

  • Mais uma vez, graças ao controlador, a ação pode recuperar o objeto que fornece acesso ao modelo da aplicação (camada de domínio):
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • Depois de fazer isto, podemos solicitar a lista de artigos:
    // la liste des erreurs
    ArrayList erreurs = new ArrayList();
    // on demande la liste des articles
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
      // on mémorise l'erreur
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • Se ocorrerem erros, é enviada a visualização [ERRORS]:
1
2
3
4
5
6
7
8
     // mistakes?
    if (erreurs.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • caso contrário, a vista [LIST] é enviada:
1
2
3
4
5
6
     // displays the list of items
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");

A vista que será efetivamente enviada ao cliente é fornecida por [struts-config.xml]:

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

Esta é a vista [/vues/liste.jsp]. O leitor é convidado a verificar o que é esperado por esta vista. Esta informação é fornecida aqui pela ação no objeto [request] como atributos.

3.6.6.3. infos.do

Esta ação é utilizada para fornecer informações sobre um dos itens apresentados na vista [LIST]:

Esta ação é configurada da seguinte forma no ficheiro [struts-config.xml]:

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

O código para a classe [InfosArticleAction] é o seguinte:

package istia.st.articles.web.struts;

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

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

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

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

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

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // list of errors
    ArrayList erreurs = new ArrayList();
     // retrieve the requested id
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // the key item id is requested
    Article article = null;
    try {
      article = articlesDomain.getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // not normal
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
    if (article == null) {
       // not normal
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // put the article in the session
    request.getSession().setAttribute("article", article);
     // the info page is displayed
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionListe() });
    return mapping.findForward("afficherInfosArticle");
  }
}

Comentários:

  • O início do método [execute] é idêntico ao discutido anteriormente. O mesmo se aplica às outras ações.
  • O método recupera o parâmetro [id], que normalmente deve estar presente na URL. A URL deve, de facto, ter o formato [/infos.do?id=X]. São realizadas várias verificações para confirmar a presença e a validade do parâmetro [id]. Se houver algum problema, a vista [ERRORS] é apresentada.
  • Se [id] for válido, o item correspondente é solicitado à camada [domain]. Se isto lançar uma exceção ou se o item não for encontrado, a vista [ERRORS] é enviada novamente.
  • Se tudo correr bem, o item recuperado é armazenado na sessão. Este é um ponto discutível. Aqui, assumimos que o cliente pode comprar este item. Se o fizer, iremos recuperá-lo da sessão em vez de o solicitar novamente à camada [domain].
  • Por fim, a vista [INFOS] é apresentada. A vista que será efetivamente enviada ao cliente é fornecida pelo [struts-config.xml]:
        <action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

Esta é a vista [/vues/infos.jsp]. O leitor é convidado a verificar o que é esperado por esta vista. Esta informação é fornecida aqui pela ação no objeto [request] como atributos.

3.6.6.4. purchase.do

Esta ação é utilizada para comprar o artigo apresentado pela vista [INFOS] anterior:

Assim que o item é comprado, a vista [LIST] é exibida novamente (vista à direita). Se analisarmos o código HTML da vista à esquerda acima, vemos que a tag <form> está definida da seguinte forma:

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

Podemos ver que o formulário é enviado para o controlador com a ação [achat.do].

Esta ação está configurada da seguinte forma no [struts-config.xml]:

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

O código da classe [AchatArticleAction] é o seguinte:

package istia.st.articles.web.struts;

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

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

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

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

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // we return to the list of items
    return mapping.findForward("afficherListeArticles");
  }
}

Comentários:

  • O início do método [execute] é idêntico aos estudados anteriormente.
  • Recordemos o formato do formulário enviado ao controlador:
1
2
3
4
5
6
7
8
9
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>
  • Existem dois parâmetros na solicitação: [id]: número do item, [qte]: quantidade comprada.
  • A presença e a validade do parâmetro [qte] são verificadas. Se este parâmetro for considerado incorreto, a vista [INFOS] é devolvida ao utilizador juntamente com uma mensagem de erro:
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
  • O item adquirido é recuperado da sessão. A sessão pode ter expirado. Neste caso, é apresentada a vista [ERRORS]:
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
  • Se a sessão não tiver expirado, o item é adicionado ao carrinho, que também é recuperado da sessão:
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • Por fim, enviamos a vista [LIST]:
    // on revient à la liste des articles
    return mapping.findForward("afficherListeArticles");
  • A vista que será efetivamente enviada ao cliente é fornecida pelo [struts-config.xml]:
        <action 
            path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherListeArticles" path="/main.do"/>
        </action>

Esta é a vista [/main.do]. Esta vista não é uma vista, mas sim uma ação. A ação [/main.do] descrita acima será, portanto, executada e exibirá a lista de itens.

3.6.6.5. cart.do

Esta ação é utilizada para apresentar todas as compras do cliente. Está disponível através da opção de menu [Ver Carrinho]:

O código HTML associado ao link [Ver carrinho] é o seguinte:

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

A ação [panier.do] está configurada da seguinte forma no ficheiro [struts-config.xml]:

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

O código para a classe [VoirPanierAction] é o seguinte:

package istia.st.articles.web.struts;

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

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

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

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // the basket is displayed
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // empty basket
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    } else {
       // there's something in the basket
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(), mainServlet.getHActionValidationPanier() });
      return mapping.findForward("afficherPanier");
    }
  }
}

Comentários:

  • O início do método [execute] é idêntico aos discutidos anteriormente.
  • O carrinho de compras é recuperado da sessão onde normalmente se encontra. A sessão pode ter expirado, caso em que não existe carrinho de compras. Não tratamos isto como um erro, mas simplesmente assumimos que o carrinho de compras está vazio.
  • Se o carrinho estiver vazio, é apresentada a vista [EMPTY CART]
  • caso contrário, é apresentada a vista [PANIER]

As vistas efetivamente enviadas ao cliente são definidas pela ação:

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

3.6.6.6. checkout.do

Esta ação remove um item do carrinho de compras:

Após a ação [retirerachat.do], o carrinho de compras é exibido novamente (visualização à direita, acima). Se analisarmos o código HTML do link [Confirmar Carrinho] na visualização à esquerda, acima, vemos o seguinte:

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

A ação [retirerachat.do] recebe, portanto, como parâmetro, o ID do item a ser removido do carrinho. Esta ação está configurada da seguinte forma no [struts-config.xml]:

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

O código para a classe [RetirerAchatAction] é o seguinte:

package istia.st.articles.web.struts;

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

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

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

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // we pick up the basket
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    }
     // retrieve the id of the item to be removed
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // we remove the purchase
    panier.enlever(id);
     // the basket is displayed again
    return mapping.findForward("afficherPanier");
  }
}

Comentários:

  • O início do método [execute] é idêntico aos estudados anteriormente.
  • O código a seguir verifica a presença e a validade do parâmetro [id]. Se estiver incorreto, é enviada a visualização [ERRORS].
  • Caso contrário, a compra é removida do carrinho:
    // on enlève l'achat
    panier.enlever(id);
  • depois, o carrinho é exibido novamente:
    // on affiche de nouveau le panier
    return mapping.findForward("afficherPanier");

A vista efetivamente enviada ao cliente é definida pela ação:

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

Podemos ver que, em termos de visualizações, é a ação [/cart.do] que será acionada. Isto já foi descrito. Ela exibirá a visualização [CART] ou [EMPTY CART], dependendo do estado do carrinho.

3.6.6.7. confirmCart.do

Esta ação é utilizada para confirmar as compras do cliente. Na prática, isto envolve uma única ação: os níveis de stock dos artigos comprados são reduzidos nas quantidades adquiridas na base de dados. Esta ação provém do seguinte menu:

 

O código HTML para o link [Confirmar carrinho] é o seguinte:

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

Quando este link é clicado, os níveis de stock são atualizados e a lista de artigos é apresentada novamente.

Esta ação está configurada da seguinte forma no [struts-config.xml]:

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

O código para a classe [ValiderPanierAction] é o seguinte:

package istia.st.articles.web.struts;

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

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

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

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

     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
         // validate basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
        }
         // recover any errors
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
         // everything looks OK - the item list is displayed
    return mapping.findForward("afficherListeArticles");
  }
}

Comentários:

  • O início do método [execute] é idêntico aos estudados anteriormente.
  • Recuperamos o carrinho de compras da sessão. Se a sessão tiver expirado, exibimos a vista [ERRORS]:
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • Processamos as compras no carrinho. Podem ocorrer erros se os níveis de stock forem insuficientes para satisfazer as compras. Neste caso, apresentamos a vista [ERRORS]:
         // validate basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
        }
         // recover any errors
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
  • Se tudo correu bem, exibimos novamente a lista de itens:
        // tout semble OK - on affiche la liste des articles
    return mapping.findForward("afficherListeArticles");

A vista efetivamente enviada ao cliente é definida pela ação:

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

Podemos ver que, em termos da vista, é a ação [/main.do] que será acionada. Isto já foi descrito. Ela irá apresentar a vista [LIST].

3.7. Arquitetura MVC com Spring

3.7.1. Arquitetura geral da aplicação

Vamos rever a arquitetura MVC da aplicação:

Na primeira versão:

  • o controlador era gerido por um servlet
  • as visualizações eram geridas por páginas JSP
  • o modelo era gerido por um conjunto de três ficheiros .jar

Na versão Struts:

  • o controlador era gerido por um servlet derivado do [ActionServlet] genérico do Struts
  • as vistas eram geridas pelas mesmas páginas JSP que na versão [Struts]
  • O modelo era gerido pelos mesmos três arquivos

Na versão Spring:

  • o controlador será tratado por um servlet fornecido pelo Spring [DispatcherServlet]
  • As vistas serão geridas pelas mesmas páginas JSP de antes, com algumas pequenas diferenças
  • O modelo será gerido pelos mesmos três ficheiros

Veremos que a migração da nossa aplicação do Struts para o Spring é simples, se estivermos dispostos a abdicar da utilização de todos os elementos recomendados para uma arquitetura Spring MVC padrão. As principais alterações são as seguintes:

  • as ações que anteriormente eram tratadas em métodos específicos do servlet/controlador, ou por instâncias de classes derivadas da classe [Action] no Struts, são agora tratadas por instâncias de classes que implementam a interface [Controller] do Spring
  • Os ficheiros de configuração necessários são os seguintes:
    • [web.xml], uma vez que se trata de uma aplicação web. Este ficheiro contém um ouvinte que, quando a aplicação for inicializada, utilizará o [applicationContext.xml]
    • [applicationContext.xml] para criar os beans necessários à aplicação, em particular o bean do serviço de acesso ao modelo
  • As vistas JSP serão idênticas às do Struts. Teremos de criar uma nova.

Recordemos a arquitetura MVC do STRUTS utilizada na versão anterior:

M = Modelo
classes de negócios, classes de acesso a dados e a base de dados
V = Visualizações
as páginas JSP
C = Controlador
o servlet para processar os pedidos do cliente, objetos [Action]

Com o Spring, usamos a mesma arquitetura:

M = modelo
classes de negócios, classes de acesso a dados e a base de dados
V = vistas
as páginas JSP
C = Controlador
o servlet que processa os pedidos dos clientes, objetos que implementam a interface [Controller]
  • O controlador é o coração da aplicação. Todas as solicitações do cliente passam por ele. É um servlet genérico fornecido pelo SPRING. É do tipo [DispatcherServlet]. A partir de agora, nos referiremos a este controlador como o controlador [Spring].
  • O controlador [Spring] encaminhará a solicitação do cliente para uma das instâncias [Controller]. Haverá uma instância por ação a ser processada. Isso é definido na URL solicitada, tal como no Struts. Assim, saberemos que a ação solicitada é a ação [list] porque a URL solicitada é [list.do]
  • Se C for o contexto da aplicação, o controlador [Spring] utiliza um ficheiro [C-servlet.xml] que desempenha o mesmo papel que o ficheiro de configuração struts-config.xml na versão Struts. Para cada ação a ser processada pela aplicação, associamos o nome da classe do tipo Controller responsável por tratar a solicitação.
  • O controlador passa o controlo para o objeto do tipo Controller associado à ação. Faz isso chamando o método handleRequest desse objeto e passando-lhe a solicitação do cliente. É aqui que o programador executa as tarefas necessárias: pode ser necessário chamar classes de lógica de negócio ou classes de acesso a dados. No final do processamento, o objeto Controller devolve ao controlador o nome da vista que este deve enviar em resposta ao cliente.
  • No seu ficheiro de configuração, o controlador irá encontrar o URL associado ao nome da vista que lhe foi solicitado para apresentar. Em seguida, envia a vista. A interação com o cliente está concluída.

3.7.2. O Modelo

É o mesmo que nas duas versões anteriores. Consiste nos arquivos Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.7.3. Configuração da aplicação

3.7.3.1. Arquitetura geral

A arquitetura geral do projeto Eclipse é a seguinte:

Image

3.7.3.2. Configuração do acesso aos dados

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

Image

Na captura de ecrã acima, os ficheiros [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] são os das versões anteriores.

3.7.3.3. O diretório de arquivos

Em [WEB-INF/lib], encontrará as mesmas bibliotecas da versão anterior, exceto as do Struts, que já não são necessárias:

Image

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

Image

O ficheiro [web.xml] é o seguinte:

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

<!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
     <!-- application spring context loader -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- the servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- url mapping -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- entry document -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

O que diz este ficheiro?

  • A página inicial da aplicação é [/vues/index.jsp] (welcome-file)
  • Os pedidos de URL do tipo *.do serão redirecionados para o servlet [springwebarticles] (servlet-mapping)
  • O servlet [springwebarticles] é uma instância da classe [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class) fornecida pelo Spring.
  • O ouvinte [org.springframework.web.context.ContextLoaderListener] será iniciado quando a aplicação for iniciada. A sua principal função será instanciar os beans Spring definidos no ficheiro [applicationContext.xml]

O ficheiro [applicationContext.xml] é o seguinte:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Alguns elementos são familiares, enquanto outros o são menos. Três beans serão instanciados durante a inicialização da aplicação:

  • articlesDao: serviço que fornece acesso à camada [dao]
  • articlesDomain: serviço que fornece acesso ao modelo
  • config: um bean no qual reuniremos as informações que todos os clientes devem partilhar. Este bean desempenhará o papel tradicionalmente desempenhado pelo contexto da aplicação, mas com informações tipadas em vez de não tipadas.

O último ficheiro [springwebarticles.xml] define as ações aceites pela aplicação de uma forma muito semelhante à utilizada pelo ficheiro Struts [struts-config.xml]. O seu conteúdo é o seguinte:

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

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

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

     <!-- message file -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>

O que diz este ficheiro de configuração?

  • que o nosso controlador irá tratar das seguintes URLs:
main.do
para exibir a lista de artigos
liste.do
para exibir a lista de artigos
info.do
para exibir informações sobre um item específico
purchase.do
para comprar um item específico
cart.do
para visualizar o carrinho de compras
remove-purchase.do
para remover uma compra do carrinho de compras
confirmcart.do
para confirmar o carrinho de compras
  • As ações listadas acima correspondem, uma a uma, às ações tratadas pelo controlador nas versões anteriores. Para cada uma delas, é especificado o nome da classe responsável pelo seu tratamento. Tomemos o exemplo da ação [/panier.do]:
  • ela deve ser tratada pelo bean [VoirPanierController]. Este nome é arbitrário. É simplesmente uma chave.
                <prop key="/panier.do">VoirPanierController</prop>
  • A chave [VoirPanierController] é o nome de um bean definido no mesmo ficheiro de configuração:
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
  • O bean [VoirPanierController] define:
  • a classe a instanciar [istia.st.articles.web.spring.VoirPanierController] para tratar da ação
  • como instanciá-la. Aqui, o bean [config] definido por [applicationContext.xml] e instanciado no arranque da aplicação é fornecido como parâmetro. Isto será feito para todas as ações [Controller]. Assim, cada uma delas terá, num campo privado, o objeto [config] no qual encontrará todas as informações partilhadas entre todos os clientes.
  • Como os nomes das visualizações devem ser resolvidos:
    <!-- gestionnaire de vues -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

Tal como no Struts, a instância [Controller] que processa uma ação irá devolver, após o processamento, uma chave ao controlador Spring para indicar qual a vista que deve apresentar. Com base nesta chave, podem existir várias estratégias para gerar a vista associada à chave. A estratégia utilizada é aquela definida pelo bean [viewResolver]. Aqui, este bean está associado à classe [org.springframework.web.servlet.view.InternalResourceViewResolver] com vários parâmetros de inicialização. Sem entrar em detalhes, o bean [viewResolver] especifica aqui que, se a chave da vista for "XX", então a vista gerada será [/views/XX.jsp]. O tipo de vistas enviadas ao cliente pode ser alterado de várias formas:

  • alterando a classe de implementação do bean [viewResolver]
  • alterando os parâmetros de inicialização da classe de implementação

Assim, pode mudar de uma vista HTML para uma vista XML simplesmente alterando o valor do bean [viewResolver]

  • o nome de um ficheiro de mensagens para a aplicação (messageSource). Aqui, o ficheiro existirá, mas estará vazio. Não será utilizado. Deve ser colocado no [ClassPath] da aplicação. Aqui, será colocado em [WEB-INF/classes]. No Eclipse, isto é conseguido colocando-o em [WEB-INF/src]:

Image

3.7.4. As vistas JSP

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

Image

É criada uma única vista nova: redirpanier.jsp. É utilizada exclusivamente para redirecionar o cliente para a ação [/panier.do]. O seu código é o seguinte:

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

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

Recomenda-se aos leitores que consultem as definições das várias vistas na versão Struts.

3.7.5. Processamento de ações

As classes necessárias para o processamento das várias ações foram agrupadas no pacote [istia.st.articles.web.spring]:

Image

Vamos rever como funciona a aplicação Spring utilizando um exemplo:

  • O utilizador solicita a URL [http://localhost:8080/springwebarticles/main.do]

Image

O que aconteceu?

  • Foi consultado o ficheiro [web.xml] da aplicação [springwebarticles]:
<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
     <!-- application spring context loader -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- the servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- url mapping -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- entry document -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  • Se esta fosse a primeira solicitação à aplicação, várias coisas seriam acionadas:
    • o ouvinte [org.springframework.web.context.ContextLoaderListener] foi carregado
    • ele analisou o ficheiro de configuração [applicationContext.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>
  • Os beans acima foram criados no contexto da aplicação
  • O ficheiro [springwebarticles-servlet.xml] foi então utilizado:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- stock managers = controllers -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

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

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

     <!-- message file -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>
  • Os beans [Controller] definidos por este ficheiro também foram criados
  • Tudo está agora pronto para processar o pedido do cliente. O pedido foi: [http://localhost:8080/springwebarticles]. Aqui, não estamos a solicitar um URL do contexto, mas sim o próprio contexto. Por isso, é a secção [welcome-file] do ficheiro [web.xml] que é utilizada.
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
  • A vista [index.jsp] é a seguinte:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>
  • O cliente é, portanto, solicitado a redirecionar para o URL [http://localhost:8080/springwebarticles/main.do]. E assim o faz.
  • O controlador Spring recebe então um novo pedido. Utiliza o seu ficheiro [springwebarticles-servlet.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- stock managers = controllers -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

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

</beans>
  • Este ficheiro indica que a ação [/main.do] deve ser tratada pelo bean [ListController].
  • O pedido do cliente é passado para o método [handleRequest] do bean [ListController]. Este método executa a sua tarefa e devolve a chave da vista a ser apresentada ao controlador. Aqui, se tudo correr bem, esta chave será [list].
  • O controlador Spring utiliza o bean [viewResolver] do ficheiro de configuração [springwebarticles-servlet.xml] para determinar a vista associada a esta chave. Neste caso, será a vista [/vues/liste.jsp]
  • A vista [/vues/liste.jsp] é enviada ao cliente

3.7.6. Inicialização da aplicação Spring

Mencionámos que, quando a aplicação é iniciada, os beans no ficheiro [applicationContext.xml] são instanciados:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Estamos familiarizados com os beans [articlesDao, articlesDomain], mas não com o bean [config]. Este bean é definido pela seguinte classe Java:

package istia.st.articles.web.spring;

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

/**
 * @author ST - ISTIA
 */

public class Config {

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

     // getters-setters
    public IArticlesDomain getArticlesDomain() {
        return articlesDomain;
    }

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

    public Hashtable getHActionListe() {
        return hActionListe;
    }

    public Hashtable getHActionPanier() {
        return hActionPanier;
    }

    public Hashtable getHActionValidationPanier() {
        return hActionValidationPanier;
    }

     // init web application
    public void init() {
         // memorize certain application urls
        hActionListe.put("href", ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // it's over
        return;
    }
}

Esta classe desempenha a mesma função que o método [init] de um servlet de aplicação web. Inicializa a aplicação. Aqui, isso é feito da seguinte forma:

  • porque o bean [config] está definido da seguinte forma em [applicationContext.xml]:
    <!-- la configuration de l'application web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>

Quando é criado, o seu campo privado [articlesDomain] é inicializado

  • depois, devido ao atributo [init-method="init"] do bean acima, o método [init] da classe associada ao bean é executado. Aqui, ele inicializa os três dicionários [hActionListe, hActionPanier, hActionValidationPanier] utilizados para gerar os três possíveis links de menu oferecidos ao utilizador.
  • São criados acessores públicos para tornar estes campos privados acessíveis a instâncias do tipo [Controller] que irão tratar das ações.

3.7.7. As ações [Controller] da aplicação Spring

3.7.7.1. Introdução

Cada ação Spring será objeto de uma classe do tipo [Controller]. Na versão Struts, cada ação era objeto de uma classe do tipo [Action]. A escrita da classe [Controller] consiste, na maioria das vezes, em:

  • copiar e colar a classe [Action] que foi utilizada na versão Struts
  • adaptar o código às convenções do Spring

3.7.7.2. main.do, liste.do

Estas duas ações são idênticas e estão definidas no [springwebarticles-servlet.xml] da seguinte forma:

    <!-- le mapping de l'application-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
...
            </props>
        </property>
    </bean>

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

Estão associadas à classe [istia.st.articles.web.spring.ListController], que iremos discutir em detalhe em breve. Quando uma destas ações é executada num navegador, obtém-se o seguinte resultado:

Image

O código da classe [istia.st.articles.web.spring.ListController] é o seguinte:

package istia.st.articles.web.spring;

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

public class ListController implements Controller {

     // web app configuration
    Config config;

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

     // query processing
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

         // the list of items is requested
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // send error view
            return new ModelAndView("erreurs");
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // send view
        return new ModelAndView("liste");
    }

}

Comentários:

  • A classe tem um campo privado [config]. Este campo foi inicializado pelo Spring quando o bean [ListController] foi instanciado:
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Como mostrado acima, o campo [config] do [ListController] é inicializado com o bean [config]. O que é isto? É o bean [config] definido em [applicationContext.xml], ou seja, uma instância de [istia.st.articles.web.spring.Config] descrita acima.

  • Escrever o código para uma classe [Controller] envolve essencialmente escrever o código para o seu método [handleRequest]
  • Solicitamos a lista de artigos ao modelo. Esta é acessível através de [config.getArticlesDomain()]. Se ocorrer uma exceção, a vista [ERRORS] é renderizada. O resultado devolvido por [handleRequest] deve ser do tipo [ModelAndView]. Esta classe pode ser instanciada de várias formas. Aqui, e isto será sempre o caso, criamos uma instância de [ModelAndView] passando-lhe a chave da vista a ser exibida. Recorde-se que, com base na configuração do bean [viewResolver], solicitar a vista com a chave XX resultará no envio da vista [/vues/XX.jsp].
        // on demande la liste des articles
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
            // on mémorise l'erreur
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
            // on affiche la page des erreurs
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
            // envoyer la vue erreurs
            return new ModelAndView("erreurs");
        }
  • Se não houver erros, a vista [LIST] é enviada:
1
2
3
4
5
6
7
        // on affiche la liste des articles
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
        // envoyer la vue
        return new ModelAndView("liste");

3.7.7.3. infos.do

Esta ação é utilizada para fornecer informações sobre um dos itens apresentados na vista [LIST]:

Esta ação é definida em [springwebarticles-servlet.xml] da seguinte forma:

    <!-- le mapping de l'application-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/infos.do">InfosController</prop>
...
            </props>
        </property>
    </bean>
...
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

O código para a classe [InfosController] é o seguinte:

package istia.st.articles.web.spring;

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

public class InfosController implements Controller {

     // web app configuration
    Config config;

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

     // query processing
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

     // error list
    ArrayList erreurs = new ArrayList();
     // retrieve the requested id
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // the id key item is requested
    Article article = null;
    try {
      article = config.getArticlesDomain().getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // not normal
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
    if (article == null) {
       // not normal
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // put the article in the session
    request.getSession().setAttribute("article", article);
     // the info page is displayed
    request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
        return new ModelAndView("infos");
    }
}

Comentários:

  • O método [handleRequest] recupera o parâmetro [id], que normalmente deve estar presente na URL. A URL deve, de facto, ter o formato [/infos.do?id=X]. São realizadas várias verificações para confirmar a presença e a validade do parâmetro [id]. Se houver algum problema, é apresentada a vista [ERRORS].
  • Se [id] for válido, o item correspondente é solicitado à camada [domain]. Se isto lançar uma exceção ou se o item não for encontrado, a vista [ERROR] é enviada novamente.
  • Se tudo correr bem, o item recuperado é armazenado na sessão. Este é um ponto discutível. Aqui, assumimos que o cliente poderá comprar este item. Se o fizer, iremos recuperá-lo da sessão em vez de o solicitar novamente à camada [domain].
  • Por fim, a vista [INFO] é apresentada.

3.7.7.4. purchase.do

Esta ação é utilizada para comprar o item apresentado pela vista [INFOS] anterior:

Image

Se analisarmos o código HTML desta vista, vemos que a tag <form> está definida da seguinte forma:

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

Podemos ver que o formulário é enviado para o controlador com a ação [achat.do].

Esta ação está configurada da seguinte forma no ficheiro [springwebarticles-servlet.xml]:

    <!-- le mapping de l'application-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/achat.do">AchatController</prop>
...
            </props>
        </property>
    </bean>

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

O código para a classe [AchatController] é o seguinte:

package istia.st.articles.web.spring;

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

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

public class AchatController implements Controller {

   // web app configuration
  Config config;

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

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the session item
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if (article == null) {
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // we return to the list of items
    return new ModelAndView("index");
  }
}

Comentários:

  • Vamos rever o formato do formulário enviado ao controlador:
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>
  • Existem dois parâmetros no pedido: [id]: número do artigo, [qte]: quantidade adquirida.
  • A presença e a validade do parâmetro [qte] são verificadas. Se este parâmetro for considerado incorreto, a vista [INFOS] é devolvida ao utilizador juntamente com uma mensagem de erro:
    // la liste des erreurs sur cette action
    ArrayList erreurs = new ArrayList();
    // on récupère la quantité achetée
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
      // qté erronée
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
  • O item adquirido é recuperado da sessão. A sessão pode ter expirado. Neste caso, é enviada a vista [ERRORS]:
    // on récupère la session du client
    HttpSession session = request.getSession();
    // on récupère l'article mis en session
    Article article = (Article) session.getAttribute("article");
    // session expirée ?
    if (article == null) {
      // on affiche la page des erreurs
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • Se a sessão não tiver expirado, o item é adicionado ao carrinho, que também é recuperado da sessão:
1
2
3
4
5
6
7
8
9
    // on crée le nouvel achat
    Achat achat = new Achat(article, qté);
    // on ajoute l'achat au panier du client
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • Por fim, enviamos a vista [LIST]:
    // on revient à la liste des articles
    return new ModelAndView("index");
  • Acima, enviamos a vista [/views/index.jsp]. Sabemos que esta vista instrui o navegador do cliente a redirecionar para a URL [/main.do]. É este redirecionamento que irá apresentar a lista de itens.

3.7.7.5. cart.do

Esta ação é utilizada para apresentar todas as compras do cliente. Está disponível através do menu:

Image

O código HTML associado ao link acima é o seguinte:

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

A página apresentada por este link é a seguinte:

Image

Esta ação está configurada da seguinte forma em [springwebarticles-servlet.xml]:

    <!-- le mapping de l'application-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/panier.do">VoirPanierController</prop>
...
            </props>
        </property>
    </bean>
...
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

O código para a classe [VoirPanierController] é o seguinte:

package istia.st.articles.web.spring;

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

public class VoirPanierController implements Controller {

   // web app configuration
  Config config;

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

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the basket is displayed
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // empty basket
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("paniervide");
    } else {
       // there's something in the basket
      request.setAttribute("actions", new Hashtable[] {
          config.getHActionListe(), config.getHActionValidationPanier() });
      return new ModelAndView("panier");
    }
  }
}

Comentários:

  • O carrinho de compras é recuperado da sessão onde normalmente é armazenado. A sessão pode ter expirado; nesse caso, não há carrinho de compras. Não tratamos isto como um erro, mas simplesmente assumimos que o carrinho de compras está vazio.
  • Se o carrinho estiver vazio, é apresentada a vista [CARRINHO VAZIO]
  • caso contrário, é apresentada a vista [CARRINHO]

3.7.7.6. removePurchase.do

Esta ação é utilizada para remover uma compra do carrinho:

Image

Se analisarmos o código HTML do link acima, vemos o seguinte:

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

A ação [retirerachat.do] recebe, portanto, como parâmetro, o ID do artigo a ser removido do carrinho. Esta ação está configurada da seguinte forma no ficheiro [springwebarticles-servlet.xml]:

    <!-- le mapping de l'application-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/retirerachat.do">RetirerAchatController</prop>
            </props>
        </property>
    </bean>
...
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

O código da classe [RetirerAchatController] é o seguinte:

package istia.st.articles.web.spring;

import istia.st.articles.domain.Panier;

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

public class RetirerAchatController implements Controller {

   // web app configuration
  Config config;

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

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // we pick up the basket
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // retrieve the id of the item to be removed
    String strId = request.getParameter("id");
     // anything?
    if (strId == null) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // transform strId into an integer
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // not normal
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // we remove the purchase
    panier.enlever(id);
     // the basket is displayed again
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");
  }
}

Comentários:

  • O código verifica a presença e a validade do parâmetro [id]. Se estiver incorreto, é enviada a visualização [ERRORS].
  • Caso contrário, o artigo é removido do carrinho:
    // on enlève l'achat
    panier.enlever(id);
  • depois, o carrinho é recarregado:
    // on affiche de nouveau le panier
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");

Vamos rever o código da vista [/vues/redirpanier.jsp]:

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

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

Podemos ver que o cliente será redirecionado para a ação [/panier.do]. Isto já foi descrito. Apresentará a vista [PANIER] ou [PANIERVIDE], dependendo do estado do carrinho de compras.

3.7.7.7. confirmcart.do

Esta ação é utilizada para confirmar as compras do cliente. Na prática, isto envolve uma única ação: os níveis de stock dos artigos comprados são reduzidos nas quantidades adquiridas na base de dados. Esta ação provém do seguinte menu:

Image

O código HTML para o link [Confirmar Carrinho] é o seguinte:

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

Quando este link é clicado, os níveis de stock são atualizados e a lista de artigos é apresentada novamente.

Esta ação está configurada da seguinte forma no ficheiro [springwebarticles-servlet.xml]:

    <!-- le mapping de l'application-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>
...
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

O código para a classe [ValiderPanierController] é o seguinte:

package istia.st.articles.web.spring;

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

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

public class ValiderPanierController implements Controller {

   // web app configuration
  Config config;

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

   // query processing
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the buyer has confirmed his basket
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // validate basket
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // not normal
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // recover any errors
    erreurs = config.getArticlesDomain().getErreurs();
    if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe(), config.getHActionPanier() });
      return new ModelAndView("erreurs");
    }
     // everything looks OK - the item list is displayed
    return new ModelAndView("index");
  }
}

Comentários:

  • Recuperamos o carrinho de compras da sessão. Se a sessão tiver expirado, exibimos a vista [ERRORS]:
    // la liste des erreurs sur cette action
    ArrayList erreurs = new ArrayList();
    // l'acheteur a confirmé son panier
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
      // session expirée
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • Processamos as compras no carrinho. Podem ocorrer erros se os níveis de stock forem insuficientes para satisfazer as compras. Neste caso, apresentamos a vista [ERRORS]:
    // on valide le panier
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
      // pas normal
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
    // on récupère les éventuelles erreurs
    erreurs = config.getArticlesDomain().getErreurs();
    if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe(), config.getHActionPanier() });
      return new ModelAndView("erreurs");
    }
  • Se tudo correu bem, voltamos a apresentar a lista de itens:
    // tout semble OK - on affiche la liste des articles
    return new ModelAndView("index");

Sabemos que a vista [/vues/index.jsp] redireciona o cliente para a ação [/main.do]. Esta ação irá exibir a vista [LISTE].