Skip to content

3. Artykuł 2 – Przykłady trójwarstwowych architektur internetowych

Cele artykułu:

  • architektury trójwarstwowe
  • podstawowa architektura internetowa MVC
  • architektura Struts MVC
  • architektura Spring MVC

Wykorzystane narzędzia:

  • 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, sterownik JDBC. W rzeczywistości wystarczy dowolny kod źródłowy JDBC.
  • IBExpert, wersja osobista: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (marzec 2005). IBExpert umożliwia graficzne zarządzanie bazą danych Firebird SGBD.
  • Tomcat: http://jakarta.apache.org/tomcat/
  • Wtyczka Tomcat dla Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. Zobacz również dokument https://tahe.developpez.com/java/eclipse/

Zrozumienie niniejszego dokumentu wymaga spełnienia pewnych warunków wstępnych. Niektóre z nich można znaleźć w dokumentach, które sam napisałem. W takich przypadkach podaję do nich odniesienia. Oczywiście jest to jedynie sugestia, a czytelnik może korzystać ze swoich ulubionych materiałów.

  • język Java: [https://tahe.developpez.com/java/cours]
  • programowanie internetowe w Javie: [https://tahe.developpez.com/java/web/]
  • programowanie internetowe z wykorzystaniem Javy, Eclipse i Tomcat: [https://tahe.developpez.com/java/eclipse/]
  • programowanie internetowe z wykorzystaniem Struts: [https://tahe.developpez.com/java/struts/]
  • korzystanie z aspektu IoC w Spring: [https://tahe.developpez.com/java/springioc]
  • biblioteka tagów JSTL: [https://tahe.developpez.com/java/eclipse/] (częściowo)
  • dokumentacja Ibatis SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (marzec 2005 r.).

Pomysły zawarte w niniejszym dokumencie wywodzą się z książki przeczytanej latem 2004 roku, wspaniałego dzieła Roda Johnsona: J2EE „Development without EJB”, wydanej przez wydawnictwo Wrox.


3.1. Aplikacja webarticles

Chcielibyśmy tutaj przedstawić kilka elementów aplikacji internetowej służącej do handlu elektronicznego. Aplikacja ta umożliwi klientom korzystającym z sieci

  • przeglądać listę produktów pochodzących z bazy danych
  • dodać niektóre z nich do koszyka
  • oraz zatwierdzenia koszyka. Jedynym skutkiem tego zatwierdzenia będzie aktualizacja stanów magazynowych zakupionych produktów w bazie danych.

Użytkownikowi zostaną przedstawione następujące widoki:

  • widok [LISTE], zawierający listę produktów w sprzedaży

Image

  • widok [INFOS], który zawiera dodatkowe informacje o produkcie:

Image

  1. widok [PANIER], który pokazuje zawartość koszyka klienta

Image

  1. widok [PANIERVIDE] na wypadek, gdyby koszyk klienta był pusty

Image

  1. widok [ERREURS], który sygnalizuje wszelkie błędy aplikacji

Image

3.2. Ogólna architektura aplikacji

Chcemy stworzyć aplikację o następującej strukturze trójwarstwowej:

  • trzy warstwy są od siebie niezależne dzięki zastosowaniu interfejsów Java
  • integracja poszczególnych warstw odbywa się za pomocą biblioteki Spring
  • każda warstwa jest zawarta w oddzielnych pakietach: web (warstwa interfejsu użytkownika), domain (warstwa biznesowa) oraz dao (warstwa dostępu do danych).

Założymy tutaj, że warstwy [domain] i [dao] są już gotowe. Skupimy się wyłącznie na warstwie [web], którą zamierzamy zbudować na kilka sposobów:

  • przy użyciu klasycznej technologii serwletów kontrolerowych – strony JSP
  • z wykorzystaniem technologii Struts MVC
  • z wykorzystaniem technologii Spring – MVC

W każdym przypadku aplikacja będzie zgodna z architekturą MVC (Model – Widok – Kontroler). Jeśli powrócimy do powyższego schematu warstwowego, architektura MVC wpisuje się w niego w następujący sposób:

Przetwarzanie żądania od klienta przebiega zgodnie z następującymi etapami:

  1. klient wysyła żądanie do kontrolera. Kontroler ten jest serwletem, przez który przechodzą wszystkie żądania klientów. Stanowi on bramę wejściową do aplikacji. Jest to litera C w skrócie MVC.
  2. kontroler przetwarza to żądanie. W tym celu może potrzebować pomocy warstwy biznesowej, zwanej modelem M w strukturze MVC.
  3. Kontroler otrzymuje odpowiedź z warstwy biznesowej. Żądanie klienta zostało przetworzone. Może to skutkować kilkoma możliwymi odpowiedziami. Klasycznym przykładem jest
    • strona błędów, jeśli żądania nie udało się poprawnie przetworzyć
    • w przeciwnym razie strona potwierdzenia
  4. kontroler wybiera odpowiedź (= widok), którą ma wysłać do klienta. Najczęściej jest to strona zawierająca elementy dynamiczne. Kontroler dostarcza je do widoku.
  5. Widok jest wysyłany do klienta. Jest to „V” w skrócie MVC.

3.3. Model

W tym miejscu analizujemy literę M z kodu MVC. Model składa się tutaj z następujących elementów:

  1. klasy biznesowe
  2. klasy dostępu do danych
  3. baza danych

3.3.1. Baza danych

Baza danych zawiera tylko jedną tabelę o nazwie ARTICLES. Została ona wygenerowana za pomocą następujących poleceń 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
);


/* ograniczenia */
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<>'');

/* klucz główny */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
klucz podstawowy jednoznacznie identyfikujący artykuł
nom
nazwa artykułu
prix
jego cena
stockactuel
aktualny stan magazynowy
stockminimum
poziom zapasów, poniżej którego należy złożyć zamówienie na uzupełnienie zapasów

W poniższych testach wykorzystano bazę danych o nazwie [Firebird]. [Firebird] jest wersją „open source” bazy SGBD. Sterownik JDBC [firebirdsql-full.jar] znajduje się w folderze [WEB-INF/lib] aplikacji internetowej.

3.3.2. Pakiety szablonu

Szablon M jest tutaj dostarczony w postaci trzech archiwów:

  • istia.st.articles.dao: zawiera klasy dostępu do danych warstwy [dao]
  • istia.st.articles.exception: zawiera klasę wyjątków dla tego modułu zarządzania artykułami
  • istia.st.articles.domain: zawiera klasy biznesowe warstwy [domain]
archive
contenu
rôle
istia.st.articles.dao
- zawiera pakiet [istia.st.articles.dao], który z kolei zawiera następujące elementy:
- [IArticlesDao]: interfejs dostępu do warstwy DAO. Jest to jedyny interfejs widoczny dla warstwy [domain]. Nie widzi ona żadnego innego.
- [Article]: klasa definiująca artykuł
- [ArticlesDaoSqlMap]: klasa implementująca interfejs [IArticlesDao] za pomocą narzędzia SqlMap
warstwa dostępu do danych – znajduje się w całości w warstwie [dao] trójwarstwowej architektury aplikacji internetowej
istia.st.articles.domain
- zawiera pakiet [istia.st.articles.domain], który z kolei zawiera następujące elementy:
- [IArticlesDomain]: interfejs dostępu do warstwy [domain]. Jest to jedyny interfejs widoczny dla warstwy internetowej. Nie widzi ona żadnego innego.
- [AchatsArticles]: klasa implementująca [IArticlesDomain]
- [Achat]: klasa reprezentująca zakup klienta
- [Panier]: klasa reprezentująca zbiór zakupów klienta
reprezentuje model zakupów internetowych – znajduje się w całości w warstwie [domain] trójwarstwowej architektury aplikacji internetowej
istia.st.articles.exception
- zawiera pakiet [istia.st.articles.exception], który z kolei zawiera następujące elementy:
- [UncheckedAccessArticlesException]: klasa definiująca wyjątek typu [RuntimeException]. Ten typ wyjątku jest generowany przez warstwę [dao], gdy tylko wystąpi problem z dostępem do danych.
 

3.3.3. Pakiet [istia.st.articles.dao]

Klasa definiująca pozycję jest następująca:

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) {
     // inicjowanie atrybutów instancji
    setId(id);
    setNom(nom);
    setPrix(prix);
    setStockActuel(stockActuel);
    setStockMinimum(stockMinimum);
  }

     // metody pobierające i ustawiające
  public int getId() {
    return id;
  }

  public void setId(int id) {
     // czy identyfikator jest prawidłowy?
    if (id < 0)
      throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
    this.id = id;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
     // czy nazwa jest prawidłowa?
    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) {
     // czy cena jest prawidłowa?
    if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
    this.prix = prix;
  }

  public int getStockActuel() {
    return stockActuel;
  }

  public void setStockActuel(int stockActuel) {
     // prawidłowy stan magazynowy?
    if (stockActuel < 0)
      throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
    this.stockActuel = stockActuel;
  }

  public int getStockMinimum() {
    return stockMinimum;
  }

  public void setStockMinimum(int stockMinimum) {
     // Czy stan magazynowy jest prawidłowy?
    if (stockMinimum < 0)
      throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
    this.stockMinimum = stockMinimum;
  }

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

Klasa ta udostępnia:

  1. konstruktor umożliwiający ustawienie 5 informacji dotyczących artykułu
  2. metody dostępowe, często nazywane getterami/setterami, służące do odczytu i zapisu tych 5 informacji. Nazwy tych metod są zgodne ze standardem JavaBean. Typowym zastosowaniem jest wykorzystanie obiektów JavaBean w warstwie DAO w celu zapewnienia interfejsu z danymi z SGBD.
  3. weryfikacja danych wprowadzonych do pozycji. W przypadku błędnych danych generowany jest wyjątek.
  4. metoda toString, która pozwala uzyskać wartość artykułu w postaci ciągu znaków. Jest to często przydatne podczas debugowania aplikacji.

Interfejs [IArticlesDao] jest zdefiniowany w następujący sposób:

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);
}

Rola poszczególnych metod interfejsu jest następująca:

getAllArticles
zwraca wszystkie pozycje z tabeli ARTICLES w postaci listy obiektów [Article]
clearAllArticles
opróżnia tabelę ARTICLES
getArticleById
zwraca obiekt [Article] identyfikowany przez jego klucz główny
ajouteArticle
umożliwia dodanie pozycji do tabeli ARTICLES
modifieArticle
umożliwia modyfikację pozycji w tabeli [ARTICLES]
supprimerArticle
umożliwia usunięcie pozycji z tabeli [ARTICLES]
changerStockArticle
umożliwia zmianę stanu magazynowego pozycji w tabeli [ARTICLES]

Interfejs udostępnia programom klienckim pewną liczbę metod zdefiniowanych wyłącznie poprzez ich sygnatury. Nie zajmuje się tym, w jaki sposób metody te zostaną faktycznie zaimplementowane. Zapewnia to elastyczność aplikacji. Program kliencki wywołuje interfejs, a nie jego konkretną implementację.

Wybór konkretnej implementacji nastąpi za pomocą pliku konfiguracyjnego Spring. Proponujemy tutaj zaimplementować interfejs IArticlesDao przy użyciu produktu open source o nazwie SqlMap. Pozwoli nam to usunąć wszystkie instrukcje SQL z kodu Java.

Klasa implementacyjna [ArticlesDaoSqlMap] jest zdefiniowana w następujący sposób:

package istia.st.articles.dao;

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

public class ArticlesDaoSqlMap implements IArticlesDao {

   // Pola
  private SqlMapClient sqlMap;

   // Konstruktory
  public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }

   // Metody
  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) {}
}

Wszystkie metody dostępu do danych zostały zsynchronizowane, aby uniknąć problemów związanych z równoczesnym dostępem do źródła danych. W danym momencie tylko jeden wątek ma dostęp do danej metody.

Klasa [ArticlesDaoSqlMap] korzysta z narzędzia [Ibatis SqlMap]. Zaletą tego narzędzia jest umożliwienie wyodrębnienia kodu SQL odpowiedzialnego za dostęp do danych z kodu Java. Kod ten jest następnie umieszczany w pliku konfiguracyjnym. Wrócimy do tego później. Aby zostać utworzona, klasa [ArticlesDaoSqlMap] potrzebuje pliku konfiguracyjnego, którego nazwa jest przekazywana jako parametr do konstruktora klasy. Ten plik konfiguracyjny określa informacje niezbędne do:

  • uzyskanie dostępu do SGBD, w którym znajdują się artykuły
  • zarządzanie pulą połączeń
  • zarządzanie transakcjami

W naszym przykładzie plik ten będzie nosił nazwę [sqlmap-config-firebird.xml] i zdefiniuje dostęp do bazy danych 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>

Wspomniany powyżej plik konfiguracyjny [articles.xml] pozwala zdefiniować sposób tworzenia instancji klasy [istia.st.articles.dao.Article] na podstawie wiersza z tabeli [ARTICLES] w SGBD. Określa on również zapytania SQL, które umożliwią warstwie [dao] pobranie danych ze źródła danych 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">

     <!-- alias klasy istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- mapowanie ORM:  wiersz tabeli ARTICLES – instancja klasy 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>

     <!-- zapytanie SQL w celu pobrania wszystkich artykułów -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- zapytanie SQL w celu usunięcia wszystkich artykułów -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- zapytanie SQL w celu dodania artykułu -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#(id#,#nazwa#,#cena#,#aktualny stan magazynowy#,#minimalny stan magazynowy#)
</statement>

     <!-- zapytanie SQL w celu usunięcia określonego artykułu -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- zapytanie SQL w celu zmiany określonego artykułu -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nazwa#,
    prix=#cena#,aktualnystan=#aktualnystan#,minimalnystan=#minimalnystan# where
    id=#id#
</statement>

     <!-- zapytanie SQL w celu uzyskania określonego artykułu -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- zapytanie SQL w celu zmiany stanu magazynowego danego artykułu -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#ruch#
    where id=#id# oraz stockActuel+#ruch#&gt;=0
</statement>
</sqlMap>

Kod pakietu [dao] znajduje się w załączniku.

3.3.4. Pakiet [istia.st.articles.domain]

Interfejs [IArticlesDomain] oddziela warstwę [métier] od warstwy [web]. Ta ostatnia uzyskuje dostęp do warstwy [métier/domain] za pośrednictwem tego interfejsu, nie zwracając uwagi na klasę, która faktycznie go implementuje. Interfejs definiuje następujące działania dotyczące dostępu do warstwy biznesowej:

 package istia.st.articles.domain;

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

public abstract interface IArticlesDomain {

