Skip to content

3. Стаття 2 — Приклади трирівневих веб-архітектур

Мета статті:

  • тришарові архітектури
  • базова веб-архітектура MVC
  • архітектура Struts MVC
  • архітектура Spring MVC

Використані інструменти:

  • 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, драйвер JDBC. Насправді підійде будь-який драйвер JDBC.
  • IBExpert, персональна версія: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (березень 2005 р.). IBExpert дозволяє графічно адмініструвати SGBD Firebird.
  • Tomcat: http://jakarta.apache.org/tomcat/
  • Плагін Tomcat для Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. Див. також документ https://tahe.developpez.com/java/eclipse/

Для розуміння цього документа необхідні певні попередні знання. Деякі з них можна отримати з документів, які я написав. У цьому випадку я наводжу посилання на них. Зрозуміло, що це лише рекомендація, і читач може користуватися своїми улюбленими документами.

  • мова Java: [https://tahe.developpez.com/java/cours]
  • веб-програмування на Java: [https://tahe.developpez.com/java/web/]
  • веб-програмування з використанням Java, Eclipse, Tomcat: [https://tahe.developpez.com/java/eclipse/]
  • веб-програмування за допомогою Struts: [https://tahe.developpez.com/java/struts/]
  • використання аспекту IoC у Spring: [https://tahe.developpez.com/java/springioc]
  • Бібліотека тегів JSTL: [https://tahe.developpez.com/java/eclipse/] (частково)
  • документація Ibatis SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (березень 2005 р.).

Ідеї, викладені в цьому документі, походять із книги, прочитаної влітку 2004 року — чудової праці Рода Джонсона: J2EE «Development without EJB», виданої видавництвом Wrox.


3.1. Веб-додаток «webarticles»

Тут ми хочемо навести деякі елементи веб-додатку для електронної комерції. Цей додаток дозволить користувачам Інтернету

  • переглядати перелік товарів із бази даних
  • додавати деякі з них до електронного кошика
  • підтвердити його. Єдиним наслідком такого підтвердження буде оновлення в базі даних запасів придбаних товарів.

Користувачеві будуть доступні такі екрани:

  • екран [LISTE], що відображає перелік товарів, які продаються

Image

  • вигляд [INFOS], що містить додаткову інформацію про товар:

Image

  1. погляд [PANIER], що відображає вміст кошика клієнта

Image

  1. погляд [PANIERVIDE] на випадок, якщо кошик клієнта порожній

Image

  1. вигляд [ERREURS], який повідомляє про будь-яку помилку в роботі додатка

Image

3.2. Загальна архітектура додатка

Ми хочемо створити додаток із такою трирівневою структурою:

  • три рівні є незалежними завдяки використанню інтерфейсів Java
  • інтеграція різних рівнів здійснюється за допомогою Spring
  • кожен рівень реалізовано в окремих пакетах: web (рівень користувацького інтерфейсу), domain (бізнес-рівень) та dao (рівень доступу до даних).

Тут ми припустимо, що рівні [domain] та [dao] вже готові. Ми зосередимося лише на рівні [web], який пропонуємо реалізувати кількома способами:

  • за допомогою класичної технології сервлетів-контролерів — сторінки JSP
  • за допомогою технології Struts MVC
  • за допомогою технології Spring — MVC

У будь-якому випадку додаток відповідатиме архітектурі MVC (Модель — Вигляд — Контролер). Якщо повернутися до наведеної вище схеми з шарами, архітектура MVC вписується в неї наступним чином:

Обробка запиту клієнта відбувається за такими етапами:

  1. клієнт надсилає запит до контролера. Цей контролер — це сервлет, через який проходять усі запити клієнтів. Це вхідні ворота додатка. Це літера «C» у MVC.
  2. контролер обробляє цей запит. Для цього йому може знадобитися допомога бізнес-шару, який у структурі MVC позначається літерою «M».
  3. контролер отримує відповідь від бізнес-шару. Запит клієнта було оброблено. Це може призвести до кількох можливих відповідей. Класичним прикладом є
    • сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
    • сторінка підтвердження в іншому випадку
  4. контролер обирає відповідь (= представлення), яку слід надіслати клієнту. Найчастіше це сторінка, що містить динамічні елементи. Контролер передає їх до представлення.
  5. Вигляд надсилається клієнту. Це «V» у MVC.

3.3. Модель

Тут ми розглядаємо «М» у MVC. Модель у цьому випадку складається з таких елементів:

  1. бізнес-класи
  2. класи доступу до даних
  3. база даних

3.3.1. База даних

База даних містить лише одну таблицю з назвою ARTICLES. Вона була згенерована за допомогою таких команд 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
);


/* обмеження */
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<>'');

/* первинний ключ */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
первинний ключ, що однозначно ідентифікує товар
nom
назва товару
prix
її ціна
stockactuel
поточний запас
stockminimum
запас, при недосягненні якого необхідно оформити замовлення на поповнення запасів

У наведених нижче тестах використовувалася база даних [Firebird]. [Firebird] є «відкритим» варіантом SGBD. Драйвер JDBC [firebirdsql-full.jar] розміщено в папці [WEB-INF/lib] веб-додатку.

3.3.2. Пакеты шаблона

Модель M надається у вигляді трьох архівів:

  • istia.st.articles.dao: містить класи доступу до даних шару [dao]
  • istia.st.articles.exception: містить клас винятків для цієї системи управління товарами
  • istia.st.articles.domain: містить бізнес-класи рівня [domain]
archive
contenu
rôle
istia.st.articles.dao
- містить пакет [istia.st.articles.dao], який, у свою чергу, містить такі елементи:
- [IArticlesDao]: інтерфейс доступу до рівня Dao. Це єдиний інтерфейс, який бачить рівень [domain]. Інших він не бачить.
- [Article]: клас, що визначає статтю
- [ArticlesDaoSqlMap]: клас реалізації інтерфейсу [IArticlesDao] за допомогою інструменту SqlMap
шар доступу до даних — повністю розміщений у шарі [dao] трирівневої архітектури веб-додатку
istia.st.articles.domain
- містить пакет [istia.st.articles.domain], який, у свою чергу, містить такі елементи:
- [IArticlesDomain]: інтерфейс доступу до шару [domain]. Це єдиний інтерфейс, який бачить веб-шар. Інших він не бачить.
- [AchatsArticles]: клас, що реалізує [IArticlesDomain]
- [Achat]: клас, що представляє покупку клієнта
- [Panier]: клас, що представляє сукупність покупок клієнта
представляє модель здійснення покупок в Інтернеті — повністю розміщується в шарі [domain] трирівневої архітектури веб-додатку
istia.st.articles.exception
- містить пакет [istia.st.articles.exception], який, у свою чергу, містить такі елементи:
- [UncheckedAccessArticlesException]: клас, що визначає виняток типу [RuntimeException]. Цей тип винятку генерується шаром [dao], щойно виникає проблема з доступом до даних.
 

3.3.3. Пакет [istia.st.articles.dao]

Клас, що визначає статтю, має такий вигляд:

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) {
     // ініціалізація атрибутів екземпляра
    setId(id);
    setNom(nom);
    setPrix(prix);
    setStockActuel(stockActuel);
    setStockMinimum(stockMinimum);
  }

     // геттери — сеттери
  public int getId() {
    return id;
  }

  public void setId(int id) {
     // ідентифікатор дійсний?
    if (id < 0)
      throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
    this.id = id;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
     // ім'я дійсне?
    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) {
     // ціна дійсна?
    if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
    this.prix = prix;
  }

  public int getStockActuel() {
    return stockActuel;
  }

  public void setStockActuel(int stockActuel) {
     // Чи правильний запас?
    if (stockActuel < 0)
      throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
    this.stockActuel = stockActuel;
  }

  public int getStockMinimum() {
    return stockMinimum;
  }

  public void setStockMinimum(int stockMinimum) {
     // запаси дійсні?
    if (stockMinimum < 0)
      throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
    this.stockMinimum = stockMinimum;
  }

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

Цей клас надає:

  1. конструктор, що дозволяє задати 5 параметрів товару
  2. методи доступу, які часто називають геттерами/сеттерами, що слугують для зчитування та запису цих 5 елементів інформації. Назви цих методів відповідають стандарту JavaBean. Використання об’єктів JavaBean у шарі DAO для взаємодії з даними SGBD є стандартною практикою.
  3. перевірка даних, введених у товарну позицію. У разі виявлення помилкових даних генерується виняток.
  4. метод toString, який дозволяє отримати значення позиції у вигляді символьного рядка. Це часто буває корисним для налагодження програми.

Інтерфейс [IArticlesDao] визначено наступним чином:

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

Роль різних методів інтерфейсу така:

getAllArticles
повертає всі позиції з таблиці ARTICLES у вигляді списку об’єктів [Article]
clearAllArticles
очищає таблицю ARTICLES
getArticleById
повертає об’єкт [Article], ідентифікований за його первинним ключем
ajouteArticle
дозволяє додати запис до таблиці ARTICLES
modifieArticle
дозволяє змінити запис у таблиці [ARTICLES]
supprimerArticle
дозволяє видалити товар із таблиці [ARTICLES]
changerStockArticle
дозволяє змінити залишок товару в таблиці [ARTICLES]

Інтерфейс надає клієнтським програмам певну кількість методів, визначених виключно їхніми сигнатурами. Він не визначає, як ці методи будуть фактично реалізовані. Це забезпечує гнучкість у роботі додатка. Клієнтська програма звертається до інтерфейсу, а не до його конкретної реалізації.

Вибір конкретної реалізації здійснюється за допомогою конфігураційного файлу Spring. Тут ми пропонуємо реалізувати інтерфейс IArticlesDao за допомогою продукту з відкритим кодом під назвою SqlMap. Це дозволить нам видалити всі інструкції SQL з коду Java.

Клас реалізації [ArticlesDaoSqlMap] визначається наступним чином:

package istia.st.articles.dao;

// Імпорт
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;

public class ArticlesDaoSqlMap implements IArticlesDao {

   // Поля
  private SqlMapClient sqlMap;

   // Конструктори
  public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }

   // Методи
  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) {}
}

Усі методи доступу до даних було синхронізовано, щоб уникнути проблем з паралельним доступом до джерела даних. У будь-який момент часу лише один потік має доступ до певного методу.

Клас [ArticlesDaoSqlMap] використовує інструмент [Ibatis SqlMap]. Перевага цього інструменту полягає в тому, що він дозволяє винести код SQL, що забезпечує доступ до даних, з коду Java. Після цього він розміщується у файлі конфігурації. Ми ще повернемося до цього пізніше. Для своєї інстанціації клас [ArticlesDaoSqlMap] потребує конфігураційного файлу, ім’я якого передається як параметр конструктору класу. Цей конфігураційний файл визначає інформацію, необхідну для:

  • доступ до SGBD, у якому містяться статті
  • управління пулом з’єднань
  • управління транзакціями

У нашому прикладі він матиме назву [sqlmap-config-firebird.xml] і визначатиме доступ до бази даних 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>

Згаданий вище файл конфігурації [articles.xml] дозволяє визначити, як створити екземпляр класу [istia.st.articles.dao.Article] на основі рядка з таблиці [ARTICLES] у SGBD. Він також визначає запити SQL, які дозволять шару [dao] отримувати дані з джерела даних 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">

     <!-- псевдонім для класу istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- маппінг ORM :  рядок таблиці ARTICLES — екземпляр класу 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>

     <!-- запит SQL для отримання всіх статей -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- запит SQL для видалення всіх статей -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- запит SQL для додавання товару -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#назва#,#ціна#,#поточнийзапас#,#мінімальнийзапас#)
</statement>

     <!-- запит SQL для видалення певного товару -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- запит SQL для редагування певного товару -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#назва#,
    prix=#ціна#,поточнийзапас=#поточнийзапас#,мінімальнийзапас=#мінімальнийзапас# where
    id=#id#
</statement>

     <!-- запит SQL для отримання певного товару -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- запит SQL для зміни запасу певного товару -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#рух#
    where id=#id# та stockActuel+#рух#&gt;=0
</statement>
</sqlMap>

Код пакета [dao] наведено у додатку.

3.3.4. Пакет [istia.st.articles.domain]

Інтерфейс [IArticlesDomain] відокремлює рівень [métier] від рівня [web]. Останній отримує доступ до шару [métier/domain] через цей інтерфейс, не зважаючи на те, який саме клас його фактично реалізує. Інтерфейс визначає такі дії для доступу до бізнес-шару:

 package istia.st.articles.domain;

// Імпорт
import java.util.ArrayList;
import java.util.List;

public abstract interface IArticlesDomain {

   // Методи
  void acheter(Panier panier);
  List getAllArticles();
  Article getArticleById(int idArticle);
  ArrayList getErreurs();
}
List getAllArticles()
повертає список об’єктів [Article] для відображення клієнту
Article getArticleById(int idArticle)
повертає об’єкт [Article], ідентифікований за [idArticle]
void acheter(Panier panier)
підтверджує кошик клієнта, зменшуючи запаси придбаних товарів на кількість, що була придбана — може завершитися невдачею, якщо запасів недостатньо
ArrayList getErreurs()
повертає список помилок, що виникли — порожній, якщо помилок немає

Тут інтерфейс [IArticlesDomain] буде реалізовано за допомогою наступного класу [AchatsArticles]:

package istia.st.articles.domain;

// Імпорт
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 {

   // Поля
  private IArticlesDao articlesDao;
  private ArrayList erreurs;

   // Конструктори
  public AchatsArticles(IArticlesDao articlesDao) { }

   // Методи
  public ArrayList getErreurs() {}
  public List getAllArticles() {}
  public Article getArticleById(int id) {}
  public void acheter(Panier panier) { }
}

Цей клас реалізує чотири методи інтерфейсу [IArticlesDomain]. Він має два приватні поля:

IArticlesDao articlesDao
об’єкт доступу до даних, наданий рівнем доступу до даних
ArrayList erreurs
список можливих помилок

Щоб створити екземпляр класу, потрібно надати об’єкт, що забезпечує доступ до даних SGBD:

public AchatsArticles(IArticlesDao articlesDao)
конструктор

Клас [Achat] представляє покупку клієнта:

package istia.st.articles.domain;

public class Achat {

   // Поля
  private Article article;
  private int qte;

  // Конструктори
  public Achat(Article article, int qte) { }

  // Методи
  public double getTotal() {}
  public Article getArticle() {}
  public void setArticle(Article article) { }
  public int getQte() {}
  public void setQte() { }
  public String toString() {}
}

Клас [Achat] є класом JavaBean із такими полями та методами:

article
придбаний товар
qte
кількість, що була придбана
double getTotal()
сума покупки
String toString()
ідентифікаційний рядок об'єкта

Клас [Panier] представляє сукупність покупок клієнта:

package istia.st.articles.domain;

// Імпорт
import java.util.ArrayList;

public class Panier {

  // Поля
  private ArrayList achats;

   // Конструктори
  public Panier() { }

   // Методи
  public ArrayList getAchats() {}
  public void ajouter(Achat unAchat) { }
  public void enlever(int idAchat) { }
  public double getTotal() {}
  public String toString() { }
}

Клас [Panier] є класом JavaBean із такими полями та методами:

achats
список покупок клієнта — список об’єктів типу [Achat]
void ajouter(Achat unAchat)
додає покупку до списку покупок
void enlever(int idArticle)
видаляє покупку товару idArticle
double getTotal()
показує загальну суму покупок
String toString()
повертає ідентифікаційний рядок кошика
ArrayList getAchats()
повертає список покупок

Код пакета [domain] наведено в додатку.

3.3.5. Пакет [istia.st.articles.exception]

Цей пакет містить клас, що визначає виняток, який генерує рівень [dao] у разі виникнення проблеми з доступом до джерела даних:

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. Тестування моделі

Модель M було протестовано в Eclipse з такою конфігурацією:

Image

Коментарі:

  • у [WEB-INF/lib] містяться:
    • архіви, необхідні для інструменту [ibatis SqlMap], який відповідає за доступ до SGBD Firebird: ibatis-*.jar
    • архів, необхідний для інструменту [spring]: spring.jar
    • драйвер JDBC для SGBD та [Firebird]: firebirdsql-full.jar
    • архіви, необхідні для логів: log4-*.jar, commons-logging.jar
    • три архіви тестованої моделі: istia.st.articles.*.jar
    • архів, необхідний для інструменту тестування [junit]
  • у [WEB-INF/src] містяться файли конфігурації, які Eclipse автоматично скопіює до [WEB-INF/classes]:
    • конфігураційні файли інструменту [sqlmap]: sqlmap-config-firebird.xml, articles.xml
    • конфігураційний файл інструменту [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
    • файл конфігурації інструменту [log4j]: log4j.properties
  • у пакеті [istia.st.articles.tests] містяться класи тестів моделі

3.3.6.1. Тести шару [dao]

Клас тестування JUnit для шару [dao] має такий вигляд. Його аналіз дозволяє зрозуміти, як використовуються методи інтерфейсу [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;

// тестування класу ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {

     // екземпляр класу, що тестується
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // отримує екземпляр доступу до даних
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }

    public void testGetAllArticles() {
         // відображає товари
        listArticles();
    }

    public void testClearAllArticles() {
         // очищає таблицю статей
        articlesDao.clearAllArticles();
         // зчитує таблицю ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
    }

    public void testAjouteArticle() {
         // видалення вмісту з ARTICLES
        articlesDao.clearAllArticles();
         // зчитує таблицю ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // вставка
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // зчитує таблицю ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         //— відображення
        listArticles();
    }

    public void testSupprimeArticle() {
         // видалення вмісту з ARTICLES
        articlesDao.clearAllArticles();
         // зчитує таблицю ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // вставка
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // читає таблицю ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // видалення
        articlesDao.supprimeArticle(4);
         // читає таблицю ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(1, articles.size());
         // відображає таблицю
        listArticles();
    }

    public void testModifieArticle() {
         // видалення вмісту з ARTICLES
        articlesDao.clearAllArticles();
         // зчитує таблицю ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // вставка
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // читає таблицю 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");
         // зміна
        articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
         // getById
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getPrix(), 44, 1e-6);
         // відображає таблицю
        listArticles();
    }

    public void testGetArticleById() {
         // видалення вмісту з ARTICLES
        articlesDao.clearAllArticles();
         // читає таблицю ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // вставка
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // читає таблицю 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() {
         // зчитує таблицю ARTICLES
        List articles = articlesDao.getAllArticles();
         // відображаються прочитані статті
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

    public void testChangerStockArticle() throws InterruptedException {
         // видалення вмісту з ARTICLES
        articlesDao.clearAllArticles();
         // вставка
        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);
         // створення 100 потоків для оновлення запасів товару № 3
        Thread[] taches = new Thread[100];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
            taches[i].start();
        }
         // очікування завершення потоків
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // отримати товар № 3 та перевірити його запаси
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        assertEquals(1, unArticle.getStockActuel());
         // зміна запасів товару № 4
        boolean erreur = false;
        int nbLignes = articlesDao.changerStockArticle(4, -100);
        assertEquals(0, nbLignes);
         // виводить таблицю
        listArticles();
    }
}

Коментарі:

  • тестовий клас за допомогою свого методу setUp зберігає екземпляр класу, що тестується:
1
2
3
4
5
6
7
8
     // екземпляр тестованого класу
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // отримує екземпляр класу доступу до даних
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }
  • об’єкт, що тестується, надається методом [Spring]. Вище запитується Spring-бін із назвою [articlesDao]. Цей бін визначений у конфігураційному файлі 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>
     <!-- клас доступу до даних -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
</beans>

Як бачимо вище, bean [articlesDao] є екземпляром класу [istia.st.articles.dao.ArticlesDaoSqlMap]. Цей клас має конструктор, який як параметр очікує ім’я конфігураційного файлу інструменту [SqlMap]. Ця назва вказана тут. Це [sqlmap-config-firebird.xml]. Останній вже було описано. Він надає всю необхідну інформацію для доступу до даних SGBD.

Метод [testChangerStockArticle] створює 100 потоків, завданням яких є зменшення запасів певного товару. Мета цього тесту — перевірити паралельний доступ до SGBD. Оскільки метод [changerStockArticle] класу [istia.st.articles.dao.ArticlesDaoSqlMap] було синхронізовано, цей тест проходить успішно. Якщо прибрати синхронізацію, тест більше не проходить. Клас, відповідальний за оновлення запасів, має такий вигляд:

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() {
         // відстеження
        System.out.println(name + " lancé");
         // зміна запасів товару № 3
        articlesDao.changerStockArticle(3, -1);
         // відстеження
        System.out.println(name + " terminé");
    }
}
  • зазначений вище клас зменшує запас товару № 3 на 1

3.3.6.2. Тести рівня [domain]

Тестовий клас JUnit для шару [domain] має такий вигляд:

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;

// тестування класу ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {

     // екземпляр класу доступу до домену
    private IArticlesDomain articlesDomain;

     // екземпляр класу доступу до даних
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // отримує екземпляр класу доступу до домену
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // отримує екземпляр класу доступу до даних
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }

     // отримання конкретного товару
    public void testGetArticleById() {
         // видалення вмісту з ARTICLES
        articlesDao.clearAllArticles();
         // читає таблицю ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // вставка
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // читає таблицю 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");
    }

     // виведення на екран
    private void listArticles() {
         // зчитує таблицю ARTICLES
        List articles = articlesDomain.getAllArticles();
         // відображаються прочитані статті
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

     // придбання статей
    public void testAchatPanier(){
         // видалення вмісту з ARTICLES
        articlesDao.clearAllArticles();
         // зчитує таблицю ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // вставка
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // читає таблицю ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // створення кошика з двома товарами
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // перевірка
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // підтвердження кошика
        articlesDomain.acheter(panier);
         // перевірки
        assertEquals(0,articlesDomain.getErreurs().size());
        assertEquals(0,panier.getAchats().size());
         // пошук товару № 3
        article3=articlesDomain.getArticleById(3);
        assertEquals(20,article3.getStockActuel());
         // пошук товару № 4
        article4=articlesDomain.getArticleById(4);
        assertEquals(30,article4.getStockActuel());
         // новий кошик
        panier.ajouter(new Achat(article3,100));
         // підтвердження кошика
        articlesDomain.acheter(panier);
         // перевірка — куплено забагато
         // мабуть, сталася помилка
        assertEquals(1,articlesDomain.getErreurs().size());
         // пошук товару № 3
        article3=articlesDomain.getArticleById(3);
         // запаси, мабуть, не змінилися
        assertEquals(20,article3.getStockActuel());    
    }

     // вилучити покупки
    public void testRetirerAchats(){
         // видалення вмісту з ARTICLES
        articlesDao.clearAllArticles();
         // читає таблицю ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // вставка
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // читає таблицю ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // створення кошика з двома товарами
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // перевірка
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // додавання товару, який вже було придбано
        panier.ajouter(new Achat(article3,10));
         // перевірки
         // сума повинна бути збільшена до 1000
        assertEquals(1000.0,panier.getTotal(),1e-6);
         // у кошику як і раніше 2 товари
        assertEquals(2,panier.getAchats().size());
         // кількість товару № 3 має бути змінена на 20
        Achat achat=(Achat)panier.getAchats().get(0);
        assertEquals(20,achat.getQte());
         // товар № 3 видаляється з кошика
        panier.enlever(3);
         // перевірки
         // загальна сума має бути змінена на 400
        assertEquals(400.0,panier.getTotal(),1e-6);
         // у кошику має бути лише 1 товар
        assertEquals(1,panier.getAchats().size());
         // це має бути товар № 4
        assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
    }
}

Коментарі:

  • клас тестування за допомогою свого методу setUp зберігає екземпляр класу, що тестується, а також екземпляр класу доступу до даних. Останній пункт є спірним. Теоретично тестовий клас не повинен мати доступу до шару [dao], про існування якого він навіть не повинен знати. У даному випадку ми пішли в обхід цієї «етики», дотримання якої змусило б нас створити нові методи в нашому інтерфейсі [IArticlesDomain].
     // екземпляр класу доступу до домену
    private IArticlesDomain articlesDomain;

     // екземпляр класу доступу до даних
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // отримує екземпляр доступу до домену
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // отримує екземпляр класу доступу до даних
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }
  • об’єкт, що тестується, надається [Spring]. Вище ми запитуємо Spring-бін під назвою [articlesDomain]. Цей бін визначений у конфігураційному файлі 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>
     <!-- клас доступу до даних -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- бізнес-клас -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Як бачимо вище, bean [articlesDomain] є екземпляром класу [istia.st.articles.domain.AchatsArticles]. Цей клас має конструктор, який як параметр очікує об’єкт доступу до шару [dao] типу [IArticlesDao]. У цьому випадку файл конфігурації вказує, що цим об’єктом є бін із назвою [articlesDao]. Це змушує Spring створити екземпляр цього біна. Про створення екземпляра біна [articlesDao] йшлося раніше. Отже, у підсумку було створено два екземпляри бінів:

  • [articlesDao] типу [istia.st.articles.dao.ArticlesDaoSqlMap]
  • [articlesDomain] типу [ istia.st.articles.domain.AchatsArticles]

Ці дві інстанції були створені внаслідок першого виклику Spring:

1
2
3
4
         // отримує екземпляр доступу до домену
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");

У результаті отримуємо бін [articlesDomain]. Під час другого виклику Spring:

1
2
3
         // отримує екземпляр доступу до даних
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");

[Spring] просто повертає посилання на бін [articlesDao], який вже був створений під час попереднього виклику. Це принцип синглтона. Якщо ми запитуємо бін у Spring, він створює його екземпляр, якщо він ще не існує, інакше повертає посилання на існуючий бін.

3.4. Трирівневий веб-додаток MVC

Далі ми хочемо створити такий трирівневий веб-додаток:

Додаток матиме архітектуру MVC. Модель M вже написано та протестовано. Це та сама модель, що описана раніше. Вона надається нам у трьох архівах [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. Нам потрібно написати контролер C та представлення V.

Спочатку розглянемо класичний підхід, за якого:

  • контролер C реалізується за допомогою єдиного сервлета
  • види V забезпечуються сторінками JSP

3.5. Архітектура MVC на основі сервлета-контролера та сторінок JSP

Архітектура MVC додатка буде такою:

M=modèle
бізнес-класи, класи доступу до даних та база даних
V=vues
сторінки JSP
C=contrôleur
сервлет для обробки запитів клієнтів

3.5.1. Модель

Вона була представлена раніше. Складається з Java-архівів [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.5.2. Види

Види відповідають тим, що були представлені на початку документа:

LISTE
liste.jsp
Види зібрані у папці [vues] додатка
INFOS
infos.jsp
PANIER
panier.jsp
PANIERVIDE
paniervide.jsp
ERREURS
erreurs.jsp

3.5.3. Контролер

Контролер складатиметься з єдиного сервлету під назвою [WebArticles]. Він оброблятиме різні запити клієнтів. Ці запити будуть формалізовані наявністю параметра [action] у запиті HTTP клієнта:

requête
значення
дія контролера
можливі відповіді
action=liste
клієнт хоче отримати список
товарів
- запитує перелік товарів у бізнес-шарі
бізнес-шару
- [LISTE]
- [ERREURS]
action=infos
клієнт запитує
інформацію про один із товарів, відображених у вікні
[LISTE]
- надсилає запит щодо товару до бізнес-шару
- [INFOS]
- [ERREURS]
action=achat
клієнт купує товар
- надсилає запит на товар до бізнес-шару та
додає її до кошика клієнта
- [INFOS], якщо сталася помилка з кількістю
- [LISTE], якщо помилки немає
action=retirerachat
клієнт хоче видалити
товар зі свого кошика
- отримує кошик із сесії та змінює його
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
клієнт хоче переглянути свій
кошик
- отримує кошик із сесії
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
клієнт завершив покупки
і переходить до етапу оплати
- оновлює в базі даних залишки
придбаних товарів
- видалити з кошика клієнта товари, щодо яких
придбання яких було підтверджено
- [LISTE]
- [ERREURS]

3.5.4. Налаштування додатка

Ми прагнемо налаштувати додаток таким чином, щоб він був максимально гнучким щодо таких змін, як:

  1. зміна URL-адрес різних переглядів
  2. зміна класів, що реалізують інтерфейси [IArticlesDao] та [IArticlesDomain]
  3. зміна SGBD, бази даних, таблиці статей

3.5.5. Зміни URL-адрес

Назви URL-адрес переглядів будуть розміщені у файлі конфігурації додатка [web.xml] разом з деякими іншими параметрами:

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

У файлі [web.xml] містяться

  • URL-адреси різних подань додатка
  • назва [springConfigFileName] файлу конфігурації Spring, який дозволить створювати об’єкти-сінглтони для доступу до бізнес-шару та шару DAO
  • представлення [/vues/index.jsp], яке відображатиметься, коли URL-адреса, запитувана клієнтом, буде /<context>, де <context> — це контекст додатка

3.5.6. Зміна класів реалізації інтерфейсів

Відповідно до принципів тришарової архітектури, шари повинні бути ізольованими один від одного. Ця ізоляція досягається таким чином:

  • шари взаємодіють між собою через інтерфейси, а не через конкретні класи
  • код одного рівня ніколи не створює екземпляр класу іншого рівня для його використання. Він просто запитує у зовнішнього інструменту, в даному випадку [Spring], екземпляр реалізації інтерфейсу того рівня, який він хоче використовувати. Для цього, як ми знаємо, їй не потрібно знати ім’я класу реалізації, а лише ім’я Spring-біна, посилання на який вона хоче отримати.

У нашому додатку файл конфігурації Spring може виглядати так:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- клас доступу до даних -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- бізнес-клас -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

Щоб отримати доступ до шару [métier], клас шару [Interface utilisateur, UI] може запросити бін [articlesDomain]. Тоді Spring створить екземпляр об’єкта типу [istia.st.articles.domain.AchatsArticles]. Для цього йому потрібен bean типу [articlesDao], тобто об’єкт типу [istia.st.articles.dao.ArticlesDaoSqlMap]. Тоді Spring створить екземпляр такого об’єкта. Це створення відбудеться на основі інформації, що міститься у файлі [sqlmap-config-firebird.xml] — файлі конфігурації доступу до даних через SqlMap. По завершенні операції клас [UI], який запросив бін [articlesDomain], має повний ланцюжок, що пов’язує його з даними SGBD:

3.5.7. Зміни, пов’язані з SGBD або базою даних

Незалежність веб-додатку від змін, пов’язаних із SGBD або базою даних, тут забезпечується конфігураційними файлами SqlMap. Їх є два:

  1. файл [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>

Цей файл вказує на базу даних Firebird. Достатньо змінити ім’я драйвера JDBC, щоб працювати з іншим — SGBD.

  1. файл [articles.xml], який містить різні інструкції SQL, необхідні для роботи програми:
<?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">

     <!-- псевдонім класу istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- маппінг ORM: р ядок таблиці ARTICLES — екземпляр класу 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>

     <!-- запит SQL для отримання всіх статей -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- запит SQL для видалення всіх статей -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- запит SQL для додавання товару -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#назва#,#ціна#,#поточнийзапас#,#мінімальнийзапас#)
</statement>

     <!-- запит SQL для видалення певного товару -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- запит SQL для редагування певного товару -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#назва#,
    prix=#ціна#,поточнийзапас=#поточнийзапас#,мінімальнийзапас=#мінімальнийзапас# where
    id=#id#
</statement>

     <!-- запит SQL для отримання певного товару -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- запит SQL для зміни запасу певного товару -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#рух#
    where id=#id# та stockActuel+#рух#&gt;=0
</statement>
</sqlMap>

Якщо назви таблиці товарів або стовпців зміняться, доведеться переписати запити в цьому конфігураційному файлі, не змінюючи при цьому код Java. Те саме стосується й випадків, коли запит замінюється збереженою процедурою з міркувань продуктивності.

3.5.8. Загальна архітектура додатка [webarticles]

Веб-додаток на Java — це пазл із безліччю елементів. Впровадження архітектури MVC зазвичай збільшує їхню кількість. Структура додатка [webarticles] під [eclipse] є такою:

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. Види JSP

Види JSP використовують бібліотеку тегів JSTL.

3.5.9.1. entete.jsp

Щоб забезпечити певну однорідність різних переглядів, вони матимуть спільний заголовок, у якому відображається назва додатка та меню:

Меню є динамічним і визначається контролером. Контролер додає до запиту, що надсилається на сторінку JSP, атрибут-ключ «actions», значенням якого є масив типу Hastable[]. Кожен елемент цього масиву є словником, призначеним для формування пункту меню у заголовку. Кожен словник має два ключі:

  • href: URL-адреса, пов’язана з пунктом меню
  • lien: текст меню

Інші подання додатка використовуватимуть заголовок, визначений [entete.jsp], за допомогою такого тегу JSP:

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

Під час виконання цей тег призведе до того, що в код сторінки JSP, яка його містить, буде включено код сторінки [entete.jsp]. Оскільки URL-адреса сторінки є відносною (без символу /), сторінка [entete.jsp] буде шукатися в тій самій папці, що й сторінка, яка містить тег <jsp:include>.

Код:

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

Цей вигляд відображає список товарів, доступних для продажу:

Він відображається після запиту /main?action=liste або /main?action=validationpanier. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню
listarticles
ArrayList об’єктів типу [Article]
message
об'єкт String — повідомлення, яке потрібно відобразити внизу сторінки

Кожне посилання [Infos] з масиву HTML статей має URL-адресу у вигляді [?action=infos&id=ID], де ID — це поле id відображуваної статті.

Код:

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

Цей вигляд відображає інформацію про товар, а також дозволяє його придбати:

Image

Вона відображається після запиту /main?action=infos&id=ID або запиту /main?action=achat&id=ID, коли кількість, що купується, вказана неправильно. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню
article
об’єкт типу [Article] — товар, який потрібно відобразити
msg
об'єкт String — повідомлення, яке слід відобразити у разі помилки щодо кількості
qte
об'єкт String — значення, яке потрібно відобразити в полі введення [Qte]

Поля [msg] та [qte] використовуються у разі помилки введення кількості:

Image

Ця сторінка містить форму, яка відправляється за допомогою кнопки [Acheter]. Цільова URL-адреса POST — [?action=achat&id=ID], де ID — це ідентифікатор придбаного товару.

Код:

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

Цей вигляд відображає вміст кошика:

Image

Він відображається після запиту /main?action=panier або /main?action=retirerachat&id=ID. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню
panier
об’єкт типу [Panier] — кошик, який потрібно відобразити

Кожне посилання [Retirer] у масиві HTML покупок у кошику має URL-адресу у вигляді [?action=retirerachat&id=ID], де ID — це поле [id]товару, який потрібно видалити з кошика.

Код:

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

Цей вигляд відображає інформацію про те, що кошик порожній:

Image

Вона відображається після запиту /main?action=panier або /main?action=retirerachat&id=ID. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню

Код:

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

Цей екран відображається у разі помилок:

Image

Вона відображається після будь-якого запиту, що призвів до помилки, за винятком дії купівлі з неправильною кількістю, яка обробляється у вікні [INFOS]. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню
erreurs
ArrayList об’єктів String, що представляють повідомлення про помилки, які потрібно відобразити

Код:

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

Ця сторінка визначена як головна сторінка додатка у файлі [web.xml] додатка:

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

Вигляд [index.jsp] просто перенаправляє клієнта до точки входу в додаток:

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

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

3.5.10. Контролер

Залишилося написати основну частину нашого веб-додатку — контролер. Його завдання полягає в тому, щоб:

  • отримати запит від клієнта,
  • обробити запитувану клієнтом дію за допомогою бізнес-класів,
  • відправити у відповідь відповідний вигляд.

3.5.10.1. Ініціалізація контролера

Коли клас контролера завантажується сервером сервлетів, виконується його метод [init]. Це відбудеться лише один раз. Після завантаження в пам’ять контролер залишатиметься там і оброблятиме запити різних клієнтів. Для кожного клієнта створюється окремий потік виконання, і таким чином методи контролера виконуються одночасно різними потоками. Нагадаємо, що саме з цієї причини контролер не повинен мати полів, які його методи могли б змінювати. Його поля мають бути доступними лише для читання. Вони ініціалізуються методом [init], що є його основною функцією. Цей метод має ту особливість, що виконується лише один раз одним потоком. Отже, у цьому методі не виникає проблем з паралельним доступом до полів контролера. Метод [init] призначений для ініціалізації об’єктів, необхідних для веб-додатка, які будуть спільними для всіх клієнтських потоків у режимі «тільки для читання». Ці спільні об’єкти можна розмістити у двох місцях:

  • у приватних полях контролера
  • контекст виконання додатка (ServletContext)

Метод [init] додатка [webarticles] виконає такі дії:

  • перевірить наявність у файлі [web.xml] параметрів, необхідних для коректної роботи додатка. Вони були описані в розділі 3.5.5.
  • заповнить приватне поле [ArrayList erreurs] списком можливих помилок. Цей список буде порожнім, якщо помилок немає, але він все одно існуватиме.
  • якщо помилки були, метод [init] на цьому завершується. В іншому випадку він створює об’єкт типу [IArticlesDomain], який буде бізнес-об’єктом, що контролер використовуватиме для своїх потреб. Як пояснено в розділі 3.5.6, контролер запитує у фреймворку Spring необхідний йому бін. Ця операція інстанціювання може призвести до різних помилок. У такому випадку вони, знову ж таки, зберігаються у полі [erreurs] контролера.

3.5.10.2. Методи doGet, doPost

Ці два методи обробляють запити HTTP, GET та POST від клієнтів. Ці запити оброблятимуться однаково. Таким чином, метод [doPost] може перенаправляти до методу [doGet] або навпаки. Запит клієнта оброблятиметься наступним чином:

  • буде перевірено поле [erreurs]. Якщо воно не порожнє, це означає, що під час ініціалізації додатка сталися помилки і він не може працювати. У такому разі у відповідь буде надіслано представлення [ERREURS].
  • параметр [action] запиту буде отримано та перевірено. Якщо він не відповідає жодній відомій дії, надсилається представлення [ERREURS] із відповідним повідомленням про помилку.
  • Якщо параметр [action] є дійсним, запит клієнта передається для обробки до процедури, специфічної для цієї дії. Процедура, що обробляє дію [uneAction], матиме такий підпис:
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. Обробка різних дій

Методи, що обробляють різні можливі дії додатка, є такими:

метод
запит
обробка
можливі відповіді
doListe
GET /main?action=liste
- запитати список статей
у бізнес-класі
- відобразити його
[LISTE] або [ERREURS]
doInfos
GET /main?action=infos&id=ID
- отримати статтю з id=ID з
бізнес-класу
- відобразити його
[INFOS] або [ERREURS]
doAchat
POST /main?action=achat&id=ID
- кількість, що була придбана, входить до параметрів, що передаються
- запитати товар з id=ID у
бізнес-класу
- додати його до кошика в
сесії клієнта
[LISTE] або [INFOS]
або [ERREURS]
doRetirerAchat
GET /main?action=retirerachat&id=ID
- видалити товар з id=ID зі
списку покупок у кошику
сесії клієнта
[PANIER]
doPanier
GET /main?action=panier
- відобразити кошик
сесії клієнта
[PANIER] або [PANIERVIDE]
doValidationPanier
GET /main?action=validationpanier
- зменшити в базі даних
залишки всіх товарів
, що знаходяться у кошику
сесії клієнта
[LISTE] або [ERREURS]

3.5.10.4. Код

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

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

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

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

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

     // приватні поля
    private 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() {
         // отримуємо параметри ініціалізації сервлету
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // зберігаємо інформацію про помилку
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // чи є помилки?
        if (erreurs.size() != 0) {
            return;
        }
         // створюється об’єкт IArticlesDomain для доступу до бізнес-шару
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // зберігаємо інформацію про помилку
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // зберігаються певні URL-адреси додатка
        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);

         // процес завершено
        return;
    }


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

         // перевіряється, як пройшла ініціалізація сервлету
        if (erreurs.size() != 0) {
             // чи є URL-адреса сторінки помилок?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // відображаємо сторінку помилок
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // кінець
            return;
        }
         // обробляємо дію
        String action = request.getParameter("action");
        if (action == null) {
             // список статей
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // список статей
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // інформація про товар
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // придбання товару
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // перегляд кошика
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // видалення товару з кошика
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // підтвердження кошика
            doValidationPanier(request, response);
            return;
        }
         // невідома дія
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // відображення сторінки помилок
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // кінець
        return;
    }


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

         // покупець підтвердив свій кошик
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // підтверджується цей кошик
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // ненормальний стан
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // отримуються помилки
        ArrayList erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
            request.setAttribute(
                "actions",
                new Hashtable[] { hActionListe, hActionPanier });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // відображається список товарів
        request.setAttribute("message", "Votre panier a été validé");
        doListe(request, response);
         // кінець
        return;
    }


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

         // вилучення товару з кошика
        try {
            Panier panier =
                (Panier) request.getSession().getAttribute("panier");
            String strIdAchat = request.getParameter("id");
            panier.enlever(Integer.parseInt(strIdAchat));
        } catch (NumberFormatException ignored) {
        } catch (NullPointerException ignored) {
        }
         // відображення кошика
        doPanier(request, response);
    }


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

         // відображення кошика
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // кошик порожній?
        if (panier == null || panier.getAchats().size() == 0) {
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
                .forward(request, response);
             // кінець
            return;
        }
         // у кошику є товари
        request.setAttribute("panier", panier);
        request.setAttribute(
            "actions",
            new Hashtable[] { hActionListe, hActionValidationPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_PANIER))
            .forward(request, response);
         // кінець
        return;
    }


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

         // придбання товару
         // отримано кількість
        int qté = 0;
        try {
            qté = Integer.parseInt(request.getParameter("qte"));
            if (qté <= 0)
                throw new NumberFormatException();
        } catch (NumberFormatException ex) {
             // неправильна кількість
            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);
             // кінець
            return;
        }
         // отримання сеансу клієнта
        HttpSession session = request.getSession();
         // створення покупки
        Article article = (Article) session.getAttribute("article");
        Achat achat = new Achat(article, qté);
         // додаємо покупку до кошика клієнта
        Panier panier = (Panier) session.getAttribute("panier");
        if (panier == null) {
            panier = new Panier();
            session.setAttribute("panier", panier);
        }
        panier.ajouter(achat);
         // повернення до списку товарів
        String url = config.getInitParameter(URL_MAIN) + "?action=liste";
        getServletContext().getRequestDispatcher(url).forward(
            request,
            response);
         // кінець
        return;
    }


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

         // відображається список товарів
        request.setAttribute("infos", infos);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_DEBUG))
            .forward(request, response);
         // кінець
        return;
    }


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

         // те саме, що get
        doGet(request, response);
    }


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

         // список помилок
        ArrayList erreurs = new ArrayList();
         // отримується запитуваний ідентифікатор
        String strId = request.getParameter("id");
         // щось?
        if (strId == null) {
             // ненормально
            erreurs.add("action incorrecte([infos,id=null]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // перетворюємо strId у ціле число
        int id = 0;
        try {
            id = Integer.parseInt(strId);
        } catch (Exception ex) {
             // ненормально
            erreurs.add("action incorrecte([infos,id=" + strId + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // запитується ідентифікатор ключа
        Article article = null;
        try {
            article=articlesDomain.getArticleById(id);
        } catch (UncheckedAccessArticlesException ex) {
             // ненормально
            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) {
             // ненормально
            erreurs.add("Article de clé [" + id + "] inexistant");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // запис додано до сесії
        request.getSession().setAttribute("article", article);
         // відображається інформаційна сторінка
        request.setAttribute("actions", new Hashtable[] { hActionListe });
         /   / request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_INFOS))
            .forward(request, response);
         // кінець
        return;
    }

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

         // відображається сторінка помилок
        request.setAttribute("erreurs", erreurs);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
            .forward(request, response);
         // кінець
        return;
    }


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

         // список помилок
        ArrayList erreurs = new ArrayList();
         // запит списку товарів
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // записування помилки
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // є помилки?
        if (erreurs.size() != 0) {
             // відображається сторінка помилок
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // кінець
            return;
        }
         // відображається список товарів
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // кінець
        return;
    }

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