   // Metody
  void acheter(Panier panier);
  List getAllArticles();
  Article getArticleById(int idArticle);
  ArrayList getErreurs();
}
List getAllArticles()
zwraca listę obiektów [Article], która ma zostać przedstawiona klientowi
Article getArticleById(int idArticle)
zwraca obiekt [Article] zidentyfikowany przez [idArticle]
void acheter(Panier panier)
zatwierdza koszyk klienta, zmniejszając stany magazynowe zakupionych artykułów o zakupioną ilość – może zakończyć się niepowodzeniem, jeśli stan magazynowy jest niewystarczający
ArrayList getErreurs()
zwraca listę wystąpionych błędów – pusta, jeśli nie ma błędów

W tym przypadku interfejs [IArticlesDomain] zostanie zaimplementowany przez następującą klasę [AchatsArticles]:

package istia.st.articles.domain;

// Importy
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 {

   // Pola
  private IArticlesDao articlesDao;
  private ArrayList erreurs;

   // Konstruktorzy
  public AchatsArticles(IArticlesDao articlesDao) { }

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

Klasa ta implementuje cztery metody interfejsu [IArticlesDomain]. Posiada dwa pola prywatne:

IArticlesDao articlesDao
obiekt dostępu do danych dostarczony przez warstwę dostępu do danych
ArrayList erreurs
lista ewentualnych błędów

Aby utworzyć instancję tej klasy, należy podać obiekt umożliwiający dostęp do danych z SGBD:

public AchatsArticles(IArticlesDao articlesDao)
konstruktor

Klasa [Achat] reprezentuje zakup dokonany przez klienta:

package istia.st.articles.domain;

public class Achat {

   // Pola
  private Article article;
  private int qte;

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

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

Klasa [Achat] jest instancją klasy JavaBean z następującymi polami i metodami:

article
zakupiony artykuł
qte
zakupiona ilość
double getTotal()
podaje kwotę zakupu
String toString()
ciąg identyfikacyjny obiektu

Klasa [Panier] reprezentuje wszystkie zakupy klienta:

package istia.st.articles.domain;

// Importy
import java.util.ArrayList;

public class Panier {

  // Pola
  private ArrayList achats;

   // Konstruktorzy
  public Panier() { }

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

Klasa [Panier] jest instancją klasy JavaBean z następującymi polami i metodami:

achats
lista zakupów klienta – lista obiektów typu [Achat]
void ajouter(Achat unAchat)
dodaje zakup do listy zakupów
void enlever(int idArticle)
usuwa zakup artykułu idArticle
double getTotal()
podaje łączną kwotę zakupów
String toString()
zwraca ciąg identyfikacyjny koszyka
ArrayList getAchats()
zwraca listę zakupów

Kod pakietu [domain] znajduje się w załączniku.

3.3.5. Pakiet [istia.st.articles.exception]

Ten pakiet zawiera klasę definiującą wyjątek zgłaszany przez warstwę [dao] w przypadku wystąpienia problemu z dostępem do źródła danych:

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. Testy modelu

Szablon M został przetestowany w środowisku Eclipse przy następującej konfiguracji:

Image

Uwagi:

  • w pliku [WEB-INF/lib] znajdują się:
    • archiwa niezbędne dla narzędzia [ibatis SqlMap], odpowiedzialnego za dostęp do bazy danych Firebird SGBD: ibatis-*.jar
    • plik niezbędny dla narzędzia [spring]: spring.jar
    • sterownik JDBC dla SGBD i [Firebird]: firebirdsql-full.jar
    • archiwa niezbędne do generowania logów: log4-*.jar, commons-logging.jar
    • trzy archiwa testowanego modelu: istia.st.articles.*.jar
    • plik archiwum niezbędny dla narzędzia testowego [junit]
  • w archiwum [WEB-INF/src] znajdują się pliki konfiguracyjne, które zostaną automatycznie skopiowane do archiwum [WEB-INF/classes] przez Eclipse:
    • pliki konfiguracyjne narzędzia [sqlmap]: sqlmap-config-firebird.xml, articles.xml
    • narzędzia [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
    • plik konfiguracyjny narzędzia [log4j]: log4j.properties
  • w pakiecie [istia.st.articles.tests] znajdują się klasy testowe modelu

3.3.6.1. Testy warstwy [dao]

Klasa testowa JUnit warstwy [dao] wygląda następująco. Jej analiza pozwala zrozumieć, w jaki sposób wykorzystywane są metody interfejsu [IArticlesDao]:

package istia.st.articles.tests.dao;

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

// test klasy ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {

     // instancja testowanej klasy
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // pobiera instancję dostępu do danych
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }

    public void testGetAllArticles() {
         // wyświetla artykuły
        listArticles();
    }

    public void testClearAllArticles() {
         // opróżnia tabelę artykułów
        articlesDao.clearAllArticles();
         // odczytuje tabelę ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
    }

    public void testAjouteArticle() {
         // usuwanie zawartości tabeli ARTICLES
        articlesDao.clearAllArticles();
         // odczytuje tabelę ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // wstawianie
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // odczytuje tabelę ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         //wyświetla ją
        listArticles();
    }

    public void testSupprimeArticle() {
         // usunięcie zawartości z tabeli ARTICLES
        articlesDao.clearAllArticles();
         // odczytuje tabelę ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // wstawianie
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // odczytuje tabelę ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // usunięcie
        articlesDao.supprimeArticle(4);
         // odczytuje tabelę ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(1, articles.size());
         // wyświetla tabelę
        listArticles();
    }

    public void testModifieArticle() {
         // usunięcie zawartości tabeli ARTICLES
        articlesDao.clearAllArticles();
         // odczytuje tabelę ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // wstawianie
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // odczytuje tabelę ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
         // modyfikacja
        articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
         // getById
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getPrix(), 44, 1e-6);
         // wyświetla tabelę
        listArticles();
    }

    public void testGetArticleById() {
         // usunięcie zawartości z ARTICLES
        articlesDao.clearAllArticles();
         // odczytuje tabelę ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // wstawianie
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // odczytuje tabelę ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

    private void listArticles() {
         // odczytuje tabelę ARTICLES
        List articles = articlesDao.getAllArticles();
         // wyświetla przeczytane artykuły
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

    public void testChangerStockArticle() throws InterruptedException {
         // usunięcie zawartości z ARTICLES
        articlesDao.clearAllArticles();
         // wstawianie
        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);
         // utworzenie 100 wątków aktualizacji stanu magazynowego artykułu 3
        Thread[] taches = new Thread[100];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
            taches[i].start();
        }
         // oczekiwanie na zakończenie wątków
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // pobranie artykułu 3 i sprawdzenie jego stanu magazynowego
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        assertEquals(1, unArticle.getStockActuel());
         // zmiana stanu magazynowego artykułu 4
        boolean erreur = false;
        int nbLignes = articlesDao.changerStockArticle(4, -100);
        assertEquals(0, nbLignes);
         // wyświetla tabelę
        listArticles();
    }
}

Komentarze:

  • klasa testowa przechowuje, za pomocą swojej metody setUp, instancję klasy, która ma zostać przetestowana:
1
2
3
4
5
6
7
8
     // instancja testowanej klasy
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // pobiera instancję klasy dostępu do danych
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }
  • obiekt testowy jest dostarczany przez metodę [Spring]. Powyżej wywołujemy fasolkę Spring o nazwie [articlesDao]. Ta fasolka jest zdefiniowana w pliku konfiguracyjnym 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>
     <!-- klasa dostępu do danych -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
</beans>

Jak widać powyżej, bean [articlesDao] jest instancją klasy [istia.st.articles.dao.ArticlesDaoSqlMap]. Klasa ta posiada konstruktor, który jako parametr oczekuje nazwy pliku konfiguracyjnego narzędzia [SqlMap]. Nazwa ta została tutaj podana. Jest to [sqlmap-config-firebird.xml]. Została ona już wcześniej opisana. Zawiera ona wszystkie informacje niezbędne do uzyskania dostępu do danych SGBD.

Metoda [testChangerStockArticle] tworzy 100 wątków odpowiedzialnych za zmniejszenie stanu magazynowego danego artykułu. Chodzi tutaj o przetestowanie współbieżnego dostępu do pliku SGBD. Ponieważ metoda [changerStockArticle] klasy [istia.st.articles.dao.ArticlesDaoSqlMap] została zsynchronizowana, test ten przebiega pomyślnie. Jeśli usunie się synchronizację, test nie przebiega już pomyślnie. Klasa odpowiedzialna za aktualizację stanu magazynowego jest następująca:

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() {
         // monitorowanie
        System.out.println(name + " lancé");
         // zmiana stanu magazynowego artykułu 3
        articlesDao.changerStockArticle(3, -1);
         // monitorowanie
        System.out.println(name + " terminé");
    }
}
  • Powyższa klasa zmniejsza o 1 stan magazynowy artykułu nr 3

3.3.6.2. Testy warstwy [domain]

Klasa testowa JUnit warstwy [domain] wygląda następująco:

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 klasy ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {

     // instancja klasy dostępu do domeny
    private IArticlesDomain articlesDomain;

     // instancja klasy dostępu do danych
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // pobiera instancję klasy dostępu do domeny
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // pobiera instancję klasy dostępu do danych
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }

     // pobieranie konkretnego artykułu
    public void testGetArticleById() {
         // usunięcie zawartości z ARTICLES
        articlesDao.clearAllArticles();
         // odczytuje tabelę ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // wstawianie
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // odczytuje tabelę ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDomain.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

     // wyświetlanie na ekranie
    private void listArticles() {
         // odczytuje tabelę ARTICLES
        List articles = articlesDomain.getAllArticles();
         // wyświetla odczytane pozycje
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

     // zakupy artykułów
    public void testAchatPanier(){
         // usuwanie zawartości z ARTICLES
        articlesDao.clearAllArticles();
         // odczytuje tabelę ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // wstawianie
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // odczytuje tabelę ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // utworzenie koszyka z dwoma produktami
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // weryfikacje
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // zatwierdzenie koszyka
        articlesDomain.acheter(panier);
         // weryfikacje
        assertEquals(0,articlesDomain.getErreurs().size());
        assertEquals(0,panier.getAchats().size());
         // wyszukiwanie produktu nr 3
        article3=articlesDomain.getArticleById(3);
        assertEquals(20,article3.getStockActuel());
         // wyszukaj produkt nr 4
        article4=articlesDomain.getArticleById(4);
        assertEquals(30,article4.getStockActuel());
         // nowy koszyk
        panier.ajouter(new Achat(article3,100));
         // potwierdzenie koszyka
        articlesDomain.acheter(panier);
         // sprawdź – kupiono za dużo
         // chyba wystąpił błąd
        assertEquals(1,articlesDomain.getErreurs().size());
         // wyszukaj produkt nr 3
        article3=articlesDomain.getArticleById(3);
         // stan magazynowy nie powinien się zmienić
        assertEquals(20,article3.getStockActuel());    
    }

     // usunąć zakupy
    public void testRetirerAchats(){
         // usunięcie zawartości z ARTICLES
        articlesDao.clearAllArticles();
         // odczytuje tabelę ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // wstawianie
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // odczytuje tabelę ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // utworzenie koszyka z dwoma produktami
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // weryfikacje
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // dodanie już zakupionego produktu
        panier.ajouter(new Achat(article3,10));
         // weryfikacje
         // suma musi zostać zmieniona na 1000
        assertEquals(1000.0,panier.getTotal(),1e-6);
         // w koszyku nadal znajdują się 2 produkty
        assertEquals(2,panier.getAchats().size());
         // ilość produktu 3 musi zostać zmieniona na 20
        Achat achat=(Achat)panier.getAchats().get(0);
        assertEquals(20,achat.getQte());
         // usuwamy produkt nr 3 z koszyka
        panier.enlever(3);
         // sprawdzenie
         // suma musi wynosić 400
        assertEquals(400.0,panier.getTotal(),1e-6);
         // W koszyku znajduje się tylko 1 pozycja
        assertEquals(1,panier.getAchats().size());
         // musi to być produkt nr 4
        assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
    }
}

Uwagi:

  • klasa testowa, dzięki swojej metodzie setUp, przechowuje instancję klasy, która ma być testowana, a także instancję klasy dostępu do danych. Ta ostatnia kwestia budzi wątpliwości. Teoretycznie klasa testowa nie powinna potrzebować dostępu do warstwy [dao], o której nawet nie powinna wiedzieć. W tym przypadku zignorowaliśmy tę „etykę”, której przestrzeganie zmusiłoby nas do utworzenia nowych metod w naszym interfejsie [IArticlesDomain].
     // instancja klasy dostępu do domeny
    private IArticlesDomain articlesDomain;

     // instancja klasy dostępu do danych
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // pobiera instancję klasy dostępu do domeny
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // pobiera instancję klasy dostępu do danych
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }
  • obiekt do testowania jest dostarczany przez [Spring]. Powyżej żądamy fasoli Spring o nazwie [articlesDomain]. Ta fasola jest zdefiniowana w pliku konfiguracyjnym 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>
     <!-- klasa dostępu do danych -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- klasa biznesowa -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Jak widać powyżej, bean [articlesDomain] jest instancją klasy [istia.st.articles.domain.AchatsArticles]. Klasa ta posiada konstruktor, który jako parametr oczekuje obiektu dostępu do warstwy [dao] typu [IArticlesDao]. W tym przypadku plik konfiguracyjny wskazuje, że obiektem tym jest bean o nazwie [articlesDao]. Wymusza to na Springu utworzenie instancji tego beana. Utworzenie instancji beana [articlesDao] zostało wyjaśnione wcześniej. Ostatecznie utworzono więc dwie instancje beanów:

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

Te dwie instancje są tworzone przez pierwsze wywołanie Springa:

1
2
3
4
         // pobiera instancję dostępu do domeny
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");

W ten sposób pobierany jest bean [articlesDomain]. Podczas drugiego wywołania Springa:

1
2
3
         // pobiera instancję dostępu do danych
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");

[Spring] zwraca jedynie odwołanie do fasoli [articlesDao], która została już utworzona podczas poprzedniego wywołania. Na tym polega zasada działania singletonu. Jeśli zażądamy fasoli od Springa, instancjonuje ją, jeśli jeszcze nie istnieje, w przeciwnym razie zwraca odwołanie do istniejącej fasoli.

3.4. Trójwarstwowa aplikacja internetowa MVC

W dalszej części zamierzamy zbudować następującą trójwarstwową aplikację internetową:

Aplikacja będzie miała architekturę MVC. Model M został napisany i przetestowany. Jest to ten opisany wcześniej. Został nam dostarczony w trzech archiwach [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Musimy napisać kontroler C i widoki V.

Najpierw rozważymy klasyczne podejście, w którym:

  • kontroler C jest realizowany przez pojedynczy serwlet
  • widoki V są realizowane przez strony JSP

3.5. Architektura MVC oparta na serwlecie kontrolującym i stronach JSP

Architektura aplikacji MVC będzie wyglądać następująco:

M=modèle
klasy biznesowe, klasy dostępu do danych oraz baza danych
V=vues
strony JSP
C=contrôleur
serwlet przetwarzający żądania klientów

3.5.1. Model

Został on przedstawiony wcześniej. Składa się z archiwów Java o nazwie [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.5.2. Widoki

Widoki odpowiadają tym, które zostały przedstawione na początku dokumentu:

LISTE
liste.jsp
Widoki są zgromadzone w folderze [vues] aplikacji
INFOS
infos.jsp
PANIER
panier.jsp
PANIERVIDE
paniervide.jsp
ERREURS
erreurs.jsp

3.5.3. Kontroler

Kontroler będzie składał się z pojedynczego serwletu o nazwie [WebArticles]. Będzie on przetwarzał różne żądania klientów. Żądania te będą formalizowane poprzez obecność parametru [action] w żądaniu HTTP wysyłanym przez klienta:

requête
znaczenie
działanie kontrolera
możliwe odpowiedzi
action=liste
klient chce listę
artykułów
– prosi o listę artykułów na poziomie
biznesowej
– [LISTE]
- [ERREURS]
action=infos
klient prosi o
informacji na temat jednego z artykułów wyświetlanych w widoku
[LISTE]
- wysyła zapytanie o produkt do warstwy biznesowej
- [INFOS]
- [ERREURS]
action=achat
klient kupuje artykuł
- wysyła zapytanie o produkt do warstwy biznesowej i
dodaje go do koszyka klienta
- [INFOS] w przypadku błędu ilości
- [LISTE], jeśli nie ma błędu
action=retirerachat
klient chce usunąć
zakup z koszyka
- pobiera koszyk z sesji i modyfikuje go
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
klient chce wyświetlić swój
koszyk
- pobiera koszyk z sesji
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
klient zakończył zakupy
i przechodzi do etapu płatności
- aktualizuje w bazie stan magazynowy
zakupionych produktów
- usuwa z koszyka klienta produkty, których
został zatwierdzony
- [LISTE]
- [ERREURS]

3.5.4. Konfiguracja aplikacji

Postaramy się skonfigurować aplikację tak, aby była jak najbardziej elastyczna w obliczu zmian, takich jak:

  1. zmiana adresów URL poszczególnych widoków
  2. zmiana klas implementujących interfejsy [IArticlesDao] i [IArticlesDomain]
  3. zmiana klasy SGBD, bazy danych oraz tabeli artykułów

3.5.5. Zmiany adresów URL

Nazwy adresów URL widoków zostaną umieszczone w pliku konfiguracyjnym aplikacji [web.xml] wraz z kilkoma innymi parametrami:

<?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>

W pliku [web.xml] znajdują się

  • adresy URL różnych widoków aplikacji
  • nazwa pliku konfiguracyjnego Springa [springConfigFileName], który umożliwi tworzenie obiektów singletonowych zapewniających dostęp do warstw biznesowych i DAO
  • widok [/vues/index.jsp], który zostanie wyświetlony, gdy adres URL żądany przez klienta będzie miał postać /<context>, gdzie <context> to kontekst aplikacji

3.5.6. Zmiana klas implementujących interfejsy

Zgodnie z zasadami architektury trójwarstwowej warstwy muszą być od siebie odizolowane. Izolację tę uzyskuje się w następujący sposób:

  • warstwy komunikują się ze sobą za pośrednictwem interfejsów, a nie konkretnych klas
  • kod jednej warstwy nigdy sam nie instancjonuje klasy z innej warstwy w celu jej wykorzystania. Po prostu zwraca się do narzędzia zewnętrznego, w tym przypadku [Spring], z prośbą o instancję implementacji interfejsu warstwy, z której chce skorzystać. W tym celu wiemy, że nie musi ona znać nazwy klasy implementującej, a jedynie nazwę bean’a Springa, do którego chce uzyskać odwołanie.

W naszej aplikacji plik konfiguracyjny Springa mógłby wyglądać następująco:

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

Aby uzyskać dostęp do warstwy [métier], klasa z warstwy [Interface utilisateur, UI] może zażądać bean’a [articlesDomain]. Spring utworzy wówczas instancję obiektu typu [istia.st.articles.domain.AchatsArticles]. Do tej instancji potrzebny jest bean typu [articlesDao], czyli obiekt typu [istia.st.articles.dao.ArticlesDaoSqlMap]. Następnie Spring utworzy instancję takiego obiektu. Instancja ta zostanie utworzona na podstawie informacji zawartych w pliku [sqlmap-config-firebird.xml], który jest plikiem konfiguracyjnym dostępu do danych za pośrednictwem SqlMap. Po zakończeniu operacji klasa [UI], która zażądała bean’a [articlesDomain], posiada cały łańcuch łączący ją z danymi z SGBD:

3.5.7. Zmiany związane z SGBD lub bazą danych

Niezależność aplikacji internetowej od zmian związanych z SGBD lub bazą danych jest tutaj zapewniona przez pliki konfiguracyjne SqlMap. Są dwa:

  1. plik [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>

Ten plik odnosi się tutaj do bazy danych Firebird. Wystarczy zmienić nazwę sterownika JDBC, aby pracować z innym sterownikiem, np. SGBD.

  1. plik [articles.xml], który zawiera różne instrukcje SQL niezbędne dla aplikacji:
<?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">

     <!-- alias klasy istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- mapowanie ORM:  wiersz tabeli ARTICLES – instancja klasy Artykuł -->
  <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>

     <!-- zapytanie SQL w celu pobrania wszystkich artykułów -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- zapytanie SQL w celu usunięcia wszystkich artykułów -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- zapytanie SQL w celu dodania artykułu -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#(id#,#nazwa#,#cena#,#aktualny stan magazynowy#,#minimalny stan magazynowy#)
</statement>

     <!-- zapytanie SQL w celu usunięcia określonego artykułu -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- zapytanie SQL w celu zmiany określonego artykułu -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#nazwa#,
    prix=#cena#,aktualnystan=#aktualnystan#,minimalnystan=#minimalnystan# where
    id=#id#
</statement>

     <!-- zapytanie SQL w celu uzyskania określonego artykułu -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- zapytanie SQL w celu zmiany stanu magazynowego danego artykułu -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#ruch#
    where id=#id# oraz stockActuel+#ruch#&gt;=0
</statement>
</sqlMap>

Gdyby nazwy tabeli artykułów lub kolumn uległy zmianie, konieczne byłoby przepisanie zapytań w tym pliku konfiguracyjnym bez konieczności modyfikowania kodu Java. Tak samo byłoby w przypadku, gdyby zapytanie zostało zastąpione procedurą przechowywaną ze względu na wydajność.

3.5.8. Ogólna architektura aplikacji [webarticles]

Aplikacja internetowa napisana w Javie to układanka składająca się z wielu elementów. Nadanie jej architektury MVC zazwyczaj zwiększa ich liczbę. Struktura aplikacji [webarticles] w ramach [eclipse] wygląda następująco:

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

3.5.9. Widoki JSP

Widoki JSP korzystają z biblioteki znaczników JSTL.

3.5.9.1. entete.jsp

Aby zapewnić spójność poszczególnych widoków, będą one miały ten sam nagłówek, zawierający nazwę aplikacji wraz z menu:

Menu jest dynamiczne i ustalane przez kontroler. Kontroler umieszcza w żądaniu przesyłanym do strony JSP atrybut klucza „actions”, którego wartością jest tablica typu Hastable[]. Każdy element tej tablicy jest słownikiem służącym do wygenerowania opcji w menu nagłówka. Każdy słownik ma dwa klucze:

  • href: adres URL powiązany z opcją menu
  • lien: tekst menu

Pozostałe widoki aplikacji będą korzystać z nagłówka zdefiniowanego przez [entete.jsp] za pomocą następującego tagu JSP:

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

Podczas wykonywania ten tag spowoduje włączenie do kodu strony JSP, która go zawiera, kodu strony [entete.jsp]. Ponieważ adres URL strony jest adresem względnym (brak znaku /), strona [entete.jsp] będzie wyszukiwana w tym samym katalogu, co strona zawierająca tag <jsp:include>.

Kod:

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

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

3.5.9.2. liste.jsp

Ten widok wyświetla listę artykułów dostępnych w sprzedaży:

Lista ta jest wyświetlana po wysłaniu żądania /main?action=liste lub /main?action=validationpanier. Elementy żądania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu
listarticles
ArrayList obiektów typu [Article]
message
obiekt typu String – komunikat do wyświetlenia na dole strony

Każdy link [Infos] z tablicy HTML artykułów ma adres URL w postaci [?action=infos&id=ID], gdzie ID to pole id wyświetlanego artykułu.

Kod:

<%@ 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

Ten widok wyświetla informacje o produkcie i umożliwia również jego zakup:

Image

Jest wyświetlany po wysłaniu żądania /main?action=infos&id=ID lub żądania /main?action=achat&id=ID, gdy ilość zakupiona jest nieprawidłowa. Elementy żądania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu
article
obiekt typu [Article] – artykuł do wyświetlenia
msg
obiekt typu String – komunikat do wyświetlenia w przypadku błędu dotyczącego ilości
qte
obiekt typu String – wartość do wyświetlenia w polu wprowadzania danych [Qte]

Pola [msg] i [qte] są używane w przypadku błędu wprowadzenia ilości:

Image

Ta strona zawiera formularz, który jest wysyłany za pomocą przycisku [Acheter]. Adres docelowy POST to [?action=achat&id=ID], gdzie ID to identyfikator zakupionego artykułu.

Kod:

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

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

3.5.9.4. panier.jsp

Ten widok wyświetla zawartość koszyka:

Image

Jest ona wyświetlana po wysłaniu żądania /main?action=koszyk lub /main?action=usunąćzakup&id=ID. Elementy żądania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu
panier
obiekt typu [Panier] – koszyk do wyświetlenia

Każdy link [Retirer] z tablicy HTML zawierającej zakupy w koszyku ma adres URL w postaci [?action=retirerachat&id=ID], gdzie ID to pole [id]produktu, który chcemy usunąć z koszyka.

Kod:

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

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

3.5.9.5. paniervide.jsp

Ten widok wyświetla informację, że koszyk jest pusty:

Image

Informacja ta jest wyświetlana po wysłaniu żądania /main?action=koszyk lub /main?action=usunąćzakup&id=ID. Elementy żądania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu

Kod:

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

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

3.5.9.6. erreurs.jsp

Ten widok jest wyświetlany w przypadku wystąpienia błędów:

Image

Jest on wyświetlany po każdym zapytaniu, które zakończyło się błędem, z wyjątkiem akcji zakupu z błędną ilością, obsługiwanej przez widok [INFOS]. Elementy zapytania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu
erreurs
ArrayList obiektów typu String reprezentujących komunikaty o błędach do wyświetlenia

Kod:

<%@ 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

Ta strona jest zdefiniowana jako strona główna aplikacji w pliku [web.xml] aplikacji:

<?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>

Widok [index.jsp] po prostu przekierowuje klienta do punktu wejścia aplikacji:

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

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

3.5.10. Kontroler

Pozostaje jeszcze napisanie serca naszej aplikacji internetowej, czyli kontrolera. Jego rolą jest:

  • odbioru żądania od klienta,
  • przetworzyć żądanie zgłoszone przez użytkownika przy użyciu klas biznesowych,
  • wysłać w odpowiedzi odpowiedni widok.

3.5.10.1. Inicjalizacja kontrolera

Gdy klasa kontrolera zostanie załadowana przez serwer serwletów, wykonywana jest jej metoda [init]. Odbywa się to tylko raz. Po załadowaniu do pamięci kontroler pozostaje w niej i przetwarza żądania różnych klientów. Każdy klient jest obsługiwany przez osobny wątek wykonawczy, dzięki czemu metody kontrolera są wykonywane jednocześnie przez różne wątki. Należy pamiętać, że z tego powodu kontroler nie może posiadać pól, które mogłyby być modyfikowane przez jego metody. Jego pola muszą być tylko do odczytu. Są one inicjowane przez metodę [init], co stanowi jej główną rolę. Metoda ta ma bowiem tę cechę, że jest wykonywana tylko raz przez jeden wątek. Nie ma zatem problemów z równoczesnym dostępem do pól kontrolera w tej metodzie. Metoda [init] ma na celu zainicjowanie obiektów niezbędnych dla aplikacji internetowej, które będą współdzielone w trybie tylko do odczytu przez wszystkie wątki klienckie. Te współdzielone obiekty można umieścić w dwóch miejscach:

  • w polach prywatnych kontrolera
  • kontekście wykonania aplikacji (ServletContext)

Metoda [init] aplikacji [webarticles] wykona następujące czynności:

  • sprawdzi, czy w pliku [web.xml] znajdują się parametry niezbędne do prawidłowego działania aplikacji. Zostały one opisane w paragrafie 3.5.5.
  • ustawi pole prywatne [ArrayList erreurs] zawierające listę ewentualnych błędów. Lista ta będzie pusta, jeśli nie ma błędów, ale mimo to będzie istnieć.
  • jeśli wystąpiły błędy, metoda [init] kończy działanie w tym miejscu. W przeciwnym razie tworzy obiekt typu [IArticlesDomain], który będzie obiektem biznesowym wykorzystywanym przez kontroler do swoich potrzeb. Jak wyjaśniono w punkcie 3.5.6, kontroler zażąda od frameworka Spring potrzebnego mu beana. Ta operacja instancjonowania może spowodować różne błędy. Jeśli tak się stanie, zostaną one ponownie zapisane w polu [erreurs] kontrolera.

3.5.10.2. Metody doGet, doPost

Te dwie metody obsługują żądania klientów o numerach HTTP, GET i POST. Zostaną one przetworzone bez rozróżnienia. Metoda [doPost] może zatem przekierować do metody [doGet] lub odwrotnie. Żądanie klienta zostanie przetworzone w następujący sposób:

  • sprawdzane będzie pole [erreurs]. Jeśli nie jest puste, oznacza to, że podczas inicjalizacji aplikacji wystąpiły błędy i aplikacja nie może działać. W odpowiedzi zostanie wówczas wysłany widok [ERREURS].
  • parametr [action] z żądania zostanie pobrany i sprawdzony. Jeśli nie odpowiada on żadnej znanej akcji, wysyłany jest widok [ERREURS] wraz z odpowiednim komunikatem o błędzie.
  • Jeśli parametr [action] jest prawidłowy, żądanie klienta jest przekazywane do procedury specyficznej dla danej akcji w celu przetworzenia. Procedura przetwarzająca akcję [uneAction] będzie miała następującą sygnaturę:
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. Przetwarzanie różnych akcji

Metody przetwarzające różne możliwe akcje aplikacji są następujące:

metoda
żądanie
przetwarzanie
możliwe odpowiedzi
doListe
GET /main?action=liste
- wyświetlić listę artykułów
od klasy biznesowej
- wyświetlenie listy
[LISTE] lub [ERREURS]
doInfos
GET /main?action=infos&id=ID
- pobrać artykuł o identyfikatorze id=ID z
klasy biznesowej
- wyświetlić go
[INFOS] lub [ERREURS]
doAchat
POST /main?action=zakup&id=ID
- ilość zakupiona jest częścią przesłanych parametrów
- należy zwrócić się o artykuł o identyfikatorze id=ID do
klasy biznesowej
- dodaj go do koszyka w
sesji klienta
[LISTE] lub [INFOS]
lub [ERREURS]
doRetirerAchat
GET /main?action=retirerachat&id=ID
- usunąć produkt o identyfikatorze id=ID z
listy zakupów w koszyku
sesji klienta
[PANIER]
doPanier
GET /main?action=koszyk
- wyświetl koszyk z
sesji klienta
[PANIER] lub [PANIERVIDE]
doValidationPanier
GET /main?action=validationpanier
- zmniejszyć w bazie danych
stanów magazynowych wszystkich artykułów
znajdujących się w koszyku
sesji klienta
[LISTE] lub [ERREURS]

3.5.10.4. Kod

  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 {

     // pola prywatne
    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() {
         // pobieramy parametry inicjalizacyjne serwletu
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // zapisujemy błąd
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // czy są jakieś błędy?
        if (erreurs.size() != 0) {
            return;
        }
         // tworzy się obiekt IArticlesDomain umożliwiający dostęp do warstwy biznesowej
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // zapisuje się błąd
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // zapisuje się niektóre adresy URL aplikacji
        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);

         // zakończono
        return;
    }


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

         // sprawdzamy, jak przebiegła inicjalizacja serwletu
        if (erreurs.size() != 0) {
             // czy mamy adres URL strony błędów?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // wyświetlamy stronę błędów
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // koniec
            return;
        }
         // przetwarzamy akcję
        String action = request.getParameter("action");
        if (action == null) {
             // lista artykułów
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // lista artykułów
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // informacje o artykule
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // zakup produktu
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // wyświetlenie koszyka
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // usunięcie produktu z koszyka
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // potwierdzenie koszyka
            doValidationPanier(request, response);
            return;
        }
         // nieznana akcja
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // wyświetla się strona błędów
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // koniec
        return;
    }


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

         // kupujący potwierdził zawartość koszyka
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // koszyk został zatwierdzony
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // nieprawidłowy stan
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // pobierane są błędy
        ArrayList erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
            request.setAttribute(
                "actions",
                new Hashtable[] { hActionListe, hActionPanier });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // wyświetla się lista produktów
        request.setAttribute("message", "Votre panier a été validé");
        doListe(request, response);
         // koniec
        return;
    }


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

         // usuwamy produkt z koszyka
        try {
            Panier panier =
                (Panier) request.getSession().getAttribute("panier");
            String strIdAchat = request.getParameter("id");
            panier.enlever(Integer.parseInt(strIdAchat));
        } catch (NumberFormatException ignored) {
        } catch (NullPointerException ignored) {
        }
         // wyświetlanie koszyka
        doPanier(request, response);
    }


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

         // wyświetlanie koszyka
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // koszyk pusty?
        if (panier == null || panier.getAchats().size() == 0) {
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
                .forward(request, response);
             // koniec
            return;
        }
         // w koszyku znajduje się jakiś produkt
        request.setAttribute("panier", panier);
        request.setAttribute(
            "actions",
            new Hashtable[] { hActionListe, hActionValidationPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_PANIER))
            .forward(request, response);
         // koniec
        return;
    }


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

         // zakup produktu
         // pobieranie ilości
        int qté = 0;
        try {
            qté = Integer.parseInt(request.getParameter("qte"));
            if (qté <= 0)
                throw new NumberFormatException();
        } catch (NumberFormatException ex) {
             // błędna ilość
            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);
             // koniec
            return;
        }
         // pobieranie sesji klienta
        HttpSession session = request.getSession();
         // tworzenie transakcji zakupu
        Article article = (Article) session.getAttribute("article");
        Achat achat = new Achat(article, qté);
         // dodajemy zakup do koszyka klienta
        Panier panier = (Panier) session.getAttribute("panier");
        if (panier == null) {
            panier = new Panier();
            session.setAttribute("panier", panier);
        }
        panier.ajouter(achat);
         // powrót do listy produktów
        String url = config.getInitParameter(URL_MAIN) + "?action=liste";
        getServletContext().getRequestDispatcher(url).forward(
            request,
            response);
         // koniec
        return;
    }


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

         // wyświetla się lista produktów
        request.setAttribute("infos", infos);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_DEBUG))
            .forward(request, response);
         // koniec
        return;
    }


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

         // tak samo jak get
        doGet(request, response);
    }


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

         // lista błędów
        ArrayList erreurs = new ArrayList();
         // pobieramy żądany identyfikator
        String strId = request.getParameter("id");
         // coś tam?
        if (strId == null) {
             // nieprawidłowe
            erreurs.add("action incorrecte([infos,id=null]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // przekształcamy strId na liczbę całkowitą
        int id = 0;
        try {
            id = Integer.parseInt(strId);
        } catch (Exception ex) {
             // nieprawidłowe
            erreurs.add("action incorrecte([infos,id=" + strId + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // żądany jest identyfikator klucza
        Article article = null;
        try {
            article=articlesDomain.getArticleById(id);
        } catch (UncheckedAccessArticlesException ex) {
             // nieprawidłowe
            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) {
             // nieprawidłowe
            erreurs.add("Article de clé [" + id + "] inexistant");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // umieszczamy pozycję w sesji
        request.getSession().setAttribute("article", article);
         // wyświetla się strona z informacjami
        request.setAttribute("actions", new Hashtable[] { hActionListe });
         /   / request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_INFOS))
            .forward(request, response);
         // koniec
        return;
    }

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

         // wyświetla się strona błędów
        request.setAttribute("erreurs", erreurs);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
            .forward(request, response);
         // koniec
        return;
    }


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

         // lista błędów
        ArrayList erreurs = new ArrayList();
         // żądana jest lista artykułów
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // zapisywanie błędu
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // czy są jakieś błędy?
        if (erreurs.size() != 0) {
             // wyświetla się strona błędów
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // koniec
            return;
        }
         // wyświetla się lista artykułów
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // koniec
        return;
    }

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