Ми даємо читачеві час, щоб він міг прочитати та зрозуміти цей код. Сподіваємося, що коментарі допоможуть йому в цьому.

3.5.10.5. Тестування додатка

Покажемо кілька знімків екрана з тестів. По-перше, головна сторінка додатка:

Image

Запитана URL-адреса насправді була [http://localhost:8080/webarticles]. Читач побачить, що у файлі [web.xml] ми визначаємо головну сторінку додатка:

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

Вигляд [index.jsp] визначено наступним чином:

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

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

Отже, відбулося перенаправлення на URL-адресу [http://localhost:8080/webarticles/main?action=liste], про що свідчить URL-адреса браузера на знімку екрана. Отже, було зроблено запит на URL-адресу [/main?action=liste]. У [web.xml] URL-адреса /main пов’язана з сервлетом [webarticles]:

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

Також у [web.xml] сервлет [webarticles] пов’язаний із сервлетом [ istia.st.articles.web.WebArticles]:

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

Отже, сервлет [ istia.st.articles.web.WebArticles] завантажується контейнером сервлетів Tomcat, якщо він ще не був завантажений, і виконується його метод [init]:

    public void init() {
         // отримуються параметри ініціалізації сервлету
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // зберігається інформація про помилку
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // чи є помилки?
        if (erreurs.size() != 0) {
            return;
        }
         // створюється об’єкт IArticlesDomain для доступу до бізнес-шару
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // зберігаємо помилку
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // зберігаються певні URL-адреси додатка
        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);

         // процес завершено
        return;
    }

Коментарі: метод [init]

  • перевіряє наявність певних параметрів конфігурації
  • створює екземпляр служби доступу до домену додатка за допомогою Spring
  • заповнює список помилок, щоб вказати на можливі помилки ініціалізації
  • деякі приватні поля:
    • помилки: список помилок, виявлених [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: словники. Кожен із них містить інформацію, необхідну для відображення опції головного меню, яке відображається у вигляді [entete.jsp]
    • acticlesDomain: служба доступу до моделі додатка

Метод [init] виконується лише один раз — під час початкового завантаження сервлету. Далі виконується один із методів [doGet, doPost] залежно від типу [GET, POST] запиту клієнта. У даному випадку обидва методи виконують одну й ту саму дію, і код було розміщено в [doGet]:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
         // перевіряється, як пройшла ініціалізація сервлету
        if (erreurs.size() != 0) {
             // чи є URL-адреса сторінки помилок?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // відображаємо сторінку помилок
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // кінець
            return;
        }
         // обробляємо дію
        String action = request.getParameter("action");
        if (action == null) {
             // список статей
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // список статей
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // інформація про товар
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // придбання товару
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // перегляд кошика
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // видалення товару з кошика
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // підтвердження кошика
            doValidationPanier(request, response);
            return;
        }
         // невідома дія
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // відображається сторінка помилок
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // кінець
        return;
    }
  • метод [doGet] спочатку перевіряє, чи не сталося помилок ініціалізації після виконання методу [init]. Якщо так, він відображає представлення [ERREURS], і на цьому все.
  • В іншому випадку він отримує параметр [action] із запиту клієнта. Нагадаємо, що додаток було створено для обробки запитів, у яких обов’язково має бути присутній параметр [action].
  • Вона запускає метод, пов’язаний із цією дією. У даному випадку це буде метод [doListe].

Метод [doListe] має такий вигляд:

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

         // список помилок
        ArrayList erreurs = new ArrayList();
         // запит списку статей
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // записування помилки
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // є помилки?
        if (erreurs.size() != 0) {
             // відображається сторінка помилок
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // кінець
            return;
        }
         // відображається список товарів
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // кінець
        return;
    }
  • Нагадаємо, що метод [init] зберіг службу доступу до моделі додатка (доменний шар) у приватному полі сервлета:
     // приватні поля
    private IArticlesDomain articlesDomain = null;
  • За допомогою цієї служби доступу можна отримати список статей:
         // список помилок
        ArrayList erreurs = new ArrayList();
         // запит списку товарів
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // запис помилки
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
  • у разі виникнення помилок надсилається представлення [ERREURS]:
         // є помилки?
        if (erreurs.size() != 0) {
             // відображається сторінка помилок
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // кінець
            return;
        }
  • в іншому випадку надсилається погляд [LISTE]:
1
2
3
4
5
6
7
         // відображається список товарів
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);

У нашому прикладі все пройшло успішно, і ми отримали вигляд [LISTE]. Пропоную читачеві ще раз переглянути код подання [LISTE], щоб переконатися, що динамічні параметри, які очікує це подання, дійсно надані вище контролером. Таку саму перевірку слід проводити для кожного подання:

  • знайти динамічні параметри подання
  • переконатися, що контролер дійсно вказує їх в атрибутах запиту, що передається до подання

Тепер ми просто наведемо послідовність екранів, з якими стикається користувач додатка. Читачеві пропонується щоразу повторювати міркування, аналогічні попереднім:

Зі списку статей користувач може вибрати статтю:

Тут покупець може придбати товар № 3. Давайте зробимо помилку при введенні кількості:

Помилка була виявлена. Тепер придбаємо кілька товарів:

Покупка зареєстрована, список товарів оновлено. Перевіримо кошик:

Товар дійсно знаходиться у кошику. Вилучимо його:

Товар було вилучено з кошика, і кошик знову відобразився. Тут він порожній.

Купимо 100 одиниць товару № 3 та 2 одиниці товару № 4:

Придбати товар № 3 не вдалося, оскільки ми хотіли купити 100 штук, а на складі було лише 30. Цей товар залишився у кошику:

Товар № 4 було придбано, про що свідчить його новий залишок у кількості 39 (40-1):

3.6. Архітектура MVC із використанням Struts

3.6.1. Загальна архітектура додатка

Повернемося до архітектури MVC додатка:

У попередній версії:

  • функцію контролера виконував сервлет
  • відображення забезпечувалося сторінками JSP
  • модель забезпечувалася набором із трьох архівів .jar

У версії Struts:

  • контролер буде реалізовано за допомогою сервлета, похідного від загального сервлета [ActionServlet] з Struts
  • представлення забезпечуватимуться тими самими сторінками JSP, що й раніше, за винятком деяких дрібних деталей
  • модель буде реалізована тими самими трьома архівами