Pozwalamy czytelnikowi poświęcić trochę czasu na przeczytanie i zrozumienie tego kodu. Mamy nadzieję, że komentarze mu w tym pomogą.

3.5.10.5. Testy aplikacji

Pokażmy kilka zrzutów ekranu z testów. Na początek strona główna aplikacji:

Image

W rzeczywistości żądany adres URL brzmiał [http://localhost:8080/webarticles]. Czytelnik zauważy, że w pliku [web.xml] definiujemy stronę główną aplikacji:

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

Widok [index.jsp] jest zdefiniowany w następujący sposób:

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

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

Nastąpiło zatem przekierowanie na adres URL [http://localhost:8080/webarticles/main?action=liste], co widać na adresie URL przeglądarki na zrzucie ekranu. W związku z tym wysłano żądanie do adresu URL [/main?action=liste]. Wciąż w ramach [web.xml] adres URL /main jest powiązany z serwletem [webarticles]:

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

Również w przypadku [web.xml] serwlet [webarticles] jest powiązany z serwletem [ istia.st.articles.web.WebArticles]:

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

Serwlet [ istia.st.articles.web.WebArticles] jest zatem ładowany przez kontener serwletów Tomcat, jeśli nie został jeszcze załadowany, a jego metoda [init] jest wykonywana:

    public void init() {
         // pobierane są parametry inicjalizacyjne serwletu
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // zapisywanie błędu
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // czy są jakieś błędy?
        if (erreurs.size() != 0) {
            return;
        }
         // tworzymy obiekt IArticlesDomain umożliwiający dostęp do warstwy biznesowej
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // zapisujemy błąd
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // zapisuje się niektóre adresy URL aplikacji
        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);

         // zakończono
        return;
    }

Uwagi: metoda [init]

  • sprawdza obecność określonych parametrów konfiguracyjnych
  • instancjonuje usługę dostępu do domeny aplikacji za pomocą Springa
  • ustawia listę błędów w celu wskazania ewentualnych błędów inicjalizacji
  • kilka pól prywatnych:
    • błędy: lista błędów wykrytych przez [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: słowniki. Każdy z nich zawiera informacje niezbędne do wyświetlenia opcji w menu głównym wyświetlanym przez widok [entete.jsp]
    • acticlesDomain: usługa dostępu do modelu aplikacji

Metoda [init] jest wykonywana tylko raz, podczas początkowego ładowania serwletu. Następnie, w zależności od typu [GET, POST] żądania klienta, wykonywana jest jedna z metod [doGet, doPost]. W tym przypadku obie metody wykonują tę samą czynność, a kod umieszczono w metodzie [doGet]:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
         // sprawdzamy, jak przebiegła inicjalizacja serwletu
        if (erreurs.size() != 0) {
             // czy mamy adres URL strony błędów?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // wyświetlamy stronę błędów
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // koniec
            return;
        }
         // przetwarzamy akcję
        String action = request.getParameter("action");
        if (action == null) {
             // lista artykułów
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // lista artykułów
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // informacje o artykule
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // zakup produktu
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // wyświetlenie koszyka
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // usunięcie produktu z koszyka
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // potwierdzenie koszyka
            doValidationPanier(request, response);
            return;
        }
         // nieznana operacja
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // wyświetlana jest strona błędów
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // koniec
        return;
    }
  • Metoda [doGet] rozpoczyna się od sprawdzenia, czy po zakończeniu działania metody [init] wystąpiły błędy inicjalizacji. Jeśli tak, wyświetla widok [ERREURS] i na tym kończy się jej działanie.
  • W przeciwnym razie pobiera parametr [action] z żądania klienta. Należy pamiętać, że aplikacja została zaprojektowana tak, aby odpowiadać na żądania, w których obowiązkowo występuje parametr [action].
  • Aplikacja uruchamia metodę powiązaną z akcją. W tym przypadku będzie to metoda [doListe].

Metoda [doListe] wygląda następująco:

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

         // lista błędów
        ArrayList erreurs = new ArrayList();
         // żądana jest lista artykułów
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // zapisywanie błędu
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // czy są jakieś błędy?
        if (erreurs.size() != 0) {
             // wyświetla się strona błędów
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // koniec
            return;
        }
         // wyświetla się lista artykułów
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // koniec
        return;
    }
  • Przypomnijmy, że metoda [init] zapisała usługę dostępu do modelu aplikacji (warstwa domenowa) w prywatnym polu serwletu:
     // pola prywatne
    private IArticlesDomain articlesDomain = null;
  • Korzystając z tej usługi dostępu, można wygenerować listę artykułów:
         // lista błędów
        ArrayList erreurs = new ArrayList();
         // żądanie listy artykułów
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // zapisywanie błędu
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
  • w przypadku wystąpienia błędów wysyłany jest widok [ERREURS]:
         // czy są jakieś błędy?
        if (erreurs.size() != 0) {
             // wyświetla się strona błędów
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // koniec
            return;
        }
  • w przeciwnym razie wysyłany jest widok [LISTE]:
1
2
3
4
5
6
7
         // wyświetla się lista artykułów
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);

W naszym przykładzie wszystko przebiegło pomyślnie i otrzymaliśmy widok [LISTE]. Zachęcamy czytelnika do ponownego zapoznania się z kodem widoku [LISTE], aby przekonać się, że parametry dynamiczne oczekiwane przez ten widok zostały rzeczywiście dostarczone powyżej przez kontroler. Tego samego rodzaju weryfikację należy przeprowadzić dla każdego widoku:

  • znalezienie parametrów dynamicznych widoku
  • upewnić się, że kontroler rzeczywiście umieszcza je w atrybutach żądania przesyłanego do widoku

Teraz przedstawimy po prostu ścieżkę ekranów, z którymi styka się użytkownik aplikacji. Zachęcamy czytelnika, aby za każdym razem przeprowadził rozumowanie analogiczne do poprzedniego:

Z listy artykułów użytkownik może wybrać artykuł:

Kupujący może tutaj kupić artykuł nr 3. Wprowadźmy błąd przy wpisywaniu ilości:

Błąd został zgłoszony. Teraz kupmy kilka produktów:

Zakup został zarejestrowany, a lista produktów ponownie wyświetlona. Sprawdźmy koszyk:

Zakup znajduje się w koszyku. Usuńmy go:

Produkt został usunięty z koszyka, a koszyk ponownie wyświetlony. Teraz jest pusty.

Kupmy 100 sztuk produktu nr 3 i 2 sztuki produktu nr 4:

Zakup produktu nr 3 okazał się niemożliwy, ponieważ chciano kupić 100 sztuk, a w magazynie było ich tylko 30. Ten produkt pozostał w koszyku:

Produkt nr 4 został natomiast zakupiony, o czym świadczy jego nowa ilość w magazynie wynosząca 39 (40-1):

3.6. Architektura MVC z wykorzystaniem Struts

3.6.1. Ogólna architektura aplikacji

Wróćmy do architektury aplikacji MVC:

W poprzedniej wersji:

  • kontrolę sprawował serwlet
  • widoki były realizowane przez strony JSP
  • model był realizowany przez zestaw trzech plików .jar

W wersji Struts:

  • kontroler będzie realizowany przez serwlet wywodzący się z generycznego serwletu [ActionServlet] z biblioteki Struts
  • widoki będą obsługiwane przez te same strony JSP, co poprzednio, z niewielkimi zmianami
  • model będzie realizowany przez te same trzy archiwa

Przekonamy się, że przejście poprzedniej aplikacji na Struts obejmuje następujące zadania:

  • akcje, które były wcześniej obsługiwane w konkretnych metodach serwletu/kontrolera, są teraz obsługiwane przez instancje klas wywodzących się z klasy [Action] biblioteki Struts
  • należy utworzyć pliki konfiguracyjne [web.xml] i [struts-config.xml]
  • wprowadź kilka zmian w stronach JSP

Przypomnijmy ogólną architekturę MVC wykorzystywaną przez STRUTS:

M=modèle
klasy biznesowe, klasy dostępu do danych oraz baza danych
V=vues
strony JSP
C=contrôleur
serwlet przetwarzający żądania klientów, obiekty [Action] oraz fasole [ActionForm] powiązane z formularzami.
  • Kontroler stanowi serce aplikacji. Wszystkie żądania klienta przechodzą przez niego. Jest to serwlet generyczny dostarczany przez STRUTS. W niektórych przypadkach może zaistnieć potrzeba jego rozszerzenia. W prostych przypadkach nie jest to konieczne. Ten serwlet generyczny pobiera potrzebne informacje z pliku, najczęściej nazwanego struts-config.xml.
  • Jeśli żądanie klienta zawiera parametry formularza, kontroler umieszcza je w obiekcie typu Bean. Obiekty typu Bean tworzone w ten sposób są przechowywane w sesji lub w żądaniu klienta. To ustawienie można konfigurować. Nie trzeba ich ponownie tworzyć, jeśli zostały już utworzone.
  • W pliku konfiguracyjnym struts-config.xml do każdego pliku URL, który ma być przetwarzany programowo (a więc nie odpowiadającego widokowi JSP, który można by wywołać bezpośrednio), przypisuje się pewne informacje:
    • nazwę klasy typu Action odpowiedzialnej za przetworzenie żądania. Również w tym przypadku instancja obiektu Action może być przechowywana w sesji lub w żądaniu.
    • jeśli żądany URL jest skonfigurowany (w przypadku wysłania formularza do kontrolera), podawana jest nazwa bean odpowiedzialnego za przechowywanie informacji z formularza.
  • Dysponując tymi informacjami dostarczonymi przez plik konfiguracyjny, po otrzymaniu żądania URL od klienta kontroler jest w stanie ustalić, czy należy utworzyć bean i który. Po utworzeniu instancji bean może sprawdzić, czy dane, które zapisał i które pochodzą z formularza, są prawidłowe, czy nie. Metoda bean o nazwie „validate” jest wywoływana automatycznie przez kontroler. Bean jest tworzony przez programistę. Umieszcza on zatem w metodzie „validate” kod sprawdzający poprawność danych z formularza. Jeśli dane okażą się nieprawidłowe, kontroler nie będzie kontynuował działania. Przekaże kontrolę do widoku, którego nazwę znajdzie w swoim pliku konfiguracyjnym. Wymiana danych zostaje wówczas zakończona. Należy zauważyć, że programista może zlecić, aby poprawność formularza nie była sprawdzana. Robi to również w pliku struts-config.xml. W takim przypadku kontroler nie wywołuje metody `validate` fasoli.
  • Jeśli dane fasoli są poprawne, nie ma weryfikacji lub nie ma fasoli, kontroler przekazuje kontrolę obiektowi typu Action powiązanemu z plikiem URL. Odbywa się to poprzez wywołanie metody `execute` tego obiektu, do której przekazuje odwołanie do fasoli, którą ewentualnie utworzył. To właśnie w tym momencie programista wykonuje swoje zadania: może być konieczne wywołanie klas biznesowych lub klas dostępu do danych. Po zakończeniu przetwarzania obiekt `Action` zwraca kontrolerowi nazwę widoku, który ma zostać wysłany w odpowiedzi do klienta.
  • W swoim pliku konfiguracyjnym kontroler znajdzie obiekt URL powiązany z nazwą widoku, który ma wyświetlić. Następnie wysyła ten widok. Wymiana danych z klientem została zakończona.

W naszej aplikacji nie będziemy używać obiektów typu [Bean] jako obiektów buforujących pomiędzy klientem a klasami typu [Action]. Obiekt [Action] będzie bezpośrednio pobierał parametry żądania klienta z obiektu [HttpServletRequest], który otrzyma. Ułatwia to przeniesienie naszej pierwotnej aplikacji. Ostateczna architektura naszej aplikacji będzie zatem następująca:

M=modèle
klasy biznesowe, klasy dostępu do danych oraz baza danych
V=vues
strony JSP
C=contrôleur
serwlet przetwarzający żądania klientów, obiekty [Action]

3.6.2. Model

Został przedstawiony wcześniej. Składa się z archiwów Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.6.3. Konfiguracja aplikacji

3.6.3.1. Ogólna architektura

Ogólna architektura projektu Eclipse przedstawia się następująco:

Image

3.6.3.2. Konfiguracja dostępu do danych

Ponieważ interfejs dostępu do danych pozostaje niezmieniony, powiązane pliki konfiguracyjne są takie same jak w poprzedniej wersji. Zostały one zdefiniowane w pliku [WEB-INF/src]:

Image

Na powyższym zrzucie ekranu pliki [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] pochodzą z poprzedniej wersji.

3.6.3.3. Katalog archiwów

W pliku [WEB-INF/lib] znajdują się te same archiwa, co w poprzedniej wersji, oraz archiwum wymagane przez Struts:

Image

3.6.3.4. Konfiguracja aplikacji

Aplikację konfiguruje się za pomocą dwóch plików: [web.xml, struts-config.xml] w folderze [WEB-INF]:

Image

Plik [web.xml] ma następującą treść:

<?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>

Co zawiera ten plik?

  • Strona główna aplikacji to [vues/index.jsp] (welcome-file)
  • Żądania adresów URL w postaci *.do będą przekierowywane do serwletu [strutswebarticles] (servlet-mapping)
  • serwlet [strutswebarticles] jest instancją klasy [ istia.st.articles.web.struts.MainServlet] (nazwa serwletu, klasa serwletu)
  • ten serwlet przyjmuje dwa parametry inicjalizacyjne
    • nazwa pliku konfiguracyjnego Struts (config)
    • nazwa pliku konfiguracyjnego Spring (springConfigFileName)

Plik [struts-config.xml] ma następującą treść:

<?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>

Co zawiera ten plik konfiguracyjny?

  • że nasz kontroler będzie obsługiwał następujące adresy URL:
main.do
w celu wyświetlenia listy artykułów
liste.do
w celu wyświetlenia listy artykułów
infos.do
aby wyświetlić informacje o konkretnym artykule
achat.do
aby kupić dany produkt
panier.do
aby wyświetlić koszyk
retirerachat.do
aby usunąć produkt z koszyka
validerpanier.do
w celu zatwierdzenia koszyka
  • Powyższe akcje odpowiadają jedna po drugiej akcjom obsługiwanym przez serwlet w poprzedniej wersji. Dla każdej z nich podano następujące informacje:
  • nazwa klasy odpowiedzialnej za obsługę tej akcji
  • możliwe odpowiedzi (= widoki) po przetworzeniu akcji. Kontroler wybierze tylko jedną z nich.
  • nazwa pliku komunikatów dla aplikacji (message-resources). W tym przypadku plik ten będzie istniał, ale będzie pusty. Nie zostanie wykorzystany. Należy go umieścić w katalogu [ClassPath] aplikacji. Tutaj zostanie umieszczony w katalogu [WEB-INF/classes]. W środowisku Eclipse uzyskuje się ten wynik, umieszczając go w katalogu [WEB-INF/src]:

Image

3.6.4. Widoki JSP

Wykorzystywane widoki JSP to również te z poprzedniej wersji. Zmiany są bardzo niewielkie: chodzi o adresy URL w postaci [?action=XX?id=YY& ...], które zmieniają się na [/XX.do?id=YY&....]. Powtarzamy tutaj wyjaśnienia podane wcześniej, aby użytkownik nie musiał wracać do poprzedniej części. Należy pamiętać, że informacje przekazywane do widoku przez kontroler są dokładnie takie same w obu wersjach. W tym zakresie nic się nie zmieniło.

3.6.4.1. entete.jsp

Aby zapewnić pewną spójność różnych widoków, będą one miały ten sam nagłówek, zawierający nazwę aplikacji wraz z menu:

Menu jest dynamiczne i ustalane przez kontroler. Kontroler umieszcza w żądaniu przesyłanym do strony JSP atrybut klucza „actions”, którego wartością jest tablica typu Hastable[]. Każdy element tej tablicy jest słownikiem służącym do wygenerowania opcji w menu nagłówka. Każdy słownik ma dwa klucze:

  • href: adres URL powiązany z opcją menu
  • lien: tekst menu

Pozostałe widoki aplikacji będą korzystać z nagłówka zdefiniowanego przez [entete.jsp] za pomocą następującego tagu JSP:

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

Podczas wykonywania ten tag spowoduje włączenie do kodu strony JSP, która go zawiera, kodu strony [entete.jsp]. Ponieważ adres URL strony jest adresem względnym (brak znaku /), strona [entete.jsp] będzie wyszukiwana w tym samym katalogu, co strona zawierająca tag <jsp:include>.

Kod:

<%@ 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>

Komentarze: brak zmian w stosunku do poprzedniej wersji

3.6.4.2. liste.jsp

Ten widok wyświetla listę artykułów dostępnych w sprzedaży:

Lista ta jest wyświetlana w wyniku zapytania /main.do lub /validerpanier.do. Elementy zapytania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu
listarticles
ArrayList obiektów typu [Article]
message
obiekt typu String – komunikat do wyświetlenia na dole strony

Każdy link [Infos] z tablicy HTML artykułów ma adres URL w postaci [/infos.do?id=ID], gdzie ID to pole id wyświetlanego artykułu.

Kod:

<%@ 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>

Uwagi: jedna zmiana (wyróżniona powyżej)

3.6.4.3. infos.jsp

Ten widok wyświetla informacje o produkcie i umożliwia jego zakup:

Image

Jest wyświetlany po wysłaniu zapytania /infos.do?id=ID lub /achat.do?id=ID, gdy ilość zakupiona jest nieprawidłowa. Elementy zapytania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu
article
obiekt typu [Article] – artykuł do wyświetlenia
msg
obiekt typu String – komunikat do wyświetlenia w przypadku błędu dotyczącego ilości
qte
obiekt String – wartość do wyświetlenia w polu wprowadzania danych [Qte]

Pola [msg] i [qte] są używane w przypadku błędu wprowadzenia ilości:

Image

Ta strona zawiera formularz, który jest wysyłany po naciśnięciu przycisku [Acheter]. Adres docelowy POST to [/achat.do?id=ID], gdzie ID to identyfikator zakupionego artykułu.

Kod:

<%@ 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>

Uwagi: jedna zmiana (zaznaczona powyżej)

3.6.4.4. panier.jsp

Ten widok wyświetla zawartość koszyka:

Image

Jest ona wyświetlana w wyniku żądania /panier.do lub /retirerachat.do?id=ID. Elementy żądania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu
panier
obiekt typu [Panier] – koszyk do wyświetlenia

Każdy link [Retirer] z tablicy HTML zawierającej zakupy w koszyku ma adres URL w postaci [retirerachat.do?id=ID], gdzie ID to pole [id]produktu, który chcemy usunąć z koszyka.

Kod:

<%@ 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>

Uwagi: zmiana (podkreślona powyżej)

3.6.4.5. paniervide.jsp

Ten widok wyświetla informację, że koszyk jest pusty:

Image

Informacja ta jest wyświetlana w wyniku zapytania /panier.do lub /retirerachat.do?id=ID. Elementy zapytania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu

Kod:

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>

Uwagi: brak zmian.

3.6.4.6. erreurs.jsp

Ten widok jest wyświetlany w przypadku wystąpienia błędów:

Image

Jest on wyświetlany po każdym zapytaniu, które zakończyło się błędem, z wyjątkiem akcji zakupu z błędną ilością, która jest obsługiwana przez widok [INFOS]. Elementy zapytania kontrolera są następujące:

actions
obiekt Hashtable[] – tablica opcji menu
erreurs
ArrayList obiektów typu String reprezentujących komunikaty o błędach do wyświetlenia

Kod:

<%@ 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>

Uwagi: brak zmian.

3.6.4.7. index.jsp

Ta strona jest zdefiniowana jako strona główna aplikacji w pliku [web.xml] aplikacji:

<?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>

Widok [index.jsp] ogranicza się do przekierowania klienta do punktu wejścia aplikacji:

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

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

Uwagi: jedna zmiana (zaznaczona powyżej)

3.6.5. Kontroler Struts

Struts posiada kontroler generyczny o nazwie [ActionServlet]. Wiadomo, że serwlet posiada metodę [init], która pozwala zainicjować aplikację podczas jej uruchamiania. Jeśli używamy generycznego kontrolera Struts o nazwie [ActionServlet], nie mamy dostępu do jego metody [init]. W tym przypadku mamy pewne zadania do wykonania podczas uruchamiania aplikacji, głównie utworzenie instancji obiektu dostępu do modelu. Potrzebujemy więc metody [init]. W związku z tym tworzymy klasę pochodną [Actionservlet] w następującej klasie [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 {
   // pola prywatne
  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);

     // metody pobierające i ustawiające
  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{

     // inicjalizacja klasy nadrzędnej
    super.init();
     // pobieranie parametrów inicjalizacji serwletu
    config = getServletConfig();
    String param = null;
    for (int i = 0; i < parameters.length; i++) {
      param = config.getInitParameter(parameters[i]);
      if (param == null) {
         // zapisywanie błędu
        erreurs.add("Paramètre [" + parameters[i]
            + "] absent dans le fichier [web.xml]");
      }
    }
     // czy są jakieś błędy?
    if (erreurs.size() != 0) {
      return;
    }
     // tworzymy obiekt IArticlesDomain umożliwiający dostęp do warstwy biznesowej
    try {
      articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
          new ClassPathResource((String) config
              .getInitParameter(SPRING_CONFIG_FILENAME))))
          .getBean("articlesDomain");
    } catch (Exception ex) {
       // zapisujemy błąd
      erreurs.add("Erreur de configuration de l'accès aux données : "
          + ex.toString());
      return;
    }
     // zapisuje się niektóre adresy URL aplikacji
    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);

     // zakończono
    return;
  }
}