Ми побачимо, що перехід попереднього додатка на Struts полягає в таких завданнях:

  • дії, які раніше оброблялися в окремих методах сервлета/контролера, тепер обробляються екземплярами класів, похідних від класу [Action] з Struts
  • створити конфігураційні файли [web.xml] та [struts-config.xml]
  • внести деякі зміни до сторінок JSP

Нагадаємо загальну архітектуру MVC, яку використовує STRUTS:

M=modèle
бізнес-класи, класи доступу до даних та база даних
V=vues
сторінки JSP
C=contrôleur
сервлет для обробки запитів клієнтів, об’єкти [Action] та біни [ActionForm], пов’язані з формами.
  • контролер є серцем додатка. Усі запити клієнта проходять через нього. Це загальний сервлет, що надається STRUTS. У деяких випадках може знадобитися його похідна реалізація. У простих випадках це не потрібно. Цей загальний сервлет отримує необхідну інформацію з файлу, який найчастіше називається struts-config.xml.
  • Якщо запит клієнта містить параметри форми, контролер поміщає їх в об’єкт Bean. Об’єкти Bean, створені таким чином з часом, зберігаються в сесії або в запиті клієнта. Цей параметр можна налаштувати. Їх не потрібно створювати заново, якщо вони вже існують.
  • У конфігураційному файлі struts-config.xml до кожного URL, який має оброблятися програмою (тобто не відповідає представленню JSP, яке можна було б запросити безпосередньо), прив’язується певна інформація:
    • ім'я класу типу Action, відповідального за обробку запиту. І в цьому випадку інстанційований об'єкт Action може зберігатися в сесії або в запиті.
    • якщо запитуваний URL має параметри (наприклад, у разі надсилання форми до контролера), вказується ім’я біна, відповідального за збереження інформації з форми.
  • Маючи цю інформацію, надану його конфігураційним файлом, при отриманні запиту URL від клієнта контролер здатний визначити, чи потрібно створювати bean і який саме. Після створення екземпляра bean може перевірити, чи є дані, які він зберіг і які походять із форми, дійсними чи ні. Контролер автоматично викликає метод біна під назвою validate. Бін створюється розробником. Отже, розробник розміщує в методі validate код, що перевіряє правильність даних форми. Якщо дані виявляються недійсними, контролер не продовжує обробку. Він передає управління представленню, ім’я якого знайде у своєму файлі конфігурації. На цьому обмін завершується. Слід зазначити, що розробник може вимагати, щоб валідність форми не перевірялася. Він робить це також у файлі struts-config.xml. У цьому випадку контролер не викликає метод validate біна.
  • Якщо дані біна правильні, або якщо перевірка не проводиться, або якщо біна немає, контролер передає управління об’єкту типу Action, пов’язаному з URL. Він робить це, викликаючи метод `execute` цього об’єкта, якому передає посилання на бін, який він, можливо, створив. Саме тут розробник виконує необхідні дії: йому, можливо, доведеться викликати бізнес-класи або класи доступу до даних. По завершенні обробки об’єкт `Action` повертає контролеру назву подання, яке той повинен надіслати у відповідь клієнту.
  • У своєму конфігураційному файлі контролер знайде URL, пов’язаний з назвою подання, яке йому було доручено відобразити. Після цього він надсилає це подання. Обмін даними з клієнтом завершено.

У нашому додатку ми не будемо використовувати об’єкти [Bean] як проміжні об’єкти між клієнтом та класами [Action]. Об’єкт [Action] безпосередньо отримуватиме параметри запиту клієнта з об’єкта [HttpServletRequest], який він отримає. Це полегшує перенесення нашого початкового додатка. Отже, остаточна архітектура нашого додатка буде такою:

M=modèle
бізнес-класи, класи доступу до даних та база даних
V=vues
сторінки JSP
C=contrôleur
сервлет для обробки запитів клієнтів, об’єкти [Action]

3.6.2. Модель

Вона була представлена раніше. Складається з Java-архівів [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.6.3. Конфігурація додатка

3.6.3.1. Загальна архітектура

Загальна архітектура проекту Eclipse така:

Image

3.6.3.2. Налаштування доступу до даних

Оскільки інтерфейс доступу до даних не змінюється, відповідні файли конфігурації залишаються такими самими, як і в попередній версії. Вони визначені у файлі [WEB-INF/src]:

Image

На знімку екрана вище файли [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] належать до попередньої версії.

3.6.3.3. Каталог архівів

У [WEB-INF/lib] містяться ті самі архіви, що й у попередній версії, а також архів, необхідний для Struts:

Image

3.6.3.4. Налаштування додатка

Налаштування додатка здійснюється за допомогою двох файлів: [web.xml, struts-config.xml] у папці [WEB-INF]:

Image

Файл [web.xml] має такий вигляд:

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

Що міститься в цьому файлі?

  • Головна сторінка додатка — [vues/index.jsp] (welcome-file)
  • запити на URL-адреси у форматі *.do будуть перенаправлені на сервлет [strutswebarticles] (servlet-mapping)
  • сервлет [strutswebarticles] є екземпляром класу [ istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
  • Цей сервлет приймає два параметри ініціалізації
    • ім’я файлу конфігурації Struts (config)
    • ім'я файлу конфігурації Spring (springConfigFileName)

Файл [struts-config.xml] має такий вигляд:

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

Що вказано в цьому конфігураційному файлі?

  • що наш контролер оброблятиме такі URL-адреси:
main.do
для відображення списку статей
liste.do
для відображення списку статей
infos.do
для перегляду інформації про конкретний товар
achat.do
щоб придбати певний товар
panier.do
для перегляду кошика покупок
retirerachat.do
щоб видалити товар з кошика
validerpanier.do
для підтвердження кошика покупок
  • зазначені вище дії один в один відповідають діям, що оброблялися сервлетом у попередній версії. Для кожної з них вказано таку інформацію:
  • назва класу, відповідального за обробку цієї дії
  • можливі відповіді (= перегляди) після обробки дії. Контролер обирає лише одну з них.
  • ім’я файлу повідомлень для додатка (message-resources). У цьому випадку файл існуватиме, але буде порожнім. Він не використовуватиметься. Його слід розмістити у [ClassPath] додатка. Тут він буде розміщений у [WEB-INF/classes]. У середовищі Eclipse цей результат можна отримати, розмістивши його у [WEB-INF/src]:

Image

3.6.4. Види JSP

Використовувані подання JSP також є тими самими, що й у попередній версії. Змінено дуже мало: це URL-адреси у форматі [?action=XX?id=YY& ...], які стають [/XX.do?id=YY&....]. Ми повторюємо тут вже надані пояснення, щоб користувачеві не довелося повертатися назад. Важливо розуміти, що інформація, яка передається контролером до подання, є абсолютно однаковою в обох версіях. У цьому плані нічого не змінилося.

3.6.4.1. entete.jsp

Щоб забезпечити певну уніфікованість різних вікон, вони матимуть спільний заголовок, у якому відображається назва додатка разом із меню:

Меню є динамічним і визначається контролером. Контролер додає до запиту, що надсилається на сторінку JSP, атрибут-ключ «actions», значенням якого є масив типу Hastable[]. Кожен елемент цього масиву є словником, призначеним для генерації пункту меню у заголовку. Кожен словник має два ключі:

  • href: URL-адреса, пов’язана з пунктом меню
  • lien: текст меню

Інші подання додатка використовуватимуть заголовок, визначений [entete.jsp], за допомогою такого тегу JSP:

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

Під час виконання ця тег призведе до включення в код сторінки JSP, яка її містить, коду сторінки [entete.jsp]. Оскільки URL-адреса сторінки є відносною (без символу /), сторінка [entete.jsp] буде шукатися в тій самій папці, що й сторінка з тегом <jsp:include>.

Код:

<%@ 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.6.4.2. liste.jsp

Цей вигляд відображає список товарів, доступних для продажу:

Він відображається після запиту /main.do або /validerpanier.do. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню
listarticles
ArrayList об’єктів типу [Article]
message
об'єкт String — повідомлення, яке відображається внизу сторінки

Кожне посилання [Infos] у масиві HTML статей має URL-адресу у вигляді [/infos.do?id=ID], де ID — це поле id відображуваної статті.

Код:

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

Коментарі: одна зміна (виділена вище)

3.6.4.3. infos.jsp

Цей екран відображає інформацію про товар, а також дозволяє його придбати:

Image

Вона відображається після запиту /infos.do?id=ID або запиту /achat.do?id=ID, коли кількість, що купується, є неправильною. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню
article
об’єкт типу [Article] — товар, який потрібно відобразити
msg
об’єкт String — повідомлення, яке слід відобразити у разі помилки щодо кількості
qte
об'єкт String — значення, яке потрібно відобразити в полі введення [Qte]

Поля [msg] та [qte] використовуються у разі помилки при введенні кількості:

Image

Ця сторінка містить форму, яка відправляється за допомогою кнопки [Acheter]. Цільова URL-адреса POST — [/achat.do?id=ID], де ID — це ідентифікатор придбаного товару.

Код:

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

Коментарі: одна зміна (виділена вище)

3.6.4.4. panier.jsp

Цей вигляд відображає вміст кошика:

Image

Він відображається після запиту /panier.do або /retirerachat.do?id=ID. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню
panier
об’єкт типу [Panier] — кошик, який потрібно відобразити

Кожне посилання [Retirer] у масиві HTML, що містить товари з кошика, має URL-адресу у вигляді [retirerachat.do?id=ID], де ID — це поле [id]товару, який потрібно видалити з кошика.

Код:

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

Коментарі: одна зміна (виділена вище)

3.6.4.5. paniervide.jsp

На цьому екрані відображається інформація про те, що кошик порожній:

Image

Вона відображається після запиту /panier.do або /retirerachat.do?id=ID. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню

Код:

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.6.4.6. erreurs.jsp

Цей вигляд відображається у разі помилок:

Image

Вона відображається після будь-якого запиту, що призвів до помилки, за винятком дії купівлі з неправильною кількістю, яка обробляється у вікні [INFOS]. Елементи запиту контролера такі:

actions
об’єкт Hashtable[] — масив опцій меню
erreurs
ArrayList об’єктів типу String, що представляють повідомлення про помилки, які потрібно відобразити

Код:

<%@ 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.6.4.7. index.jsp

Ця сторінка визначена як головна сторінка додатка у файлі [web.xml] додатка:

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

Вигляд [index.jsp] просто перенаправляє клієнта до точки входу в додаток:

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

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

Коментарі: одна зміна (виділена вище)

3.6.5. Контролер Struts

Struts має загальний контролер під назвою [ActionServlet]. Відомо, що сервлет має метод [init], який дозволяє ініціалізувати додаток під час його запуску. Якщо ми використовуємо загальний контролер Struts [ActionServlet], ми не маємо доступу до його методу [init]. Тут нам потрібно виконати певні дії під час запуску додатка, а саме — створити екземпляр об’єкта для доступу до моделі. Тому нам потрібен метод [init]. Отже, ми похідний клас [Actionservlet] у наступному класі [MainServlet]:

package istia.st.articles.web.struts;

import istia.st.articles.domain.IArticlesDomain;

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

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

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

/**
 * @author ST - ISTIA
 *  
 */
public class MainServlet extends ActionServlet {
   // приватні поля
  private 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);

     // геттери — сеттери
  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{

     // ініціалізація батьківського класу
    super.init();
     // отримано параметри ініціалізації сервлета
    config = getServletConfig();
    String param = null;
    for (int i = 0; i < parameters.length; i++) {
      param = config.getInitParameter(parameters[i]);
      if (param == null) {
         // зберігаємо інформацію про помилку
        erreurs.add("Paramètre [" + parameters[i]
            + "] absent dans le fichier [web.xml]");
      }
    }
     // чи є помилки?
    if (erreurs.size() != 0) {
      return;
    }
     // створюється об’єкт IArticlesDomain для доступу до бізнес-шару
    try {
      articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
          new ClassPathResource((String) config
              .getInitParameter(SPRING_CONFIG_FILENAME))))
          .getBean("articlesDomain");
    } catch (Exception ex) {
       // зберігаємо помилку
      erreurs.add("Erreur de configuration de l'accès aux données : "
          + ex.toString());
      return;
    }
     // зберігаються певні URL-адреси додатка
    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);

     // процес завершено
    return;
  }
}