Komentarze:

  • znaczenie tej klasy polega na jej metodzie [init] i jej polach prywatnych
  • metoda [init] działa tak samo jak metoda [init] z kontrolera poprzedniej wersji:
    • sprawdza obecność określonych parametrów konfiguracyjnych
    • tworzy instancję serwisu zapewniającego dostęp do domeny aplikacji za pomocą Spring
    • ustawia listę błędów, aby wskazać ewentualne błędy inicjalizacji
  • przed rozpoczęciem pracy metoda [init] wywołuje metodę [init] klasy nadrzędnej [ActionServlet]. To właśnie ona przetwarza plik konfiguracyjny Struts [struts-config.xml].
  • Zdefiniowano pewną liczbę pól prywatnych wraz z ich metodami dostępu:
    • błędy: lista błędów wykrytych przez metodę [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: słowniki. Każdy z nich zawiera informacje niezbędne do wyświetlenia opcji w menu głównym wyświetlanym przez widok [entete.jsp]
    • acticlesDomain: usługa dostępu do modelu aplikacji
  • Kontroler aplikacji Struts jest dostępny dla klas [Action] odpowiedzialnych za obsługę różnych możliwych akcji. Klasy te będą miały dostęp do wyżej wymienionych pól prywatnych, ponieważ są one wyposażone w publiczne metody dostępu.

Instancjonowanie tego kontrolera zapewnia plik [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>

Każdy obiekt klasy URL, którego nazwa kończy się na .do, zostanie przetworzony przez instancję klasy [istia.st.articles.web.struts.MainServlet]

3.6.6. Akcje aplikacji Struts

3.6.6.1. Wprowadzenie

Każda akcja Struts będzie reprezentowana przez klasę. W poprzedniej wersji każda akcja była reprezentowana przez metodę w kontrolerze aplikacji. Tworzenie klasy [Action] polega najczęściej na:

  • skopiowanie i wklejenie metody, która była używana w poprzedniej wersji
  • dostosowanie kodu do konwencji Struts

3.6.6.2. main.do, liste.do

Te dwie akcje są identyczne i zdefiniowane w pliku [struts-config.xml] poprzez:

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>

Gdy jedna z tych akcji [main.do, liste.do] zostanie wykonana w przeglądarce, otrzymujemy następujący wynik:

Image

Kod klasy [ListeArticlesAction] jest następujący:

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 {

     // serwlet kontrolny
    MainServlet mainServlet = (MainServlet) this.getServlet();

     // błędy inicjalizacji?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // wyświetla się strona błędów
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

     // obiekt dostępu do domeny
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();

     // lista błędów
    ArrayList erreurs = new ArrayList();
     // żądana jest lista artykułów
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // zapisywanie błędu
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
     // błędów?
    if (erreurs.size() != 0) {
       // wyświetla się strona błędów
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // wyświetla się lista artykułów
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");
  }
}

Uwagi:

  • napisanie kodu klasy [Action] polega zasadniczo na napisaniu kodu jej metody [execute]
  • W instancji kontrolera zapisano pewną liczbę informacji. Odwołanie do niej uzyskuje się za pomocą:
     // serwlet kontrolny
    MainServlet mainServlet = (MainServlet) this.getServlet();
  • pobieramy listę błędów inicjalizacji zapisanych przez kontroler. Jeśli lista ta nie jest pusta, widok [ERREURS] jest wysyłany do klienta:
1
2
3
4
5
6
7
8
     // błędy inicjalizacji?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // wyświetla stronę błędów
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

Widok, który zostanie faktycznie wysłany do klienta, jest dostarczany przez [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>

Jest to widok [/vues/erreurs.jsp]. Zachęcamy czytelnika do sprawdzenia, czego oczekuje ten widok. Informacje te są tutaj dostarczane przez akcję w obiekcie [request] jako atrybuty.

  • Również dzięki kontrolerowi akcja może pobrać obiekt dostępu do modelu aplikacji (warstwa domenowa):
     // obiekt dostępu do domeny
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • Po wykonaniu tej czynności można wygenerować listę artykułów:
     // lista błędów
    ArrayList erreurs = new ArrayList();
     // żądana jest lista artykułów
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // zapisywanie błędu
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • W przypadku wystąpienia błędów wysyłany jest widok [ERREURS]:
1
2
3
4
5
6
7
8
     // błędów?
    if (erreurs.size() != 0) {
       // wyświetla się strona z błędami
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • w przeciwnym razie wysyłany jest widok [LISTE]:
1
2
3
4
5
6
     // wyświetla się lista artykułów
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");

Widok, który zostanie faktycznie wysłany do klienta, jest dostarczany przez [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>

Jest to widok [/vues/liste.jsp]. Zachęcamy czytelnika do sprawdzenia, jakie dane są oczekiwane w tym widoku. Informacje te są tutaj dostarczane przez akcję w obiekcie [request] w postaci atrybutów.

3.6.6.3. infos.do

Ta akcja służy do dostarczenia informacji o jednym z elementów wyświetlanych w widoku [LISTE]:

Ta akcja jest skonfigurowana w następujący sposób w widoku [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>

Kod klasy [InfosArticleAction] jest następujący:

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 {

     // serwlet kontrolny
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // błędy inicjalizacji?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // wyświetla stronę błędów
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // obiekt dostępu do domeny
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // lista błędów
    ArrayList erreurs = new ArrayList();
     // pobierany jest żądany identyfikator
    String strId = request.getParameter("id");
     // coś?
    if (strId == null) {
       // nie jest to normalne
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // przekształcamy strId na liczbę całkowitą
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // nieprawidłowe
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // żądany jest identyfikator klucza
    Article article = null;
    try {
      article = articlesDomain.getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // nieprawidłowe
      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) {
       // nieprawidłowe
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // umieszczamy pozycję w sesji
    request.getSession().setAttribute("article", article);
     // wyświetla się strona informacyjna
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionListe() });
    return mapping.findForward("afficherInfosArticle");
  }
}

Uwagi:

  • początek metody [execute] jest identyczny jak w przypadku metody omówionej wcześniej. Tak samo będzie w przypadku pozostałych akcji.
  • Metoda pobiera parametr [id], który powinien znajdować się w adresie URL. Adres ten musi mieć postać [/infos.do?id=X]. Przeprowadzane są różne testy w celu sprawdzenia obecności i poprawności parametru [id]. W przypadku wystąpienia problemu wysyłany jest widok [ERREURS].
  • Jeśli parametr [id] jest prawidłowy, odpowiedni artykuł jest pobierany z warstwy [domain]. Jeśli warstwa ta zgłasza wyjątek lub artykuł nie zostanie znaleziony, ponownie wysyłany jest widok [ERREURS].
  • Jeśli wszystko przebiega pomyślnie, uzyskany artykuł jest umieszczany w sesji. Jest to kwestia dyskusyjna. W tym przypadku wychodzimy z założenia, że klient być może kupi ten artykuł. Jeśli tak się stanie, pobierzemy go z sesji, zamiast ponownie żądać go z warstwy [domain].
  • W końcu wyświetlany jest widok [INFOS]. Widok, który zostanie faktycznie wysłany do klienta, jest dostarczany przez [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>

Jest to widok [/vues/infos.jsp]. Zachęcamy czytelnika do sprawdzenia, czego oczekuje ten widok. Informacje te są tutaj dostarczane przez akcję w obiekcie [request] w postaci atrybutów.

3.6.6.4. achat.do

Ta akcja służy do zakupu artykułu wyświetlanego przez poprzedni widok [INFOS]:

Po zakupie produktu ponownie wyświetla się widok [LISTE] (widok po prawej stronie). Jeśli przyjrzymy się kodowi HTML z widoku po lewej stronie powyżej, zauważymy, że tag <form> jest zdefiniowany w następujący sposób:

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>

Widać zatem, że formularz jest wysyłany do kontrolera za pomocą akcji [achat.do].

Akcja ta jest skonfigurowana w następujący sposób w pliku [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>

Kod klasy [AchatArticleAction] brzmi następująco:

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 {

     // serwlet kontrolny
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // błędy inicjalizacji?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // wyświetla się strona błędów
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // lista błędów związanych z tą akcją
    ArrayList erreurs = new ArrayList();
     // pobieranie ilości zakupionej
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // błędna ilość
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
     // pobieranie sesji klienta
    HttpSession session = request.getSession();
     // pobieranie produktu dodanego do koszyka
    Article article = (Article) session.getAttribute("article");
     // czy sesja wygasła?
    if(article==null){
       // wyświetlanie strony błędów
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // tworzy się nowe zamówienie
    Achat achat = new Achat(article, qté);
     // dodajemy zakup do koszyka klienta
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // powrót do listy produktów
    return mapping.findForward("afficherListeArticles");
  }
}

Uwagi:

  • początek metody [execute] jest identyczny jak w przypadku metod omówionych wcześniej.
  • Przypomnijmy formę formularza wysyłanego do kontrolera:
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>
  • W żądaniu znajdują się dwa parametry: [id]: numer zakupionego artykułu, [qte]: zakupiona ilość.
  • Sprawdzana jest obecność i poprawność parametru [qte]. Jeśli parametr ten zostanie uznany za nieprawidłowy, do użytkownika zwracany jest widok [INFOS] wraz z komunikatem o błędzie:
     // lista błędów związanych z tą czynnością
    ArrayList erreurs = new ArrayList();
     // pobierana jest ilość zakupiona
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // błędna ilość
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
  • zakupiony artykuł jest pobierany z sesji. Sesja mogła wygasnąć. W takim przypadku wysyłany jest widok [ERREURS]:
     // pobieranie sesji klienta
    HttpSession session = request.getSession();
     // pobieranie produktu dodanego do koszyka
    Article article = (Article) session.getAttribute("article");
     // czy sesja wygasła?
    if(article==null){
       // wyświetlanie strony błędów
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
  • jeśli sesja nie wygasła, produkt jest umieszczany w koszyku, który również jest pobierany z sesji:
     // pobieranie sesji klienta
    HttpSession session = request.getSession();
     // pobieramy artykuł dodany do sesji
    Article article = (Article) session.getAttribute("article");
     // czy sesja wygasła?
    if(article==null){
       // wyświetla się strona błędów
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // tworzy się nowe zamówienie
    Achat achat = new Achat(article, qté);
     // dodajemy zakup do koszyka klienta
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • na koniec wysyłamy widok [LISTE]:
     // powrót do listy produktów
    return mapping.findForward("afficherListeArticles");
  • Widok, który zostanie faktycznie wysłany do klienta, jest dostarczany przez [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>

Chodzi o widok [/main.do]. Ten widok nie jest widokiem, lecz akcją. W związku z tym zostanie wykonana opisana wcześniej akcja [/main.do] i wyświetlona lista artykułów.

3.6.6.5. panier.do

Ta akcja służy do wyświetlenia wszystkich zakupów klienta. Jest ona dostępna poprzez opcję menu [Voir le panier]:

Kod HTML powiązany z linkiem [Voir le panier] jest następujący:

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

Akcja [panier.do] jest skonfigurowana w następujący sposób w [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>

Kod klasy [VoirPanierAction] brzmi następująco:

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 {

     // serwlet kontrolny
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // błędy inicjalizacji?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // wyświetla się strona błędów
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // wyświetlanie koszyka
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // koszyk pusty
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    } else {
       // w koszyku znajduje się coś
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(), mainServlet.getHActionValidationPanier() });
      return mapping.findForward("afficherPanier");
    }
  }
}

Uwagi:

  • początek metody [execute] jest identyczny jak w przypadku metod omówionych wcześniej.
  • koszyk jest pobierany z sesji, w której zwykle się znajduje. Sesja mogła wygasnąć, a wtedy nie ma koszyka. Nie traktujemy tego jako błędu, ale po prostu zakładamy, że koszyk jest pusty.
  • Jeśli koszyk jest pusty, wyświetlany jest widok [PANIERVIDE]
  • w przeciwnym razie wyświetlany jest widok [PANIER]

Widoki faktycznie wysyłane do klienta są definiowane przez akcję:

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

3.6.6.6. retirerachat.do

Ta akcja służy do usunięcia zakupu z koszyka:

Po wykonaniu akcji [retirerachat.do] koszyk jest ponownie wyświetlany (widok po prawej stronie powyżej). Jeśli przyjrzymy się kodowi HTML z linku [Valider le panier] z widoku po lewej stronie powyżej, otrzymamy następujący wynik:

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

Akcja [retirerachat.do] otrzymuje zatem jako parametr identyfikator produktu, który ma zostać usunięty z koszyka. Akcja ta jest skonfigurowana w następujący sposób w [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>

Kod klasy [RetirerAchatAction] brzmi następująco:

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 {

     // serwlet kontrolny
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // błędy inicjalizacji?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // wyświetla się strona błędów
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // lista błędów związanych z tą akcją
    ArrayList erreurs = new ArrayList();
     // pobieranie koszyka
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sesja wygasła
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    }
     // pobieranie identyfikatora produktu do usunięcia
    String strId = request.getParameter("id");
     // coś tam?
    if (strId == null) {
       // nieprawidłowe
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // przekształcamy strId na liczbę całkowitą
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // nieprawidłowe
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // usuwamy zakup
    panier.enlever(id);
     // ponownie wyświetla się koszyk
    return mapping.findForward("afficherPanier");
  }
}

Uwagi:

  • początek metody [execute] jest identyczny jak w przypadku metod omówionych wcześniej.
  • Poniższy kod ma na celu sprawdzenie obecności i poprawności parametru [id]. Jeśli okaże się on nieprawidłowy, wysyłany jest widok [ERREURS].
  • W przeciwnym razie zakup jest usuwany z koszyka:
     // usuwa się zakup
    panier.enlever(id);
  • następnie koszyk jest ponownie wyświetlany:
     // ponowne wyświetlenie koszyka
    return mapping.findForward("afficherPanier");

Widok faktycznie wysyłany do klienta jest definiowany przez akcję:

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

Widać, że w przypadku widoku uruchomiona zostanie akcja [/panier.do]. Została ona już opisana. Wyświetli ona widok [PANIER] lub [PANIERVIDE] w zależności od stanu koszyka.

3.6.6.7. validerpanier.do

Ta akcja służy do zatwierdzenia zakupów dokonanych przez klienta. W praktyce oznacza to jedną czynność: w bazie danych stan magazynowy zakupionych artykułów jest pomniejszany o zakupione ilości. Akcja ta pochodzi z następującego menu:

 

Kod HTML dla linku [Valider le panier] wygląda następująco:

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

Po aktywowaniu tego linku stany magazynowe są pomniejszane, a lista artykułów ponownie wyświetlana.

Działanie to jest skonfigurowane w następujący sposób w [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>

Kod klasy [ValiderPanierAction] jest następujący:

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 {

     // serwlet kontrolny
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // błędy inicjalizacji?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // wyświetlanie strony błędów
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // obiekt dostępu do domeny
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // lista błędów związanych z tą akcją
    ArrayList erreurs = new ArrayList();
         // kupujący potwierdził zawartość koszyka
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sesja wygasła
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
         // koszyk został zatwierdzony
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // nieprawidłowy stan
            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");
        }
         // pobierane są ewentualne błędy
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
         // wszystko wygląda na w porządku OK – wyświetla się lista produktów
    return mapping.findForward("afficherListeArticles");
  }
}

Uwagi:

  • początek metody [execute] jest identyczny jak w przypadku metod omówionych wcześniej.
  • Pobieramy koszyk z sesji. Jeśli sesja wygasła, wysyłamy widok [ERREURS]:
     // lista błędów związanych z tą czynnością
    ArrayList erreurs = new ArrayList();
         // kupujący potwierdził zawartość koszyka
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sesja wygasła
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • Potwierdza się zakupy z koszyka. Mogą wystąpić błędy, jeśli stan magazynowy jest niewystarczający do zrealizowania zamówienia. W takim przypadku wysyłany jest widok [ERREURS]:
         // zatwierdzamy koszyk
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // nieprawidłowy stan
            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");
        }
         // pobierane są ewentualne błędy
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
  • jeśli wszystko przebiegło pomyślnie, ponownie wyświetla się lista artykułów:
         // wszystko wygląda na w porządku OK – wyświetla się lista produktów
    return mapping.findForward("afficherListeArticles");

Widok faktycznie wysyłany do klienta jest definiowany przez akcję:

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

Widać, że w przypadku widoku uruchomiona zostanie akcja [/main.do]. Została ona już opisana. Spowoduje ona wyświetlenie widoku [LISTE].

3.7. Architektura MVC z wykorzystaniem Spring

3.7.1. Ogólna architektura aplikacji

Wróćmy do architektury aplikacji MVC:

W pierwszej wersji:

  • kontrolę sprawował serwlet
  • widoki były realizowane przez strony JSP
  • model był realizowany przez zestaw trzech plików .jar

W wersji Struts:

  • kontrolerem był serwlet wywodzący się z generycznego serwletu [ActionServlet] z biblioteki Struts
  • widoki były obsługiwane przez te same strony JSP, co w wersji [Struts]
  • model był obsługiwany przez te same trzy pliki

W wersji Spring:

  • kontroler będzie obsługiwany przez serwlet dostarczony przez Spring o nazwie [DispatcherServlet]
  • widoki będą obsługiwane przez te same strony JSP, co poprzednio, z niewielkimi zmianami
  • model będzie obsługiwany przez te same trzy archiwa

Przekonamy się, że przejście naszej aplikacji ze Struts na Spring jest proste, jeśli zgodzimy się nie korzystać ze wszystkich elementów zalecanych dla ortodoksyjnej architektury Spring MVC. Główne zmiany są następujące:

  • akcje, które w Struts były obsługiwane w konkretnych metodach serwletu/kontrolera lub przez instancje klas pochodnych klasy [Action], są teraz obsługiwane przez instancje klasy implementującej interfejs Spring [Controller]
  • niezbędne pliki konfiguracyjne to:
    • [web.xml], ponieważ mamy do czynienia z aplikacją internetową. Plik ten zawiera słuchacz (Listener), który w momencie inicjalizacji aplikacji wykorzysta plik [applicationContext.xml]
    • [applicationContext.xml], który umożliwi utworzenie beanów potrzebnych aplikacji, w szczególności tego związanego z usługą dostępu do modelu
  • Widoki JSP będą identyczne jak w Struts. Będziemy musieli utworzyć nowy.

Przypomnijmy architekturę MVC STRUTS zastosowaną w poprzedniej wersji:

M=modèle
klasy biznesowe, klasy dostępu do danych oraz baza danych
V=vues
strony JSP
C=contrôleur
serwlet przetwarzający żądania klientów, obiekty [Action]

W przypadku Springa stosujemy identyczną architekturę:

M=modèle
klasy biznesowe, klasy dostępu do danych oraz baza danych
V=vues
strony JSP
C=contrôleur
serwlet przetwarzający żądania klientów, obiekty implementujące interfejs [Controller]
  • kontroler stanowi serce aplikacji. Wszystkie żądania klienta przechodzą przez niego. Jest to serwlet generyczny dostarczany przez SPRING. Należy do typu [DispatcherServlet]. Od tej pory będziemy nazywać ten kontroler kontrolerem [Spring].
  • Kontroler [Spring] przekieruje żądanie klienta do jednej z instancji typu [Controller]. Dla każdej akcji do przetworzenia będzie istniała jedna taka instancja. Jest ona definiowana w żądanym adresie URL, podobnie jak w Struts. W ten sposób będziemy wiedzieć, że żądana akcja to akcja [liste], ponieważ żądany adres URL to [liste.do]
  • jeśli C jest kontekstem aplikacji, kontroler [Spring] korzysta z pliku [C-servlet.xml], który pełni rolę pliku konfiguracyjnego struts-config.xml w wersji Struts. Każdej akcji, która ma zostać przetworzona przez aplikację, przypisuje się nazwę klasy typu Controller odpowiedzialnej za obsługę żądania.
  • Kontroler przekazuje kontrolę obiektowi typu Controller powiązanemu z akcją. Czyni to, wywołując metodę handleRequest tego obiektu, do której przekazuje żądanie klienta. To właśnie w tym momencie programista wykonuje swoje zadania: w razie potrzeby będzie musiał skorzystać z klas biznesowych lub klas dostępu do danych. Po zakończeniu przetwarzania obiekt typu Controller zwraca kontrolerowi nazwę widoku, który ma zostać wysłany w odpowiedzi do klienta.
  • W swoim pliku konfiguracyjnym kontroler znajdzie klasę URL powiązaną z nazwą widoku, o wyświetlenie którego został poproszony. Następnie wysyła ten widok. Wymiana danych z klientem została zakończona.

3.7.2. Model

Jest taki sam jak w dwóch poprzednich wersjach. Składa się z archiwów Java o nazwie [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.7.3. Konfiguracja aplikacji

3.7.3.1. Ogólna architektura

Ogólna architektura projektu Eclipse wygląda następująco:

Image

3.7.3.2. Konfiguracja dostępu do danych

Ponieważ interfejs dostępu do danych pozostaje niezmieniony, powiązane pliki konfiguracyjne są takie same jak w poprzedniej wersji. Zostały one zdefiniowane w pliku [WEB-INF/src]:

Image

Na powyższym zrzucie ekranu pliki [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] pochodzą z poprzednich wersji.

3.7.3.3. Katalog archiwów

W pliku [WEB-INF/lib] znajdują się te same archiwa, co w poprzedniej wersji, z wyjątkiem archiwów Struts, które nie są już potrzebne:

Image

3.7.3.4. Konfiguracja aplikacji

Aplikację konfiguruje się za pomocą trzech plików: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] w folderze [WEB-INF]:

Image

Plik [web.xml] ma następującą treść:

<?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>
     <!-- moduł ładujący kontekst Spring aplikacji -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- serwlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- mapowanie adresów URL -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- dokument wejściowy -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

Co zawiera ten plik?

  • strona główna aplikacji to [/vues/index.jsp] (welcome-file)
  • żądania adresów URL w postaci *.do będą przekierowywane do serwletu [springwebarticles] (servlet-mapping)
  • serwlet [springwebarticles] jest instancją klasy [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class) dostarczonej przez Spring.
  • słuchacz [org.springframework.web.context.ContextLoaderListener] zostanie uruchomiony podczas startu aplikacji. Jego głównym zadaniem będzie utworzenie instancji beanów Spring zdefiniowanych w pliku [applicationContext.xml]

Plik [applicationContext.xml] ma następującą treść:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- klasa dostępu do danych -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- klasa biznesowa -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- konfiguracja aplikacji internetowej-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Znajdziemy tu zarówno elementy znane, jak i mniej znane. Podczas inicjalizacji aplikacji zostaną utworzone instancje trzech beanów:

  • articlesDao: usługa dostępu do warstwy [dao]
  • articlesDomain: usługa dostępu do modelu
  • config: bean, w którym zgromadzimy informacje, które muszą być wspólne dla wszystkich klientów. Bean ten będzie pełnił rolę, jaką tradycyjnie pełni kontekst aplikacji, ale z informacjami typowanymi zamiast nietypowanymi.

Ostatni plik [springwebarticles.xml] definiuje akcje akceptowane przez aplikację w sposób bardzo zbliżony do tego, jaki stosuje plik Struts [struts-config.xml]. Jego zawartość jest następująca:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- menedżery akcji = kontrolery -->
    <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>

     <!-- mapowanie aplikacji-->
    <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>

     <!-- menedżer widoków -->
    <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>

     <!-- plik komunikatów -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>

Co mówi ten plik konfiguracyjny?

  • że nasz kontroler będzie obsługiwał następujące adresy URL:
main.do
w celu wyświetlenia listy artykułów
liste.do
w celu wyświetlenia listy artykułów
infos.do
aby wyświetlić informacje o konkretnym artykule
achat.do
aby kupić dany produkt
panier.do
aby wyświetlić koszyk
retirerachat.do
aby usunąć produkt z koszyka
validerpanier.do
w celu zatwierdzenia koszyka
  • powyższe akcje odpowiadają jedna po drugiej akcjom przetwarzanym przez kontroler w poprzednich wersjach. Dla każdej z nich podano nazwę klasy odpowiedzialnej za jej przetworzenie. Weźmy na przykład akcję [/panier.do]:
  • musi ona zostać przetworzona przez bean [VoirPanierController]. Nazwa ta jest dowolna. Jest to zwykły klucz.
                <prop key="/panier.do">VoirPanierController</prop>
  • klucz [VoirPanierController] to nazwa komponentu zdefiniowanego w tym samym pliku konfiguracyjnym:
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
  • Bean [VoirPanierController] definiuje:
  • klasę do instancjonowania [ istia.st.articles.web.spring.VoirPanierController] w celu przetworzenia akcji
  • sposób jej instancjonowania. W tym przypadku jako parametr przekazywany jest bean [config], zdefiniowany przez [applicationContext.xml] i instancjonowany podczas uruchomienia aplikacji. Takie postępowanie będzie miało zastosowanie do wszystkich akcji [Controller]. W ten sposób każda z nich będzie dysponować w polu prywatnym obiektem [config], w którym znajdzie wszystkie informacje wspólne dla wszystkich klientów.
  • Sposób, w jaki należy rozpoznawać nazwy widoków:
     <!-- menedżer widoków -->
    <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>

Podobnie jak w przypadku Struts, instancja [Controller], która przetwarza akcję, po zakończeniu przetwarzania zwróci klucz do kontrolera Spring, aby wskazać mu, który widok ma wyświetlić. Na podstawie tego klucza mogą istnieć różne strategie generowania widoku powiązanego z kluczem. Wybraną strategią jest ta zdefiniowana przez bean [viewResolver]. W tym przypadku bean ten jest powiązany z klasą [org.springframework.web.servlet.view.InternalResourceViewResolver] z różnymi parametrami inicjalizacji. Nie wchodząc w szczegóły, bean [viewResolver] określa tutaj, że jeśli klucz widoku to „XX”, wówczas wygenerowanym widokiem będzie [/vues/XX.jsp]. Rodzaj widoków wysyłanych do klienta można zmieniać na różne sposoby:

  • zmieniając klasę implementacyjną dla komponentu [viewResolver]
  • zmieniając parametry inicjalizacji klasy implementacyjnej

W ten sposób można przejść z widoku HTML do widoku XML, po prostu zmieniając wartość komponentu [viewResolver]

  • nazwa pliku komunikatów dla aplikacji (messageSource). W tym przypadku plik będzie istniał, ale będzie pusty. Nie zostanie wykorzystany. Należy go umieścić w katalogu [ClassPath] aplikacji. Tutaj zostanie umieszczony w katalogu [WEB-INF/classes]. W środowisku Eclipse uzyskuje się ten wynik, umieszczając go w katalogu [WEB-INF/src]:

Image

3.7.4. Widoki JSP

Wykorzystywane widoki JSP będą tymi samymi, których używa Struts. Żaden z nich nie został zmodyfikowany:

Image

Tworzony jest tylko jeden nowy widok: redirpanier.jsp. Służy on wyłącznie do przekierowania klienta do akcji [/panier.do]. Jego kod wygląda następująco:

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

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

Zachęcamy czytelnika do ponownego zapoznania się z definicjami poszczególnych widoków w wersji Struts.

3.7.5. Obsługa akcji

Klasy niezbędne do przetwarzania różnych akcji zostały zebrane w pakiecie [istia.st.articles.web.spring]:

Image

Przypomnijmy mechanizm działania aplikacji Spring na przykładzie:

  • użytkownik wysyła żądanie na adres URL [http://localhost:8080/springwebarticles/main.do]

Image

Co się stało?

  • Wywołano plik [web.xml] z aplikacji [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>
     <!-- moduł ładujący kontekst Spring aplikacji -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- serwlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- mapowanie adresów URL -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- dokument wejściowy -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  • jeśli było to pierwsze żądanie skierowane do aplikacji, uruchomiło się kilka procesów:
    • załadowano moduł nasłuchujący [ org.springframework.web.context.ContextLoaderListener]
    • wykorzystał on plik konfiguracyjny [applicationContext.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- klasa dostępu do danych -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- klasa biznesowa -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- konfiguracja aplikacji internetowej-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>
  • powyższe komponenty typu bean zostały utworzone w kontekście aplikacji
  • następnie wykorzystano plik [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>
     <!-- menedżery akcji = kontrolery -->
    <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>

     <!-- mapowanie aplikacji-->
    <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>

     <!-- menedżer widoków -->
    <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>

     <!-- plik komunikatów -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>
  • Beany [Controller] zdefiniowane przez ten plik również zostały utworzone
  • wszystko jest teraz gotowe do przetworzenia żądania klienta. Brzmiało ono: [http://localhost:8080/springwebarticles]. W tym przypadku nie żąda się obiektu URL z kontekstu, lecz sam kontekst. Wykorzystywana jest zatem sekcja [welcome-file] z pliku [web.xml].
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
  • Widok [index.jsp] wygląda następująco:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>
  • klient jest zatem proszony o przekierowanie się na adres URL [http://localhost:8080/springwebarticles/main.do]. Robi to.
  • Kontroler Spring otrzymuje zatem nowe żądanie. Wykorzystuje on swój plik [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>
     <!-- menedżery akcji = kontrolery -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

     <!-- mapowanie aplikacji-->
    <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>
  • plik ten informuje go, że akcja [/main.do] powinna zostać przetworzona przez bean [ListController].
  • Żądanie klienta jest przekazywane do metody [handleRequest] w bean'ie [ListController]. Metoda ta wykonuje swoje zadanie i zwraca kontrolerowi klucz widoku, który ma zostać wyświetlony. W tym przypadku, jeśli wszystko przebiegnie pomyślnie, kluczem tym będzie [liste].
  • Kontroler Spring wykorzystuje bean [viewResolver] z pliku konfiguracyjnego [springwebarticles-servlet.xml] w celu określenia widoku powiązanego z tym kluczem. W tym przypadku będzie to widok [/vues/liste.jsp]
  • widok [/vues/liste.jsp] jest wysyłany do klienta

3.7.6. Inicjalizacja aplikacji Spring

Wspomnieliśmy, że podczas uruchamiania aplikacji instancjonowane są bean’y z pliku [applicationContext.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- klasa dostępu do danych -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- klasa biznesowa -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- konfiguracja aplikacji internetowej-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Znamy komponenty typu bean o nazwach [articlesDao, articlesDomain], ale nie znamy komponentu typu bean o nazwie [config]. Jest on zdefiniowany przez następującą klasę Java:

package istia.st.articles.web.spring;

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

/**
 * @author ST - ISTIA
 */

public class Config {

     // pola prywatne
    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);

     // metody pobierające i ustawiające
    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;
    }

     // inicjalizacja aplikacji internetowej
    public void init() {
         // zapisujemy niektóre adresy URL aplikacji
        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);

         // to już koniec
        return;
    }
}

Klasa ta wykonuje to samo, co metoda [init] w serwlecie aplikacji internetowej. Inicjuje ona aplikację. W tym przypadku przebiega to w następujący sposób:

  • ponieważ bean [config] jest zdefiniowany w następujący sposób w [applicationContext.xml]:
     <!-- konfiguracja aplikacji internetowej-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>

w momencie utworzenia jego pole prywatne [articlesDomain] zostało zainicjowane

  • Następnie, ze względu na atrybut [init-method="init"] powyższego bean’a, wykonywana jest metoda [init] klasy powiązanej z tym bean’em. W tym miejscu inicjuje ona trzy słowniki [hActionListe, hActionPanier, hActionValidationPanier] wykorzystywane do wygenerowania trzech możliwych linków w menu oferowanym użytkownikowi.
  • Tworzone są publiczne metody dostępu, aby udostępnić te pola prywatne instancjom typu [Controller], które będą przetwarzać akcje.

3.7.7. Akcje [Controller] aplikacji Spring

3.7.7.1. Wprowadzenie

Każda akcja Springa będzie odpowiadać klasie typu [Controller]. W wersji Struts każda akcja odpowiadała klasie typu [Action]. Tworzenie klasy [Controller] polega najczęściej na:

  • skopiowanie i wklejenie klasy [Action], która była używana w wersji Struts,
  • dostosowanie kodu do konwencji Spring

3.7.7.2. main.do, liste.do

Te dwie operacje są identyczne i zdefiniowane w klasie [springwebarticles-servlet.xml] poprzez:

     <!-- mapowanie aplikacji-->
    <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>

Są one powiązane z klasą [ istia.st.articles.web.spring.ListController], którą wkrótce omówimy szczegółowo. Po wykonaniu jednej z tych akcji w przeglądarce otrzymujemy następujący wynik:

Image

Kod klasy [istia.st.articles.web.spring.ListController] jest następujący:

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 {

     // konfiguracja aplikacji internetowej
    Config config;

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

     // przetwarzanie żądania
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

         // żądanie listy artykułów
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // zapisywanie błędu
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // wyświetla się strona błędów
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // wysyła widok błędów
            return new ModelAndView("erreurs");
        }
         // wyświetla listę artykułów
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // wysyłanie widoku
        return new ModelAndView("liste");
    }

}

Uwagi:

  • klasa posiada pole prywatne o nazwie [config]. Pole to zostało zainicjowane przez Spring podczas instancjonowania fasoli [ListController]:
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Jak widać powyżej, pole [config] w [ListController] jest inicjowane za pomocą bean [config]. O co tu chodzi? Chodzi o bean [config] zdefiniowany w [applicationContext.xml], c.a.d oraz o instancję [istia.st.articles.web.spring.Config] opisaną powyżej.

  • Napisanie kodu klasy [Controller] polega zasadniczo na napisaniu kodu jej metody [handleRequest]
  • żądamy od modelu listy artykułów. Dostęp do niego uzyskujemy poprzez [config.getArticlesDomain()]. Jeśli wystąpi wyjątek, wysyłany jest widok [ERREURS]. Wynik zwracany przez metodę [handleRequest] musi być typu [ModelAndView]. Klasę tę można instancjonować na różne sposoby. W tym przypadku – i tak będzie zawsze – tworzymy instancję klasy [ModelAndView], przekazując jej klucz widoku, który ma zostać wyświetlony. Przypominamy, że zgodnie z konfiguracją bean’a [viewResolver], żądanie wyświetlenia widoku o kluczu XX spowoduje wysłanie widoku [/vues/XX.jsp].
         // żądanie listy artykułów
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // zapisuje się błąd
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // wyświetla stronę błędów
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // wysyłanie widoku błędów
            return new ModelAndView("erreurs");
        }
  • jeśli nie ma błędów, wysyłany jest widok [LISTE]:
1
2
3
4
5
6
7
         // wyświetla się lista artykułów
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // wysyłanie widoku
        return new ModelAndView("liste");

3.7.7.3. infos.do

Ta akcja służy do dostarczenia informacji o jednym z artykułów wyświetlanych w widoku [LISTE]:

Ta akcja jest zdefiniowana w widoku [springwebarticles-servlet.xml] poprzez:

     <!-- mapowanie aplikacji-->
    <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>

Kod klasy [InfosController] jest następujący:

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 {

     // konfiguracja aplikacji internetowej
    Config config;

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

     // przetwarzanie żądania
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

     // lista błędów
    ArrayList erreurs = new ArrayList();
     // pobieramy żądany identyfikator
    String strId = request.getParameter("id");
     // coś tam?
    if (strId == null) {
       // nieprawidłowe
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // przekształcamy strId na liczbę całkowitą
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // nieprawidłowe
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // żądany jest identyfikator klucza
    Article article = null;
    try {
      article = config.getArticlesDomain().getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // nieprawidłowe
      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) {
       // nieprawidłowe
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // umieszczamy pozycję w sesji
    request.getSession().setAttribute("article", article);
     // wyświetla się strona z informacjami
    request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
        return new ModelAndView("infos");
    }
}

Uwagi:

  • Metoda [handleRequest] pobiera parametr [id], który powinien normalnie znajdować się w adresie URL. Adres ten musi mieć bowiem postać [/infos.do?id=X]. Przeprowadzane są różne testy w celu sprawdzenia obecności i poprawności parametru [id]. W przypadku wystąpienia problemu wysyłany jest widok [ERREURS].
  • Jeśli parametr [id] jest prawidłowy, odpowiedni artykuł jest żądany z warstwy [domain]. Jeśli warstwa ta zgłasza wyjątek lub artykuł nie zostanie znaleziony, ponownie wysyłany jest widok [ERREURS].
  • Jeśli wszystko przebiega pomyślnie, uzyskany artykuł jest umieszczany w sesji. Jest to kwestia dyskusyjna. W tym przypadku wychodzimy z założenia, że klient być może kupi ten artykuł. Jeśli tak się stanie, pobierzemy go z sesji, zamiast ponownie żądać go z warstwy [domain].
  • W końcu wyświetlany jest widok [INFOS].

3.7.7.4. achat.do

Ta akcja służy do zakupu produktu wyświetlonego w poprzednim widoku [INFOS]:

Image

Jeśli przyjrzymy się kodowi HTML tego widoku, zauważymy, że tag <form> jest zdefiniowany w następujący sposób:

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

Widać zatem, że formularz jest przesyłany do kontrolera za pomocą akcji [achat.do].

Akcja ta jest skonfigurowana w następujący sposób w [springwebarticles-servlet.xml]:

     <!-- mapowanie aplikacji-->
    <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>

Kod klasy [AchatController] jest następujący:

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 {

   // konfiguracja aplikacji internetowej
  Config config;

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

   // przetwarzanie żądania
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // lista błędów związanych z tą akcją
    ArrayList erreurs = new ArrayList();
     // pobieranie ilości zakupionej
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // błędna ilość
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
     // pobieranie sesji klienta
    HttpSession session = request.getSession();
     // pobieranie produktu dodanego do koszyka
    Article article = (Article) session.getAttribute("article");
     // czy sesja wygasła?
    if (article == null) {
       // wyświetlanie strony błędów
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // tworzy się nowe zamówienie
    Achat achat = new Achat(article, qté);
     // dodajemy zakup do koszyka klienta
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // powrót do listy produktów
    return new ModelAndView("index");
  }
}

Uwagi:

  • przypomnijmy sobie format formularza wysyłanego do kontrolera:
        <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>
  • W zapytaniu znajdują się dwa parametry: [id] – numer zakupionego artykułu, [qte] – zakupiona ilość.
  • Sprawdzana jest obecność i poprawność parametru [qte]. Jeśli parametr ten zostanie uznany za nieprawidłowy, użytkownikowi zwracany jest widok [INFOS] wraz z komunikatem o błędzie:
     // lista błędów związanych z  czynnością
    ArrayList erreurs = new ArrayList();
     // pobierana jest ilość zakupiona
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // błędna ilość
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
  • zakupiony artykuł jest pobierany z sesji. Sesja mogła wygasnąć. W takim przypadku wysyłany jest widok [ERREURS]:
     // pobieranie sesji klienta
    HttpSession session = request.getSession();
     // pobieranie produktu dodanego do koszyka
    Article article = (Article) session.getAttribute("article");
     // czy sesja wygasła?
    if (article == null) {
       // wyświetlanie strony błędów
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • jeśli sesja nie wygasła, produkt jest umieszczany w koszyku, który również jest pobierany z sesji:
1
2
3
4
5
6
7
8
9
     // tworzy się nowe zamówienie
    Achat achat = new Achat(article, qté);
     // dodajemy zakup do koszyka klienta
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • na koniec wysyłamy widok [LISTE]:
     // powrót do listy produktów
    return new ModelAndView("index");
  • Powyżej wysyłamy widok [/vues/index.jsp]. Wiemy, że widok ten nakazuje przeglądarce klienta przekierowanie się na adres URL [/main.do]. To właśnie to przekierowanie spowoduje wyświetlenie listy artykułów.

3.7.7.5. panier.do

Ta akcja służy do wyświetlenia wszystkich zakupów klienta. Jest ona dostępna poprzez menu:

Image

Kod HTML powiązany z powyższym linkiem jest następujący:

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

Strona wyświetlana po kliknięciu tego linku to:

Image

Ta akcja jest skonfigurowana w następujący sposób w [springwebarticles-servlet.xml]:

     <!-- mapowanie aplikacji-->
    <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>

Kod klasy [VoirPanierController] jest następujący:

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 {

   // konfiguracja aplikacji internetowej
  Config config;

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

   // przetwarzanie żądania
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // wyświetlanie koszyka
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // koszyk pusty
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("paniervide");
    } else {
       // w koszyku znajduje się coś
      request.setAttribute("actions", new Hashtable[] {
          config.getHActionListe(), config.getHActionValidationPanier() });
      return new ModelAndView("panier");
    }
  }
}

Uwagi:

  • koszyk jest pobierany z sesji, w której zwykle się znajduje. Sesja mogła wygasnąć, w związku z czym nie ma koszyka. Nie traktujemy tego jako błędu, lecz po prostu zakładamy, że koszyk jest pusty.
  • jeśli koszyk jest pusty, wyświetlany jest widok [PANIERVIDE]
  • w przeciwnym razie wyświetlany jest widok [PANIER]

3.7.7.6. retirerachat.do

Ta akcja służy do usunięcia produktu z koszyka:

Image

Jeśli przyjrzymy się kodowi HTML z powyższego linku, otrzymamy następujący wynik:

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

Akcja [retirerachat.do] otrzymuje zatem jako parametr identyfikator produktu, który ma zostać usunięty z koszyka. Akcja ta jest skonfigurowana w następujący sposób w [springwebarticles-servlet.xml]:

     <!-- mapowanie aplikacji-->
    <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>

Kod klasy [RetirerAchatController] jest następujący:

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 {

   // konfiguracja aplikacji internetowej
  Config config;

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

   // przetwarzanie żądania
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // lista błędów związanych z tą czynnością
    ArrayList erreurs = new ArrayList();
     // pobieranie koszyka
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // wygasła sesja
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // pobieranie identyfikatora produktu do usunięcia
    String strId = request.getParameter("id");
     // coś?
    if (strId == null) {
       // nieprawidłowe
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // przekształcamy strId na liczbę całkowitą
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // nieprawidłowe
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // usuwamy zakup
    panier.enlever(id);
     // ponownie wyświetla się koszyk
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");
  }
}

Uwagi:

  • kod sprawdza obecność i poprawność parametru [id]. Jeśli okaże się on nieprawidłowy, wysyłany jest widok [ERREURS].
  • W przeciwnym razie zakup jest usuwany z koszyka:
     // usuwa się zakup
    panier.enlever(id);
  • następnie koszyk jest ponownie wyświetlany:
     // ponownie wyświetla się koszyk
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");

Przypomnijmy kod widoku [/vues/redirpanier.jsp]:

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

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

Widać, że klient zostanie przekierowany do akcji [/panier.do]. Została ona już opisana. Wyświetli ona widok [PANIER] lub [PANIERVIDE] w zależności od stanu koszyka.

3.7.7.7. validerpanier.do

Ta akcja służy do zatwierdzenia zakupów dokonanych przez klienta. W praktyce oznacza to jedną czynność: w bazie danych stan magazynowy zakupionych artykułów zostanie pomniejszony o zakupione ilości. Akcja ta pochodzi z następującego menu:

Image

Kod HTML dla linku [Valider le panier] jest następujący:

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

Po aktywowaniu tego linku stany magazynowe są pomniejszane, a lista artykułów ponownie wyświetlana.

Ta akcja jest skonfigurowana w następujący sposób w [springwebarticles-servlet.xml]:

     <!-- mapping aplikacji-->
    <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>

Kod klasy [ValiderPanierController] jest następujący:

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 {

   // konfiguracja aplikacji internetowej
  Config config;

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

   // przetwarzanie żądania
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // lista błędów związanych z tą czynnością
    ArrayList erreurs = new ArrayList();
     // kupujący potwierdził zawartość koszyka
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sesja wygasła
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // koszyk został zatwierdzony
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // nieprawidłowy kod
      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");
    }
     // pobierane są ewentualne błędy
    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");
    }
     // wszystko wygląda na w porządku OK – wyświetla się lista produktów
    return new ModelAndView("index");
  }
}

Uwagi:

  • koszyk jest pobierany z sesji. Jeśli sesja wygasła, wysyłany jest widok [ERREURS]:
     // lista błędów związanych z  czynnością
    ArrayList erreurs = new ArrayList();
     // kupujący potwierdził zawartość koszyka
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // sesja wygasła
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • potwierdzamy zakupy z koszyka. Mogą wystąpić błędy, jeśli stan magazynowy jest niewystarczający do zrealizowania zamówienia. W takim przypadku wysyłamy widok [ERREURS]:
     // zatwierdzamy koszyk
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // nieprawidłowy stan
      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");
    }
     // pobierane  ewentualne błędy
    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");
    }
  • jeśli wszystko przebiegło pomyślnie, ponownie wyświetla się lista artykułów:
     // wszystko wygląda na w porządku OK – wyświetla się lista produktów
    return new ModelAndView("index");

Wiadomo, że widok [/vues/index.jsp] przekierowuje klienta do akcji [/main.do]. Ta akcja wyświetli widok [LISTE].