Коментарі:

  • цінність класу полягає в його методі [init] та його приватних полях
  • метод [init] виконує те саме, що й метод [init] контролера попередньої версії:
    • він перевіряє наявність певних параметрів конфігурації
    • створює екземпляр сервісу доступу до домену додатка за допомогою Spring
    • вона ініціалізує список помилок для вказівки можливих помилок ініціалізації
  • перед початком роботи метод [init] викликає метод [init] батьківського класу [ActionServlet]. Саме він оброблятиме файл конфігурації Struts [struts-config.xml].
  • Визначено певну кількість приватних полів із відповідними методами доступу:
    • помилки: список помилок, виявлених [init]
    • [hActionListe, hActionPanier, hActionValidationPanier]: словники. Кожен із них містить інформацію, необхідну для відображення пункту головного меню, що відображається поданням [entete.jsp]
    • acticlesDomain: служба доступу до моделі додатка
  • контролер додатка Struts доступний для класів [Action], які відповідають за обробку різних можливих дій. Ці класи матимуть доступ до вищезазначених приватних полів, оскільки вони мають публічні методи доступу.

Інстанціювання цього контролера забезпечується файлом [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>

Будь-який файл URL, що закінчується на .do, оброблятиметься екземпляром класу [istia.st.articles.web.struts.MainServlet]

3.6.6. Дії додатка Struts

3.6.6.1. Вступ

Кожна дія Struts буде об’єктом класу. У попередній версії кожна дія була об’єктом методу в контролері додатка. Написання класу [Action] найчастіше полягає в:

  • копіювання та вставлення методу, який використовувався у попередній версії
  • адаптації коду до конвенцій Struts

3.6.6.2. main.do, liste.do

Ці дві дії є ідентичними та визначені у [struts-config.xml] таким чином:

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>

Коли одна з цих дій [main.do, liste.do] виконується в браузері, отримуємо такий результат:

Image

Код класу [ListeArticlesAction] має такий вигляд:

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 {

     // сервлет контролю
    MainServlet mainServlet = (MainServlet) this.getServlet();

     // помилки ініціалізації?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // відображається сторінка помилок
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

     // об’єкт доступу до домену
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();

     // список помилок
    ArrayList erreurs = new ArrayList();
     // запит списку товарів
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // зберігається інформація про помилку
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
     // помилок?
    if (erreurs.size() != 0) {
       // відображається сторінка помилок
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // відображається список статей
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");
  }
}

Коментарі:

  • написання коду класу [Action] полягає, в основному, у написанні коду його методу [execute]
  • у екземплярі контролера збережено певну кількість інформації. Отримати посилання на нього можна за допомогою:
     // сервлет управління
    MainServlet mainServlet = (MainServlet) this.getServlet();
  • отримуємо список помилок ініціалізації, збережених контролером. Якщо цей список не порожній, клієнту надсилається представлення [ERREURS]:
1
2
3
4
5
6
7
8
     // помилки ініціалізації?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // відображається сторінка помилок
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

Вигляд, який фактично буде надіслано клієнту, надається нам [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>

Це вигляд [/vues/erreurs.jsp]. Читачеві пропонується перевірити, що очікується від цього вигляду. Ця інформація надається тут дією в об’єкті [request] у вигляді атрибутів.

  • Завдяки контролеру дія може отримати об’єкт доступу до моделі додатка (доменний шар):
     // об'єкт доступу до домену
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • Після цього можна отримати список статей:
     // список помилок
    ArrayList erreurs = new ArrayList();
     // запит списку товарів
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // записування помилки
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • у разі виникнення помилок надсилається представлення [ERREURS]:
1
2
3
4
5
6
7
8
     // помилок?
    if (erreurs.size() != 0) {
       // відображається сторінка помилок
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • в іншому випадку надсилається погляд [LISTE]:
1
2
3
4
5
6
     // відображається список статей
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");

Вигляд, який фактично буде надіслано клієнту, надається нам [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>

Це вигляд [/vues/liste.jsp]. Просимо читача перевірити, що очікується від цього вигляду. Ця інформація надається тут дією в об’єкті [request] у вигляді атрибутів.

3.6.6.3. infos.do

Ця дія призначена для надання інформації про один із елементів, що відображаються у поданні [LISTE]:

Ця дія налаштована в [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>

Код класу [InfosArticleAction] є таким:

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 {

     // сервлет контролю
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // помилки ініціалізації?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // відображається сторінка помилок
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // об'єкт доступу до домену
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // список помилок
    ArrayList erreurs = new ArrayList();
     // отримано запитуваний ідентифікатор
    String strId = request.getParameter("id");
     // щось?
    if (strId == null) {
       // ненормально
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // перетворюємо strId у ціле число
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // ненормально
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // запитується ідентифікатор ключа
    Article article = null;
    try {
      article = articlesDomain.getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // ненормально
      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) {
       // ненормально
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // запис додано до сесії
    request.getSession().setAttribute("article", article);
     // відображається інформаційна сторінка
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionListe() });
    return mapping.findForward("afficherInfosArticle");
  }
}

Коментарі:

  • початок методу [execute] ідентичний тому, що розглядався раніше. Це також стосується інших дій.
  • Метод отримує параметр [id], який зазвичай має бути в URL-адресі. Ця адреса має мати вигляд [/infos.do?id=X]. Проводяться різні перевірки для виявлення наявності та правильності параметра [id]. У разі виникнення проблеми надсилається представлення [ERREURS].
  • Якщо [id] є дійсним, відповідний товар запитується на рівні [domain]. Якщо на цьому рівні виникає виняток або товар не знайдено, знову надсилається представлення [ERREURS].
  • Якщо все гаразд, отриманий товар додається до сесії. Це спірне питання. Тут виходимо з того, що клієнт, можливо, придбає цей товар. Якщо він це зробить, ми будемо шукати його в сесії, а не запитувати знову на рівні [domain].
  • Нарешті, відображається вигляд [INFOS]. Вигляд, який фактично буде надіслано клієнту, надається нам [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>

Це вигляд [/vues/infos.jsp]. Пропоную читачеві перевірити, що саме очікується від цього вигляду. Ця інформація надається тут дією в об’єкті [request] у вигляді атрибутів.

3.6.6.4. achat.do

Ця дія призначена для придбання товару, відображеного у попередньому поданні [INFOS]:

Після придбання товару знову відображається вигляд [LISTE] (вигляд праворуч). Якщо розглянути код HTML з вигляду ліворуч вище, можна побачити, що тег <form> визначено таким чином:

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>

Отже, бачимо, що форма надсилається до контролера з дією [achat.do].

Ця дія налаштована в [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>

Код класу [AchatArticleAction] такий:

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 {

     // сервлет контролю
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // помилки ініціалізації?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // відображається сторінка помилок
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // список помилок для цієї дії
    ArrayList erreurs = new ArrayList();
     // отримано кількість, що була придбана
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // неправильна кількість
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
     // отримано сесію клієнта
    HttpSession session = request.getSession();
     // отримано товар, доданий до сесії
    Article article = (Article) session.getAttribute("article");
     // сесія закінчилася?
    if(article==null){
       // відображається сторінка помилок
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // створюється нова покупка
    Achat achat = new Achat(article, qté);
     // додаємо товар до кошика клієнта
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // повернення до списку товарів
    return mapping.findForward("afficherListeArticles");
  }
}

Коментарі:

  • початок методу [execute] ідентичний тим, що розглядалися раніше.
  • Нагадаємо форму формуляра, що надсилається до контролера:
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>
  • У запиті є два параметри: [id] — номер придбаного товару, [qte] — кількість, що була придбана.
  • Перевіряється наявність та правильність параметра [qte]. Якщо цей параметр визнано неправильним, користувачеві повертається представлення [INFOS] разом із повідомленням про помилку:
     // список помилок щодо цієї дії
    ArrayList erreurs = new ArrayList();
     // отримано кількість, що була придбана
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // неправильна кількість
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
  • придбаний товар отримується із сесії. Термін дії сесії міг закінчитися. У цьому випадку надсилається подання [ERREURS]:
     // отримано сесію клієнта
    HttpSession session = request.getSession();
     // отримано товар, доданий до сесії
    Article article = (Article) session.getAttribute("article");
     // сесія закінчилася?
    if(article==null){
       // відображається сторінка помилок
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
  • якщо сесія не закінчилася, товар додається до кошика, який також отримується з сесії:
     // отримуємо сесію клієнта
    HttpSession session = request.getSession();
     // завантажуємо статтю, додану до сесії
    Article article = (Article) session.getAttribute("article");
     // сесія закінчилася?
    if(article==null){
       // відображається сторінка помилок
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // створюється нова покупка
    Achat achat = new Achat(article, qté);
     // додаємо товар до кошика клієнта
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • нарешті, надсилається вигляд [LISTE]:
     // повернення до списку товарів
    return mapping.findForward("afficherListeArticles");
  • Вигляд, який фактично буде надіслано клієнту, надається нам [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>

Це подання [/main.do]. Це подання не є поданням, а дією. Отже, описана вище дія [/main.do] буде виконана та відобразить список статей.

3.6.6.5. panier.do

Ця дія призначена для відображення всіх покупок клієнта. Вона доступна через пункт меню [Voir le panier]:

Код HTML, пов'язаний із посиланням [Voir le panier], має такий вигляд:

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

Дія [panier.do] налаштована в [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>

Код класу [VoirPanierAction] такий:

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 {

     // сервлет управління
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // помилки ініціалізації?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // відображається сторінка помилок
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // відображення кошика
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // кошик порожній
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    } else {
       // у кошику є щось
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(), mainServlet.getHActionValidationPanier() });
      return mapping.findForward("afficherPanier");
    }
  }
}

Коментарі:

  • початок методу [execute] ідентичний тим, що розглядалися раніше.
  • кошик береться з сесії, в якій він зазвичай знаходиться. Сесія могла закінчитися, і тоді кошика немає. Цей випадок не розглядається як помилка, а просто вважається, що кошик порожній.
  • якщо кошик порожній, відображається сторінка [PANIERVIDE]
  • в іншому випадку — вигляд [PANIER]

Види, які фактично надсилаються клієнту, визначаються дією:

        <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

Ця дія призначена для видалення товару з кошика:

Після дії [retirerachat.do] кошик відображається знову (вигляд праворуч вище). Якщо розглянути код HTML посилання [Valider le panier] з вигляду ліворуч вище, отримаємо таке:

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

Отже, дія [retirerachat.do] отримує як параметр ідентифікатор товару, який потрібно видалити з кошика. Ця дія налаштована в [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>

Код класу [RetirerAchatAction] такий:

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 {

     // сервлет управління
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // помилки ініціалізації?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // відображається сторінка помилок
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // список помилок для цієї дії
    ArrayList erreurs = new ArrayList();
     // отримання кошика
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // сесія закінчилася
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    }
     // отримано ідентифікатор товару, який потрібно видалити
    String strId = request.getParameter("id");
     // щось?
    if (strId == null) {
       // ненормально
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // перетворюємо strId у ціле число
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // ненормально
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // видаляємо покупку
    panier.enlever(id);
     // знову відображаємо кошик
    return mapping.findForward("afficherPanier");
  }
}

Коментарі:

  • початок методу [execute] ідентичний тим, що розглядалися раніше.
  • наступний код призначений для перевірки наявності та правильності параметра [id]. Якщо він виявляється неправильним, надсилається представлення [ERREURS].
  • В іншому випадку товар видаляється з кошика:
     // видаляємо покупку
    panier.enlever(id);
  • після чого кошик відображається знову:
     // повторно відображається кошик
    return mapping.findForward("afficherPanier");

Вигляд, який фактично надсилається клієнту, визначається дією:

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

Як бачимо, насправді буде запущено дію [/panier.do]. Вона вже була описана. Вона відобразить перегляд [PANIER] або [PANIERVIDE] залежно від стану кошика.

3.6.6.7. validerpanier.do

Ця дія призначена для підтвердження покупок, зроблених клієнтом. Конкретно це означає одну єдину дію: у базі даних запаси придбаних товарів зменшуються на кількість, що була куплена. Ця дія доступна в наступному меню:

 

Код HTML посилання [Valider le panier] виглядає наступним чином:

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

При активації цього посилання запаси зменшуються, і список товарів знову відображається.

Ця дія налаштована в [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>

Код класу [ValiderPanierAction] такий:

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 {

     // сервлет контролю
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // помилки ініціалізації?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // відобразити сторінку помилок
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // об'єкт доступу до домену
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // список помилок для цієї дії
    ArrayList erreurs = new ArrayList();
         // покупець підтвердив свій кошик
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // сесія закінчилася
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
         // кошик підтверджено
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // ненормальний стан
            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");
        }
         // отримано можливі помилки
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
         // все виглядає OK — відображається список товарів
    return mapping.findForward("afficherListeArticles");
  }
}

Коментарі:

  • початок методу [execute] ідентичний тим, що розглядалися раніше.
  • з сесії витягується кошик. Якщо термін дії сесії закінчився, надсилається представлення [ERREURS]:
     // список помилок щодо цієї дії
    ArrayList erreurs = new ArrayList();
         // покупець підтвердив свій кошик
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // сесія закінчилася
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • підтверджуються покупки з кошика. Можуть виникнути помилки, якщо запасів недостатньо для задоволення покупок. У цьому випадку надсилається вигляд [ERREURS]:
         // кошик підтверджено
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // аномалія
            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");
        }
         // отримано можливі помилки
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
  • якщо все пройшло успішно, знову відображається список товарів:
         // все виглядає OK — відображаємо список товарів
    return mapping.findForward("afficherListeArticles");

Вигляд, який фактично надсилається клієнту, визначається дією:

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

Як бачимо, насправді буде запущено дію [/main.do]. Вона вже була описана. Ця дія відобразить вигляд [LISTE].

3.7. Архітектура MVC із використанням Spring

3.7.1. Загальна архітектура додатка

Повернемося до архітектури MVC додатка:

У першій версії:

  • функцію контролера виконував сервлет
  • відображення забезпечувалося сторінками JSP
  • модель забезпечувалася набором із трьох архівів .jar

У версії Struts:

  • контролер забезпечувався сервлетом, похідним від загального сервлета [ActionServlet] Struts
  • представлення забезпечувалися тими самими сторінками JSP, що й у версії [Struts]
  • модель забезпечувалася тими самими трьома архівами

У версії Spring:

  • контролер буде реалізовано за допомогою сервлета, наданого Spring, — [DispatcherServlet]
  • види забезпечуватимуться тими самими сторінками JSP, що й раніше, за винятком деяких дрібниць
  • модель буде реалізована тими самими трьома архівами

Ми побачимо, що перехід нашого додатка зі Struts на Spring є простим, якщо погодитися не використовувати всі елементи, рекомендовані для ортодоксальної архітектури Spring MVC. Основні зміни такі:

  • дії, які в Struts оброблялися в окремих методах сервлета/контролера або екземплярами класів, похідних від класу [Action], тепер обробляються екземплярами класів, що реалізують інтерфейс Spring [Controller]
  • необхідні конфігураційні файли тепер мають такий вигляд:
    • [web.xml], оскільки ми маємо справу з веб-додатком. Цей файл містить Listener, який під час ініціалізації додатка буде використовувати файл [applicationContext.xml]
    • [applicationContext.xml], що дозволить створити біни, необхідні додатку, зокрема бін сервісу доступу до моделі
  • види JSP будуть ідентичними тим, що використовуються в Struts. Нам доведеться створити новий.

Нагадаємо архітектуру MVC STRUTS, яка використовувалася в попередній версії:

M=modèle
бізнес-класи, класи доступу до даних та база даних
V=vues
сторінки JSP
C=contrôleur
сервлет для обробки запитів клієнтів, об’єкти [Action]

У Spring ми використовуємо таку саму архітектуру:

M=modèle
бізнес-класи, класи доступу до даних та база даних
V=vues
сторінки JSP
C=contrôleur
сервлет для обробки запитів клієнтів, об’єкти, що реалізують інтерфейс [Controller]
  • контролер є серцевиною додатка. Усі запити клієнта проходять через нього. Це загальний сервлет, що надається SPRING. Він належить до типу [DispatcherServlet]. Відтепер ми будемо називати цей контролер контролером [Spring].
  • Контролер [Spring] спрямує запит клієнта до одного з екземплярів типу [Controller]. Для кожної дії, що підлягає обробці, буде існувати один такий екземпляр. Він визначається у запитуваному URL-адресі, як і в Struts. Таким чином, ми знатимемо, що запитувана дія — це дія [liste], оскільки запитуваний URL-адреса — [liste.do]
  • якщо C — це контекст додатка, контролер [Spring] використовує файл [C-servlet.xml], який виконує роль конфігураційного файлу struts-config.xml у версії Struts. Кожній дії, яку має обробити додаток, прив’язується ім’я класу типу Controller, відповідального за обробку запиту.
  • контролер передає управління об’єкту типу Controller, пов’язаному з цією дією. Для цього він викликає метод handleRequest цього об’єкта, передаючи йому запит клієнта. Саме тут розробник виконує необхідні дії: за потреби йому доведеться звертатися до бізнес-класів або класів доступу до даних. По завершенні обробки об’єкт Controller повертає контролеру назву подання, яке той має надіслати клієнту у відповідь.
  • У своєму конфігураційному файлі контролер знайде URL, пов’язаний з назвою подання, яке йому було доручено відобразити. Після цього він надсилає це подання. Обмін даними з клієнтом завершено.

3.7.2. Модель

Вона така сама, як і в двох попередніх версіях. Складається з Java-архівів [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

3.7.3. Конфігурація додатка

3.7.3.1. Загальна архітектура

Загальна архітектура проекту Eclipse така:

Image

3.7.3.2. Налаштування доступу до даних

Оскільки інтерфейс доступу до даних не змінюється, відповідні файли конфігурації залишаються такими самими, як і в попередній версії. Вони визначені у файлі [WEB-INF/src]:

Image

На знімку екрана вище файли [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] належать до попередніх версій.

3.7.3.3. Каталог архівів

У [WEB-INF/lib] містяться ті самі архіви, що й у попередній версії, за винятком архівів Struts, які більше не потрібні:

Image

3.7.3.4. Налаштування додатка

Налаштування додатка здійснюється за допомогою трьох файлів: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] у папці [WEB-INF]:

Image

Файл [web.xml] має такий вигляд:

<?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>
     <!-- завантажувач контексту Spring для додатка -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- сервлет -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- маппінг URL-адрес -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- вхідний документ -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

Що міститься в цьому файлі?

  • Головна сторінка додатка — [/vues/index.jsp] (welcome-file)
  • запити на URL-адреси у форматі *.do будуть перенаправлені на сервлет [springwebarticles] (servlet-mapping)
  • сервлет [springwebarticles] є екземпляром класу [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class), наданого Spring.
  • Слухач [org.springframework.web.context.ContextLoaderListener] запускатиметься під час запуску додатка. Його основною функцією буде створення екземплярів бінів Spring, визначених у файлі [applicationContext.xml]

Файл [applicationContext.xml] має такий вигляд:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- клас доступу до даних -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- бізнес-клас -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- конфігурація веб-додатку-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Тут є як знайомі, так і менш знайомі елементи. Під час ініціалізації додатка буде створено три біни:

  • articlesDao: сервіс доступу до шару [dao]
  • articlesDomain: служба доступу до моделі
  • config: bean, у якому ми будемо збирати інформацію, якою повинні ділитися всі клієнти. Цей bean виконуватиме роль, яку зазвичай виконує контекст додатка, але з типізованою, а не нетипізованою інформацією.

Останній файл [springwebarticles.xml] визначає дії, які приймає додаток, у спосіб, дуже схожий на той, що використовується у файлі Struts [struts-config.xml]. Його вміст такий:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- обробники дій = контролери -->
    <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>

     <!-- маппінг додатка-->
    <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>

     <!-- обробник подання -->
    <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>

     <!-- файл повідомлень -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>

Що вказує цей файл конфігурації?

  • що наш контролер оброблятиме такі URL-адреси:
main.do
для відображення списку статей
liste.do
для відображення списку статей
infos.do
для перегляду інформації про конкретний товар
achat.do
щоб придбати певний товар
panier.do
для перегляду кошика покупок
retirerachat.do
щоб видалити товар з кошика
validerpanier.do
для підтвердження кошика
  • попередні дії один в один відповідають діям, що оброблялися контролером у попередніх версіях. Для кожної з них вказано ім'я класу, відповідального за її обробку. Розглянемо приклад дії [/panier.do]:
  • вона має оброблятися біном [VoirPanierController]. Ця назва є довільною. Це просто ключ.
                <prop key="/panier.do">VoirPanierController</prop>
  • ключ [VoirPanierController] — це ім’я біна, визначеного в тому самому файлі конфігурації:
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
  • біан [VoirPanierController] визначає:
  • клас [ istia.st.articles.web.spring.VoirPanierController], який потрібно інстанціювати для обробки дії
  • як створити його екземпляр. Тут як параметр передається бін [config], визначений [applicationContext.xml] та інстанційований під час запуску додатка. Це буде зроблено для всіх дій [Controller]. Таким чином, кожна з них матиме у приватному полі об’єкт [config], у якому міститься вся інформація, спільна для всіх клієнтів.
  • Спосіб, у який мають вирішуватися імена переглядів:
     <!-- менеджер подання -->
    <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>

Як і у випадку зі Struts, екземпляр [Controller], який обробляє дію, після обробки поверне ключ контролеру Spring, щоб вказати йому, який вигляд він повинен відобразити. На основі цього ключа можуть існувати різні стратегії для генерації подання, пов’язаного з цим ключем. Обраною стратегією є та, що визначена біном [viewResolver]. Тут цей бін пов’язаний із класом [org.springframework.web.servlet.view.InternalResourceViewResolver] із різними параметрами ініціалізації. Не вдаючись у подробиці, бін [viewResolver] тут вказує, що якщо ключ подання — «XX», то згенерованим поданням буде [/vues/XX.jsp]. Тип подань, що надсилаються клієнту, можна змінити різними способами:

  • змінивши клас реалізації для біна [viewResolver]
  • змінивши параметри ініціалізації класу реалізації

Таким чином, можна перейти від подання HTML до подання XML, просто змінивши значення біна [viewResolver]

  • — ім’я файлу повідомлень для додатка (messageSource). У цьому випадку файл існуватиме, але буде порожнім. Він не використовуватиметься. Його слід розмістити в [ClassPath] додатка. Тут він буде розміщений у [WEB-INF/classes]. У середовищі Eclipse цей результат можна отримати, розмістивши його в [WEB-INF/src]:

Image

3.7.4. Види JSP

Використовуватимуться перегляди JSP, які застосовує Struts. Жоден з них не змінюється:

Image

Створюється лише один новий вигляд: redirpanier.jsp. Він служить виключно для перенаправлення клієнта до дії [/panier.do]. Його код такий:

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

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

Читачеві пропонується ще раз ознайомитися з визначенням різних подань у версії Struts.

3.7.5. Обробка дій

Класи, необхідні для обробки різних дій, зібрано в пакеті [istia.st.articles.web.spring]:

Image

Нагадаємо механізм роботи додатка Spring на прикладі:

  • користувач надсилає запит на URL-адресу [http://localhost:8080/springwebarticles/main.do]

Image

Що сталося?

  • був відкритий файл [web.xml] з додатка [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>
     <!-- завантажувач контексту Spring для додатка -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- сервлет -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- маппінг URL-адрес -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- вхідний документ -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  • якщо це був перший запит до додатка, відбулася низка подій:
    • був завантажений прослуховувач [ org.springframework.web.context.ContextLoaderListener]
    • він використав файл конфігурації [applicationContext.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- клас доступу до даних -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- бізнес-клас -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- конфігурація веб-додатку-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>
  • вищезазначені біни були створені в контексті додатка
  • потім було використано файл [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>
     <!-- обробники дій = контролери -->
    <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>

     <!-- маппінг додатка-->
    <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>

     <!-- обробник подання -->
    <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>

     <!-- файл повідомлень -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>
  • біни [Controller], визначені цим файлом, також були створені
  • тепер усе готово для обробки запиту клієнта. Він мав вигляд: [http://localhost:8080/springwebarticles]. Тут запитується не URL з контексту, а сам контекст. Тому використовується розділ [welcome-file] з файлу [web.xml].
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
  • Вигляд [index.jsp] такий:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>
  • отже, клієнту пропонується перейти за URL-адресою [http://localhost:8080/springwebarticles/main.do]. Він це робить.
  • Отже, контролер Spring отримує новий запит. Він використовує свій файл [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>
     <!-- обробники дій = контролери -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

     <!-- маппінг додатка-->
    <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>
  • Цей файл вказує, що дію [/main.do] має обробити бін [ListController].
  • Запит клієнта передається до методу [handleRequest] біна [ListController]. Цей метод виконує свою роботу та повертає контролеру ключ від подання, яке потрібно відобразити. У цьому випадку, якщо все йде добре, цим ключем буде [liste].
  • Контролер Spring використовує бін [viewResolver] із файлу конфігурації [springwebarticles-servlet.xml], щоб визначити вигляд, пов’язаний із цим ключем. У цьому випадку це буде вигляд [/vues/liste.jsp]
  • Вигляд [/vues/liste.jsp] надсилається клієнту

3.7.6. Ініціалізація додатка Spring

Ми вже зазначали, що під час запуску додатка створюються екземпляри бінів із файлу [applicationContext.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- клас доступу до даних -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- бізнес-клас -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- конфігурація веб-додатку-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

Нам відомі біни [articlesDao, articlesDomain], але не бін [config]. Останній визначається наступним класом Java:

package istia.st.articles.web.spring;

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

/**
 * @author ST - ISTIA
 */

public class Config {

     // приватні поля
    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);

     // методи getter-setter
    public IArticlesDomain getArticlesDomain() {
        return articlesDomain;
    }

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

    public Hashtable getHActionListe() {
        return hActionListe;
    }

    public Hashtable getHActionPanier() {
        return hActionPanier;
    }

    public Hashtable getHActionValidationPanier() {
        return hActionValidationPanier;
    }

     // ініціалізація веб-додатку
    public void init() {
         // зберігаємо певні URL-адреси додатка
        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);

         // все готово
        return;
    }
}

Цей клас виконує те саме, що й метод [init] сервлета веб-додатку. Він ініціалізує додаток. У даному випадку це відбувається наступним чином:

  • оскільки бін [config] визначений у [applicationContext.xml] наступним чином:
     <!-- конфігурація веб-додатку-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>

під час створення його приватне поле [articlesDomain] було ініціалізовано

  • потім, завдяки атрибуту [init-method="init"] вищезазначеного біна, виконується метод [init] класу, пов’язаного з цим біном. Тут він ініціалізує три словники [hActionListe, hActionPanier, hActionValidationPanier], які використовуються для генерації трьох можливих посилань у меню, що пропонується користувачеві.
  • Створюються публічні методи доступу, щоб зробити ці приватні поля доступними для екземплярів типу [Controller], які оброблятимуть дії.

3.7.7. Дії [Controller] у додатку Spring

3.7.7.1. Вступ

Кожна дія Spring буде реалізована класом типу [Controller]. У версії Struts кожна дія реалізовувалася класом типу [Action]. Написання класу [Controller] найчастіше полягає в:

  • скопіювання та вставлення класу [Action], який використовувався у версії Struts,
  • адаптації коду до конвенцій Spring

3.7.7.2. main.do, liste.do

Ці дві дії є ідентичними та визначені в [springwebarticles-servlet.xml] наступним чином:

     <!-- маппінг додатка-->
    <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>

Вони пов’язані з класом [ istia.st.articles.web.spring.ListController], який ми незабаром детально розглянемо. Коли одна з цих дій виконується в браузері, отримуємо такий результат:

Image

Код класу [istia.st.articles.web.spring.ListController] такий:

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 {

     // конфігурація веб-додатку
    Config config;

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

     // обробка запиту
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

         // запит списку товарів
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // зберігання інформації про помилку
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // відображається сторінка помилок
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // надіслати перегляд помилок
            return new ModelAndView("erreurs");
        }
         // відображається список статей
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // надіслати перегляд
        return new ModelAndView("liste");
    }

}

Коментарі:

  • клас має приватне поле [config]. Це поле було ініціалізовано Spring під час створення екземпляра біна [ListController]:
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

Як бачимо вище, поле [config] у [ListController] ініціалізується за допомогою біна [config]. Що це таке? Це бін [config], визначений у [applicationContext.xml], c.a.d — екземпляр [istia.st.articles.web.spring.Config], описаний вище.

  • Написання коду класу [Controller] полягає, по суті, у написанні коду його методу [handleRequest]
  • з моделі запитується список статей. Доступ до неї здійснюється через [config.getArticlesDomain()]. У разі виникнення винятку надсилається представлення [ERREURS]. Результат, що повертається методом [handleRequest], повинен бути типу [ModelAndView]. Цей клас можна інстанціювати різними способами. Тут, як і в усіх інших випадках, створюється екземпляр класу [ModelAndView], передаючи йому ключ подання, яке потрібно відобразити. Нагадаємо, що згідно з конфігурацією біна [viewResolver] запит на відображення подання з ключем XX призведе до відправлення подання [/vues/XX.jsp].
         // запит списку статей
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // зберігається інформація про помилку
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // відображається сторінка помилок
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // надіслати перегляд помилок
            return new ModelAndView("erreurs");
        }
  • якщо помилок немає, надсилається представлення [LISTE]:
1
2
3
4
5
6
7
         // відображається список товарів
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // надіслати перегляд
        return new ModelAndView("liste");

3.7.7.3. infos.do

Ця дія призначена для надання інформації про один із елементів, що відображаються у поданні [LISTE]:

Ця дія визначена в [springwebarticles-servlet.xml] наступним чином:

     <!-- маппінг додатка-->
    <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>

Код класу [InfosController] такий:

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 {

     // конфігурація веб-додатку
    Config config;

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

     // обробка запиту
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

     // список помилок
    ArrayList erreurs = new ArrayList();
     // отримано запитуваний ідентифікатор
    String strId = request.getParameter("id");
     // щось таке?
    if (strId == null) {
       // ненормально
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // перетворення strId у ціле число
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // ненормально
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // запитується ідентифікатор ключа
    Article article = null;
    try {
      article = config.getArticlesDomain().getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // ненормально
      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) {
       // ненормально
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // запис додано до сесії
    request.getSession().setAttribute("article", article);
     // відображається інформаційна сторінка
    request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
        return new ModelAndView("infos");
    }
}

Коментарі:

  • метод [handleRequest] отримує параметр [id], який зазвичай має бути в URL-адресі. Ця адреса має мати вигляд [/infos.do?id=X]. Проводяться різні перевірки для виявлення наявності та правильності параметра [id]. У разі проблеми надсилається запит [ERREURS].
  • Якщо [id] є дійсним, відповідний товар запитується на рівні [domain]. Якщо на цьому рівні виникає виняток або товар не знайдено, знову надсилається представлення [ERREURS].
  • Якщо все гаразд, отриманий товар додається до сесії. Це спірне питання. Тут виходимо з того, що клієнт, можливо, придбає цей товар. Якщо він це зробить, ми будемо шукати його в сесії, а не запитувати знову на рівні [domain].
  • Нарешті, відображається вікно [INFOS].

3.7.7.4. achat.do

Ця дія призначена для купівлі товару, відображеного в попередньому вигляді [INFOS]:

Image

Якщо розглянути код HTML цього виду, можна побачити, що тег <form> визначено таким чином:

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

Отже, бачимо, що форма надсилається до контролера за допомогою дії [achat.do].

Ця дія налаштована в [springwebarticles-servlet.xml] наступним чином:

     <!-- маппінг додатка-->
    <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>

Код класу [AchatController] такий:

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 {

   // конфігурація веб-додатку
  Config config;

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

   // обробка запиту
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // список помилок для цієї дії
    ArrayList erreurs = new ArrayList();
     // отримано кількість, що була придбана
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // неправильна кількість
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
     // отримано сесію клієнта
    HttpSession session = request.getSession();
     // отримано товар, доданий до сесії
    Article article = (Article) session.getAttribute("article");
     // сесія закінчилася?
    if (article == null) {
       // відображається сторінка помилок
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // створюється нова покупка
    Achat achat = new Achat(article, qté);
     // додаємо товар до кошика клієнта
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // повернення до списку товарів
    return new ModelAndView("index");
  }
}

Коментарі:

  • нагадаємо форму бланка, що надсилається контролеру:
        <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>
  • У запиті є два параметри: [id] — номер придбаного товару, [qte] — кількість, що була придбана.
  • Перевіряється наявність та правильність параметра [qte]. Якщо цей параметр визнано неправильним, користувачеві повертається вигляд [INFOS] разом із повідомленням про помилку:
     // список помилок щодо цієї дії
    ArrayList erreurs = new ArrayList();
     // отримано кількість, що була придбана
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // неправильна кількість
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
  • придбаний товар отримується із сесії. Термін дії сесії міг закінчитися. У цьому випадку надсилається подання [ERREURS]:
     // отримано сесію клієнта
    HttpSession session = request.getSession();
     // отримано товар, доданий до сесії
    Article article = (Article) session.getAttribute("article");
     // сесія закінчилася?
    if (article == null) {
       // відображається сторінка помилок
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • якщо сесія не закінчилася, товар додається до кошика, який також отримується з сесії:
1
2
3
4
5
6
7
8
9
     // створюється нова покупка
    Achat achat = new Achat(article, qté);
     // додаємо товар до кошика клієнта
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • нарешті, надсилається вигляд [LISTE]:
     // повернення до списку товарів
    return new ModelAndView("index");
  • Вище наведено відправлення сторінки [/vues/index.jsp]. Відомо, що вона вимагає від браузера клієнта перенаправлення на URL-адресу [/main.do]. Саме це перенаправлення відобразить список товарів.

3.7.7.5. panier.do

Ця дія призначена для відображення всіх покупок клієнта. Вона доступна через меню:

Image

Код HTML, пов'язаний із наведеним вище посиланням, має такий вигляд:

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

Сторінка, на яку веде це посилання, виглядає так:

Image

Ця дія налаштована в [springwebarticles-servlet.xml] наступним чином:

     <!-- маппінг додатка-->
    <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>

Код класу [VoirPanierController] є таким:

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 {

   // конфігурація веб-додатку
  Config config;

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

   // обробка запиту
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // відображення кошика
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // кошик порожній
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("paniervide");
    } else {
       // у кошику є щось
      request.setAttribute("actions", new Hashtable[] {
          config.getHActionListe(), config.getHActionValidationPanier() });
      return new ModelAndView("panier");
    }
  }
}

Коментарі:

  • кошик береться з сесії, в якій він зазвичай знаходиться. Термін дії сесії міг закінчитися, і тоді кошика немає. Цей випадок не розглядається як помилка, а просто вважається, що кошик порожній.
  • якщо кошик порожній, відображається сторінка [PANIERVIDE]
  • в іншому випадку — вигляд [PANIER]

3.7.7.6. retirerachat.do

Ця дія призначена для видалення товару з кошика:

Image

Якщо розглянути код HTML з посилання вище, то отримаємо таке:

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

Отже, дія [retirerachat.do] отримує як параметр ідентифікатор товару, який потрібно видалити з кошика. Ця дія налаштована в [springwebarticles-servlet.xml] наступним чином:

     <!-- маппінг додатка-->
    <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>

Код класу [RetirerAchatController] такий:

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 {

   // конфігурація веб-додатку
  Config config;

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

   // обробка запиту
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // список помилок цієї дії
    ArrayList erreurs = new ArrayList();
     // отримання кошика
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // сесія закінчилася
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // отримано ідентифікатор товару, який потрібно видалити
    String strId = request.getParameter("id");
     // щось?
    if (strId == null) {
       // ненормально
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // перетворюємо strId на ціле число
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // ненормально
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // видаляємо покупку
    panier.enlever(id);
     // знову відображаємо кошик
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");
  }
}

Коментарі:

  • код перевіряє наявність та правильність параметра [id]. Якщо він виявляється неправильним, надсилається подання [ERREURS].
  • в іншому випадку товар видаляється з кошика:
     // видаляємо покупку
    panier.enlever(id);
  • після чого кошик відображається знову:
     // повторно відображається кошик
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");

Нагадаємо код подання [/vues/redirpanier.jsp]:

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

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

Бачимо, що клієнт буде перенаправлений до дії [/panier.do]. Ця дія вже була описана. Вона відобразить вигляд [PANIER] або [PANIERVIDE] залежно від стану кошика.

3.7.7.7. validerpanier.do

Ця дія служить для підтвердження покупок, зроблених клієнтом. Конкретно це означає єдину дію: у базі даних запаси придбаних товарів зменшуються на кількість, що була придбана. Ця дія вибирається з наступного меню:

Image

Код HTML для посилання [Valider le panier] є таким:

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

Коли це посилання активовано, запаси зменшуються, і список товарів знову відображається.

Ця дія налаштована в [springwebarticles-servlet.xml] наступним чином:

     <!-- маппінг додатка-->
    <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>

Код класу [ValiderPanierController] має такий вигляд:

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 {

   // конфігурація веб-додатку
  Config config;

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

   // обробка запиту
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // список помилок цієї дії
    ArrayList erreurs = new ArrayList();
     // покупець підтвердив свій кошик
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // сесія закінчилася
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // кошик підтверджено
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // ненормальний стан
      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");
    }
     // отримано можливі помилки
    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");
    }
     // все виглядає OK — відображається список товарів
    return new ModelAndView("index");
  }
}

Коментарі:

  • кошик отримується з сесії. Якщо термін дії сесії закінчився, надсилається екран [ERREURS]:
     // список помилок щодо цієї дії
    ArrayList erreurs = new ArrayList();
     // покупець підтвердив свій кошик
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // сесія закінчилася
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • підтверджуємо покупки з кошика. Можуть виникнути помилки, якщо запасів недостатньо для виконання замовлення. У цьому випадку надсилається вигляд [ERREURS]:
     // підтверджується кошик
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // ненормальний стан
      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");
    }
     // отримано можливі помилки
    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");
    }
  • якщо все пройшло успішно, знову відображається список товарів:
     // все виглядає OK — відображається список товарів
    return new ModelAndView("index");

Відомо, що представлення [/vues/index.jsp] перенаправляє клієнта до дії [/main.do]. Ця дія відобразить представлення [LISTE].