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/: DBMS، برنامج تشغيل JDBC. في الواقع، أي مصدر JDBC سيفي بالغرض.
  • IBExpert، الإصدار الشخصي: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (مارس 2005). يتيح لك IBExpert إدارة نظام إدارة قواعد البيانات 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

نود هنا أن نقدم بعض مكونات تطبيق ويب للتجارة الإلكترونية. سيسمح هذا التطبيق لعملاء الويب

  • بعرض قائمة بالعناصر من قاعدة البيانات
  • إضافة بعضها إلى عربة التسوق الإلكترونية
  • تأكيد سلة التسوق. سيؤدي هذا التأكيد ببساطة إلى تحديث مستويات المخزون للمنتجات التي تم شراؤها في قاعدة البيانات.

وستكون طرق العرض المختلفة المقدمة للمستخدم على النحو التالي:

  • طريقة العرض [LIST]، التي تعرض قائمة بالسلع المعروضة للبيع

Image

  • عرض [INFO]، الذي يوفر معلومات إضافية عن المنتج:

Image

  1. عرض [العربة]، الذي يعرض محتويات عربة التسوق الخاصة بالعميل

Image

  1. عرض [عربة التسوق فارغة]، في حالة ما إذا كانت عربة التسوق فارغة

Image

  1. عرض [الأخطاء]، الذي يبلغ عن أي أخطاء في التطبيق

Image

3.2. البنية العامة للتطبيق

نريد إنشاء تطبيق بهيكل ثلاثي المستويات كما يلي:

  • تتمتع الطبقات الثلاث بالاستقلالية من خلال استخدام واجهات Java
  • يتم التعامل مع تكامل الطبقات المختلفة بواسطة Spring
  • توجد كل طبقة في حزم منفصلة: web (طبقة واجهة المستخدم)، domain (طبقة الأعمال)، و DAO (طبقة الوصول إلى البيانات).

سنفترض هنا أن طبقتي [domain] و[DAO] موجودتان بالفعل. وسنركز فقط على طبقة [web]، التي نقترح بناؤها بعدة طرق:

  • باستخدام تقنية وحدة التحكم في السيرفلت الكلاسيكية — صفحات JSP
  • باستخدام تقنية Struts MVC
  • باستخدام تقنية Spring MVC

في جميع الحالات، سيتبع التطبيق بنية MVC (نموذج-عرض-وحدة تحكم). إذا رجعنا إلى الرسم التخطيطي الطبقي أعلاه، فإن بنية MVC تتناسب معه على النحو التالي:

تتبع معالجة طلب العميل الخطوات التالية:

  1. يرسل العميل طلبًا إلى وحدة التحكم. وحدة التحكم هذه هي سيرفلت تتولى معالجة جميع طلبات العملاء. وهي نقطة الدخول إلى التطبيق. وهي تمثل الحرف C في نموذج MVC.
  2. يقوم وحدة التحكم بمعالجة هذا الطلب. وللقيام بذلك، قد تحتاج إلى مساعدة من طبقة الأعمال، المعروفة باسم النموذج M في بنية MVC.
  3. يتلقى وحدة التحكم استجابة من طبقة الأعمال. تمت معالجة طلب العميل. قد يؤدي هذا إلى عدة استجابات محتملة. ومن الأمثلة الكلاسيكية على ذلك
    • صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
    • صفحة تأكيد في الحالات الأخرى
  4. يختار وحدة التحكم الاستجابة (= العرض) التي سيتم إرسالها إلى العميل. غالبًا ما تكون هذه صفحة تحتوي على عناصر ديناميكية. توفر وحدة التحكم هذه العناصر للعرض.
  5. يتم إرسال العرض إلى العميل. وهذا هو الحرف V في MVC.

3.3. النموذج

هنا ندرس حرف M في 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
);


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

/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
id
المفتاح الأساسي الذي يحدد العنصر بشكل فريد
الاسم
اسم العنصر
السعر
سعره
المخزون الحالي
المخزون الحالي
الحد الأدنى للمخزون
مستوى المخزون الذي يجب عنده إعادة الطلب

في الاختبارات التالية، تم استخدام قاعدة بيانات [Firebird]. [Firebird] هو نظام إدارة قواعد البيانات "مفتوح المصدر". يتم وضع برنامج تشغيل 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]
الأرشيف
المحتوى
role
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]. هذه هي الواجهة الوحيدة المرئية لطبقة الويب. ولا ترى أي واجهات أخرى.
- [ArticlePurchases]: فئة تنفذ [IArticlesDomain]
- [Purchase]: فئة تمثل عملية شراء العميل
- [ShoppingCart]: فئة تمثل إجمالي مشتريات العميل
تمثل نموذج الشراء عبر الويب - تقع بالكامل داخل طبقة [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) {
     // init instance attributes
    setId(id);
    setNom(nom);
    setPrix(prix);
    setStockActuel(stockActuel);
    setStockMinimum(stockMinimum);
  }

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

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

  public String getNom() {
    return nom;
  }

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

  public double getPrix() {
    return prix;
  }

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

  public int getStockActuel() {
    return stockActuel;
  }

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

  public int getStockMinimum() {
    return stockMinimum;
  }

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

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

توفر هذه الفئة:

  1. منشئًا لتعيين المعلومات الخمس الخاصة بعنصر ما
  2. أدوات الوصول، التي تُسمى غالبًا getters/setters، وتُستخدم لقراءة وكتابة المعلومات الخمس. تتبع أسماء هذه الطرق معيار JavaBean. يعد استخدام كائنات JavaBean في طبقة DAO للتفاعل مع بيانات نظام إدارة قواعد البيانات (DBMS) ممارسة معتادة.
  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] المحدد بواسطة مفتاحه الأساسي
addArticle
تسمح لك بإضافة مقال إلى جدول ARTICLES
modifyArticle
يسمح لك بتعديل عنصر في جدول [ARTICLES]
deleteItem
يسمح لك بحذف عنصر من جدول [ARTICLES]
updateItemStock
يسمح لك بتعديل مخزون عنصر في جدول [ARTICLES]

توفر الواجهة لبرامج العميل عددًا من الطرق المحددة فقط من خلال توقيعاتها. ولا تهتم بالكيفية التي سيتم بها تنفيذ هذه الطرق فعليًا. وهذا يضفي مرونة على التطبيق. يقوم برنامج العميل باستدعاء واجهة بدلاً من استدعاء تنفيذ محدد لتلك الواجهة.

سيتم اختيار طريقة التنفيذ المحددة من خلال ملف تكوين Spring. ونقترح هنا تنفيذ واجهة IArticlesDao باستخدام منتج مفتوح المصدر يُدعى SqlMap. وسيتيح لنا ذلك إزالة جميع عبارات SQL من كود Java.

يتم تعريف فئة التنفيذ [ArticlesDaoSqlMap] على النحو التالي:

package istia.st.articles.dao;

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

public class ArticlesDaoSqlMap implements IArticlesDao {

  // Fields
  private SqlMapClient sqlMap;

   // Constructors
  public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }

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

تمت مزامنة جميع طرق الوصول إلى البيانات لمنع مشاكل الوصول المتزامن إلى مصدر البيانات. في أي وقت، لا يمكن إلا لخيط واحد الوصول إلى طريقة معينة.

تستخدم فئة [ArticlesDaoSqlMap] أداة [Ibatis SqlMap]. ميزة هذه الأداة هي أنها تسمح بفصل كود SQL الخاص بالوصول إلى البيانات عن كود Java. ثم يتم وضعه في ملف تكوين. سنعود إلى هذا الموضوع لاحقًا. لإنشاء مثيل لفئة [ArticlesDaoSqlMap]، يلزم وجود ملف تكوين يتم تمرير اسمه كمعلمة إلى منشئ الفئة. يحدد ملف التكوين هذا المعلومات اللازمة من أجل:

  • الوصول إلى نظام إدارة قواعد البيانات الذي يحتوي على المقالات
  • إدارة مجموعة الاتصالات
  • إدارة المعاملات

في مثالنا، سيُسمى [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] في نظام إدارة قواعد البيانات. كما يحدد استعلامات 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">

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

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

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

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

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

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

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

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

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

يمكن العثور على كود حزمة [dao] في الملحق.

3.3.4. حزمة [istia.st.articles.domain]

تفصل واجهة [IArticlesDomain] طبقة [business] عن طبقة [web]. وتصل الطبقة الأخيرة إلى طبقة [business/domain] عبر هذه الواجهة دون الاهتمام بالفئة التي تنفذها فعليًا. تحدد الواجهة الإجراءات التالية للوصول إلى طبقة الأعمال:

 package istia.st.articles.domain;

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

public abstract interface IArticlesDomain {

   // Methods
  void acheter(Panier panier);
  List getAllArticles();
  Article getArticleById(int idArticle);
  ArrayList getErreurs();
}
قائمة getAllArticles()
تُرجع قائمة كائنات [Article] المراد عرضها للعميل
Article getArticleById(int idArticle)
تُرجع كائن [Article] المحدد بواسطة [idArticle]
void buy(Cart cart)
تعالج سلة التسوق الخاصة بالعميل عن طريق خصم كمية المشتريات من المخزون — قد تفشل إذا كان المخزون غير كافٍ
ArrayList getErrors()
تُرجع قائمة بالأخطاء التي حدثت - تكون فارغة في حالة عدم وجود أخطاء

هنا، سيتم تنفيذ واجهة [IArticlesDomain] بواسطة فئة [PurchaseItems] التالية:

package istia.st.articles.domain;

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

public class AchatsArticles implements IArticlesDomain {

   // Fields
  private IArticlesDao articlesDao;
  private ArrayList erreurs;

   // Manufacturers
  public AchatsArticles(IArticlesDao articlesDao) { }

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

تنفذ هذه الفئة الطرق الأربع لواجهة [IArticlesDomain]. وتحتوي على حقلين خاصين:

IArticlesDao articlesDao
كائن الوصول إلى البيانات المقدم من طبقة الوصول إلى البيانات
ArrayList errors
قائمة بأي أخطاء

لإنشاء مثيل للفئة، يجب توفير الكائن الذي يسمح بالوصول إلى بيانات نظام إدارة قواعد البيانات (DBMS):

public PurchasesItems(IArticlesDao articlesDao)
منشئ

تمثل فئة [Purchase] عملية شراء قام بها العميل:

package istia.st.articles.domain;

public class Achat {

   // Fields
  private Article article;
  private int qte;

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

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

فئة [Purchase] هي JavaBean تحتوي على الحقول والطرق التالية:

item
المنتج الذي تم شراؤه
الكمية
الكمية المشتراة
double getTotal()
تُرجع مبلغ الشراء
String toString()
تمثيل الكائن كسلسلة

تمثل فئة [Cart] إجمالي مشتريات العميل:

package istia.st.articles.domain;

// Imports
import java.util.ArrayList;

public class Panier {

  // Fields
  private ArrayList achats;

   // Manufacturers
  public Panier() { }

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

فئة [Cart] هي JavaBean تحتوي على الحقول والطرق التالية:

المشتريات
قائمة مشتريات العميل - قائمة من الكائنات من النوع [Purchase]
void add(Purchase purchase)
يضيف عملية شراء إلى قائمة المشتريات
void remove(int itemId)
يزيل عملية الشراء الخاصة بالبند idArticle
double getTotal()
تُرجع المبلغ الإجمالي للمشتريات
String toString()
تُرجع تمثيل السلسلة لعربة التسوق
ArrayList getPurchases()
تُرجع قائمة المشتريات

يمكن العثور على كود حزمة [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] المسؤولة عن الوصول إلى نظام إدارة قواعد البيانات Firebird: ibatis-*.jar
    • الملف المطلوب لأداة [Spring]: spring.jar
    • برنامج تشغيل JDBC لنظام إدارة قواعد البيانات [Firebird]: firebirdsql-full.jar
    • الأرشيفات المطلوبة للتسجيل: log4-*.jar، commons-logging.jar
    • الأرشيفات الثلاثة للنموذج الذي تم اختباره: istia.st.articles.*.jar
    • الأرشيف المطلوب لأداة الاختبار [junit]
  • في [WEB-INF/src] توجد ملفات التكوين التي سيتم نسخها تلقائيًا إلى [WEB-INF/classes] بواسطة Eclipse:
    • ملفات التكوين لأداة [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;

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

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

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

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

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

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

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

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

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

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

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

تعليقات:

  • تقوم فئة الاختبار، باستخدام طريقة setUp الخاصة بها، بتخزين مثيل للفئة قيد الاختبار:
1
2
3
4
5
6
7
8
    // une instance de la classe testée
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }
  • يتم توفير الكائن المراد اختباره بواسطة [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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
</beans>

كما هو موضح أعلاه، فإن bean [articlesDao] هو مثيل لفئة [istia.st.articles.dao.ArticlesDaoSqlMap]. تحتوي هذه الفئة على منشئ يأخذ اسم ملف تكوين أداة [SqlMap] كمعلمة. يتم توفير هذا الاسم هنا. وهو [sqlmap-config-firebird.xml]. وقد تم وصف هذا الملف سابقًا. ويوفر جميع المعلومات اللازمة للوصول إلى بيانات نظام إدارة قواعد البيانات (DBMS).

تقوم طريقة [testChangerStockArticle] بإنشاء 100 مؤشر ترابط مسؤول عن تخفيض مخزون عنصر معين. والغرض هنا هو اختبار الوصول المتزامن إلى نظام إدارة قواعد البيانات. ونظرًا لأن طريقة [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() {
         // follow-up
        System.out.println(name + " lancé");
         // modification stock article 3
        articlesDao.changerStockArticle(3, -1);
         // follow-up
        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;

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

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

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

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

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

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

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

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

تعليقات:

  • تستخدم فئة الاختبار طريقة setUp الخاصة بها لتخزين مثيل للفئة قيد الاختبار بالإضافة إلى مثيل لفئة الوصول إلى البيانات. هذه النقطة الأخيرة مثيرة للجدل. من الناحية النظرية، لا ينبغي أن تحتاج فئة الاختبار إلى الوصول إلى طبقة [DAO]، التي لا يُفترض بها حتى أن تعرف عنها. هنا، تجاهلنا هذه "القاعدة"، التي لو اتبعناها، لكانت قد تطلبت منا إنشاء طرق جديدة في واجهة [IArticlesDomain] الخاصة بنا.
    // une instance de la classe d'accès au domaine
    private IArticlesDomain articlesDomain;

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

    protected void setUp() throws Exception {
        // récupère une instance d'accès au domaine
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
        // récupère une instance d'accès aux données
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }
  • يتم توفير الكائن المراد اختباره بواسطة [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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

كما هو موضح أعلاه، فإن bean [articlesDomain] هو مثيل لفئة [istia.st.articles.domain.AchatsArticles]. تحتوي هذه الفئة على منشئ يتوقع، كمعلمة، كائنًا يوفر الوصول إلى طبقة [dao] من النوع [IArticlesDao]. هنا، يحدد ملف التكوين أن هذا الكائن هو bean المسمى [articlesDao]. وهذا يجبر Spring على إنشاء مثيل لهذا bean. تم شرح إنشاء مثيل bean [articlesDao] سابقًا. لذا، في النهاية، تم إنشاء مثيلين لـ bean:

  • [articlesDao] من النوع [istia.st.articles.dao.ArticlesDaoSqlMap]
  • [articlesDomain] من النوع [istia.st.articles.domain.AchatsArticles]

يتم تشغيل هذين الإنشاءين من خلال أول استدعاء لـ Spring:

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

ثم يتم استرداد حبة [articlesDomain]. أثناء الاستدعاء الثاني لـ Spring:

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

[Spring] يعيد ببساطة مرجعًا إلى حبة [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 = النموذج
فئات الأعمال، وفئات الوصول إلى البيانات، وقاعدة البيانات
V = طرق العرض
صفحات JSP
C = وحدة التحكم
البرنامج الخادم الذي يعالج طلبات العملاء

3.5.1. النموذج

تم عرضه سابقًا. ويتكون من أرشيفات Java [istia.st.articles.dao، istia.st.articles.domain، istia.st.articles.exception].

3.5.2. طرق العرض

تتوافق طرق العرض مع تلك التي تم عرضها في بداية هذا المستند:

LIST
list.jsp
توجد طرق العرض في مجلد [vues] الخاص بالتطبيق
معلومات
info.jsp
سلة التسوق
cart.jsp
سلة التسوق فارغة
empty-cart.jsp
الأخطاء
errors.jsp

3.5.3. وحدة التحكم

ستتألف وحدة التحكم من سيرفلت واحد باسم [WebArticles]. وستتولى معالجة طلبات العملاء المختلفة. وسيتم تحديد هذه الطلبات من خلال وجود معلمة [action] في طلب HTTP الخاص بالعميل:

request
المعنى
إجراء وحدة التحكم
الردود المحتملة
الإجراء=قائمة
يريد العميل قائمة
العناصر
- يطلب قائمة العناصر من
الشركة
- [LIST]
- [ERRORS]
action=info
يطلب العميل
معلومات عن أحد العناصر المعروضة في العرض
[LIST]
- يطلب العنصر من طبقة الأعمال
- [INFO]
- [ERRORS]
action=purchase
يقوم العميل بشراء عنصر
- يطلب المنتج من طبقة الأعمال و
يضيفه إلى سلة التسوق الخاصة به
- [معلومات] في حالة وجود خطأ في الكمية
- [LIST] في حالة عدم وجود خطأ
action=إزالة الشراء
يريد العميل إزالة
شراء من سلة التسوق
- استرجاع سلة التسوق من الجلسة وتعديلها
- [سلة التسوق]
- [سلة التسوق فارغة]
- [أخطاء]
action=cart
يريد العميل عرض
عربة التسوق
- يسترد سلة التسوق من الجلسة
- [عربة التسوق]
- [سلة التسوق فارغة]
- [أخطاء]
action=validate-cart
لقد انتهى العميل من التسوق
ويشرع في إتمام عملية الدفع
- تحديث قاعدة البيانات بمستويات المخزون
المنتجات المشتراة
- إزالة المنتجات التي
تم تأكيدها
- [قائمة]
- [أخطاء]

3.5.4. تكوين التطبيق

سنسعى إلى تكوين التطبيق لجعله مرنًا قدر الإمكان فيما يتعلق بالتغييرات مثل:

  1. التغييرات في عناوين URL لمختلف طرق العرض
  2. التغييرات في الفئات التي تنفذ واجهات [IArticlesDao] و [IArticlesDomain]
  3. التغييرات في نظام إدارة قواعد البيانات (DBMS) أو قاعدة البيانات أو جدول المقالات

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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

للوصول إلى طبقة [الأعمال]، يمكن لفئة في طبقة [واجهة المستخدم، UI] أن تطلب حبة [articlesDomain]. سيقوم Spring بعد ذلك بإنشاء مثيل لكائن من النوع [istia.st.articles.domain.AchatsArticles]. لهذا الإنشاء، يتطلب الأمر حبة من النوع [articlesDao]، أي كائن من النوع [istia.st.articles.dao.ArticlesDaoSqlMap]. سيقوم Spring بعد ذلك بإنشاء مثيل لهذا الكائن. سيستند هذا الإنشاء إلى المعلومات الموجودة في ملف [sqlmap-config-firebird.xml]، وهو ملف التكوين للوصول إلى البيانات عبر SqlMap. في نهاية العملية، تمتلك فئة [UI] التي طلبت bean [articlesDomain] السلسلة الكاملة التي تربطها ببيانات نظام إدارة قواعد البيانات (DBMS):

3.5.7. التغييرات المتعلقة بنظام إدارة قواعد البيانات أو قاعدة البيانات

يتم ضمان استقلالية تطبيق الويب عن التغييرات المتعلقة بنظام إدارة قواعد البيانات (DBMS) أو قاعدة البيانات هنا من خلال ملفات تكوين 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 للعمل مع نظام إدارة قواعد البيانات (DBMS) مختلف.

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

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

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

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

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

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

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

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

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

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

إذا تغيرت أسماء جدول المنتج أو الأعمدة، فسنحتاج إلى إعادة كتابة الاستعلامات في ملف التكوين هذا دون الحاجة إلى تغيير كود Java. وينطبق هذا أيضًا في حالة استبدال استعلام بإجراء مخزن لأسباب تتعلق بالأداء.

3.5.8. البنية العامة لتطبيق [webarticles]

تطبيق الويب Java هو لغز مكون من العديد من القطع. إن تزويده بهيكلية MVC يزيد عمومًا من عدد هذه القطع. هيكل تطبيق [webarticles] تحت [Eclipse] هو كما يلي:

الهيكل العام - فيما يلي
 أرشيفات Java التي يستخدمها
 Eclipse.
spring: لـ Spring
ibatis: لـ SqlMap
log4j، commons-logging: لـ
 من Spring و SqlMap
firebird: لنظام إدارة قواعد البيانات Firebird
mysql: لنظام إدارة قواعد البيانات MySQL
jstl، standard: لـ
 مكتبة علامات JSTL
مجلد مصدر Java: يحتوي على كود Java
 بالإضافة إلى ملفات التكوين
 يقوم Eclipse تلقائيًا بنسخ
 هذه الملفات
إلى [WEB-INF/classes].
وهذا هو المكان الذي سيجدها فيه التطبيق.
مجلد [WEB-INF] الخاص بالتطبيق: يحتوي على
 وصف [web.xml] الخاص بالتطبيق بالإضافة إلى
 ملفات تعريف مكتبة JSTL
طرق العرض

3.5.9. طرق عرض JSP

تستخدم طرق عرض JSP مكتبة علامات JSTL.

3.5.9.1. header.jsp

لضمان الاتساق عبر طرق العرض المختلفة، ستشترك جميعها في نفس الرأس، الذي يعرض اسم التطبيق مع القائمة:

القائمة ديناميكية ويتم تعيينها بواسطة وحدة التحكم. تتضمن وحدة التحكم سمة مفتاح "actions" في الطلب المرسل إلى صفحة JSP، مع قيمة مرتبطة بها عبارة عن مصفوفة Hastable[]. كل عنصر في هذه المصفوفة هو قاموس يهدف إلى إنشاء خيار قائمة في العنوان. يحتوي كل قاموس على مفتاحين:

  • href: عنوان URL المرتبط بخيار القائمة
  • link: نص القائمة

ستستخدم طرق العرض الأخرى للتطبيق العنوان المحدد بواسطة [entete.jsp] باستخدام علامة JSP التالية:

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

أثناء التشغيل، ستقوم هذه العلامة بتضمين الكود من صفحة [entete.jsp] في صفحة JSP التي تحتوي عليها. ونظرًا لأن عنوان URL للصفحة هو عنوان 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. list.jsp

تعرض هذه الصفحة قائمة العناصر المتاحة للبيع:

يتم عرضها بعد إرسال طلب إلى /main?action=list أو /main?action=cartvalidation. وفيما يلي معلمات طلب وحدة التحكم:

actions
كائن Hashtable[] - مصفوفة خيارات القائمة
listarticles
ArrayList من الكائنات من النوع [Item]
رسالة
كائن String - الرسالة المراد عرضها في أسفل الصفحة

يحتوي كل رابط [Info] في جدول المقالات بتنسيق HTML على عنوان URL بالصيغة [?action=infos&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] - العنصر المراد عرضه
msg
كائن سلسلة - الرسالة المراد عرضها في حالة حدوث خطأ في الكمية
qte
كائن سلسلة - القيمة المراد عرضها في حقل الإدخال [الكمية]

يتم استخدام حقول [msg] و [qte] في حالة حدوث خطأ في الإدخال يتعلق بالكمية:

Image

تحتوي هذه الصفحة على نموذج يتم إرساله عبر زر [شراء]. عنوان URL الهدف لطلب POST هو [?action=purchase&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. cart.jsp

تعرض هذه الصفحة محتويات سلة التسوق:

Image

يتم عرضها بعد إرسال طلب إلى /main?action=cart أو /main?action=remove&id=ID. معلمات طلب وحدة التحكم هي كما يلي:

actions
كائن Hashtable[] - مصفوفة خيارات القائمة
cart
كائن من النوع [Cart] - سلة التسوق المراد عرضها

يحتوي كل رابط [إزالة] في جدول عربة التسوق HTML على عنوان URL بالصيغة [?action=removeitem&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. emptycart.jsp

تعرض هذه الصفحة معلومات تشير إلى أن سلة التسوق فارغة:

Image

يتم عرضها بعد طلب إلى /main?action=cart أو /main?action=remove&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. errors.jsp

يتم عرض هذه الصفحة في حالة حدوث أخطاء:

Image

يتم عرضها بعد أي طلب ينتج عنه خطأ، باستثناء عملية الشراء بكمية غير صحيحة، والتي يتم التعامل معها من خلال عرض [INFOS]. عناصر طلب وحدة التحكم هي كما يلي:

actions
كائن Hashtable[] - مصفوفة خيارات القائمة
الأخطاء
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 errors] يحتوي على قائمة بأي أخطاء. ستكون هذه القائمة فارغة في حالة عدم وجود أخطاء، ولكنها ستظل موجودة بغض النظر عن ذلك.
  • في حالة حدوث أخطاء، تتوقف طريقة [init] عند هذا الحد. وإلا، فإنها تنشئ كائنًا من النوع [IArticlesDomain]، والذي سيكون كائن الأعمال الذي يستخدمه وحدة التحكم لتلبية احتياجاتها. كما هو موضح في 3.5.6، ستطلب وحدة التحكم الحبة التي تحتاجها من إطار عمل Spring. قد تؤدي عملية إنشاء المثيل هذه إلى أخطاء متنوعة. إذا حدث ذلك، فسيتم تخزينها مرة أخرى في حقل [errors] الخاص بوحدة التحكم.

3.5.10.2. طريقتا doGet و doPost

تتعامل هاتان الطريقتان مع طلبات HTTP GET و POST الواردة من العملاء. وسيتم التعامل معهما بالتبادل. وبالتالي، قد تعيد طريقة [doPost] التوجيه إلى طريقة [doGet] أو العكس. وسيتم معالجة طلب العميل على النحو التالي:

  • سيتم فحص حقل [errors]. إذا لم يكن فارغًا، فهذا يعني حدوث أخطاء أثناء تهيئة التطبيق وأنه لا يمكن تشغيله. وعندئذٍ سيتم إرسال عرض [ERRORS] كرد.
  • سيتم استرداد المعلمة [action] للطلب والتحقق منها. إذا لم تتطابق مع إجراء معروف، يتم إرسال عرض [ERRORS] مع رسالة خطأ مناسبة.
  • إذا كانت المعلمة [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. معالجة الإجراءات المختلفة

فيما يلي الطرق التي تتعامل مع الإجراءات المختلفة المحتملة للتطبيق:

الطريقة
طلب
معالجة
الاستجابات المحتملة
doList
GET /main?action=list
- طلب قائمة العناصر
من فئة الأعمال
- عرضها
[LIST] أو [ERRORS]
doInfo
GET /main?action=info&id=ID
- استرجاع العنصر ذي المعرف id=ID من
فئة الأعمال
- عرضه
[INFO] أو [ERRORS]
doPurchase
POST /main?action=purchase&id=ID
- الكمية المشتراة مضمنة في المعلمات المرسلة
- طلب العنصر ذي الرقم التعريفي id=ID من
فئة الأعمال
- أضفه إلى سلة التسوق في
جلسة عمل العميل
[LIST] أو [INFO]
أو [ERRORS]
doRemovePurchase
GET /main?action=removePurchase&id=ID
- إزالة العنصر ذي المعرف id=ID من
قائمة التسوق في
جلسة عمل العميل
[CART]
doCart
GET /main?action=cart
- عرض
جلسة العميل
[CART] أو [EMPTY_CART]
doCartValidation
GET /main?action=cartvalidation
- خفض
مستويات المخزون لجميع العناصر
في
[LIST] أو [ERRORS]
[LIST] أو [ERRORS]

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

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

         // it's over
        return;
    }


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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

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

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


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

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

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

سنترك للقارئ الوقت الكافي لقراءة وفهم هذا الكود. ونأمل أن تساعد التعليقات في ذلك.

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

         // it's over
        return;
    }

تعليقات: طريقة [init]

  • تتحقق من وجود معلمات تكوين معينة
  • تقوم بإنشاء مثيل لخدمة للوصول إلى مجال التطبيق باستخدام Spring
  • تعيّن قائمة أخطاء للإشارة إلى أي أخطاء في التهيئة
  • عدد من الحقول الخاصة:
    • errors: قائمة الأخطاء التي اكتشفها [init]
    • [hActionListe، hActionPanier، hActionValidationPanier]: قواميس. يحتوي كل منها على المعلومات اللازمة لعرض خيار في القائمة الرئيسية التي تظهر في عرض [entete.jsp]
    • acticlesDomain: الخدمة للوصول إلى نموذج التطبيق

يتم تنفيذ طريقة [init] مرة واحدة فقط، عند التحميل الأولي للـ servlet. بعد ذلك، يتم تنفيذ إحدى طريقتي [doGet، doPost] اعتمادًا على نوع [GET، POST] لطلب العميل. هنا، تقوم كلتا الطريقتين بنفس الشيء، وقد تم وضع الكود في [doGet]:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
         // check how the initialization of the servelet went
        if (erreurs.size() != 0) {
             // do we have the url of the error page?
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // action is processed
        String action = request.getParameter("action");
        if (action == null) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // list of items
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // article info
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // purchase an item
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // basket display
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // remove an item from the basket
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // shopping cart validation
            doValidationPanier(request, response);
            return;
        }
         // unknown share
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // the error page is displayed
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // end
        return;
    }
  • تبدأ طريقة [doGet] بالتحقق مما إذا كانت قد حدثت أية أخطاء تهيئة بعد طريقة [init]. إذا كان الأمر كذلك، فإنها تعرض عرض [ERRORS] وتنتهي العملية.
  • وإلا، فإنها تسترد المعلمة [action] من طلب العميل. تذكر أن التطبيق تم إنشاؤه للاستجابة للطلبات التي يجب أن تتضمن معلمة [action].
  • وتقوم بتنفيذ الطريقة المرتبطة بالإجراء. وهنا، ستكون هذه هي الطريقة [doListe].

طريقة [doListe] هي كما يلي:

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

         // error list
        ArrayList erreurs = new ArrayList();
         // the list of items is requested
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // we memorize the error
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // end
        return;
    }
  • تذكر أن طريقة [init] قد خزنت الخدمة الخاصة بالوصول إلى نموذج التطبيق (طبقة المجال) في حقل خاص في السيرفلت:
    // champs privés
    private IArticlesDomain articlesDomain = null;
  • باستخدام خدمة الوصول هذه، يمكننا طلب قائمة المقالات:
        // la liste des erreurs
        ArrayList erreurs = new ArrayList();
        // on demande la liste des articles
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
            // on mémorise l'erreur
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
  • في حالة حدوث أخطاء، يتم إرسال عرض [ERRORS]:
         // mistakes?
        if (erreurs.size() != 0) {
             // the error page is displayed
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // end
            return;
        }
  • وإلا، يتم إرسال عرض [LIST]:
1
2
3
4
5
6
7
         // displays the list of items
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);

في مثالنا، سارت الأمور بسلاسة وتمكنا من الحصول على عرض [LIST] بنجاح. ندعو القارئ إلى مراجعة كود عرض [LIST] للتحقق من أن المعلمات الديناميكية التي يتوقعها هذا العرض قد تم توفيرها بالفعل أعلاه بواسطة وحدة التحكم. يجب إجراء نفس النوع من التحقق لكل عرض:

  • البحث عن المعلمات الديناميكية للعرض
  • تأكد من أن وحدة التحكم تضع هذه المعلمات في سمات الطلب التي يتم تمريرها إلى العرض

سنقوم الآن ببساطة بتوضيح تدفق الشاشات التي يواجهها مستخدم التطبيق. ندعو القارئ إلى اتباع خط تفكير مشابه للخط السابق في كل مرة:

من قائمة العناصر، يمكن للمستخدم اختيار عنصر:

يمكن للمشتري شراء العنصر رقم 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 = النموذج
فئات الأعمال، وفئات الوصول إلى البيانات، وقاعدة البيانات
V=طرق العرض
صفحات JSP
C=وحدة التحكم
البرنامج الخادم الذي يعالج طلبات العميل، وكائنات [Action]، ووحدات [ActionForm] المرتبطة بالنماذج.
  • تعد وحدة التحكم قلب التطبيق. تمر جميع طلبات العميل من خلالها. وهي عبارة عن سيرفلت عام يوفره STRUTS. في بعض الحالات، قد تحتاج إلى توسيعها. بالنسبة للحالات البسيطة، هذا ليس ضروريًا. يسترد هذا السيرفلت العام المعلومات التي يحتاجها من ملف يُسمى غالبًا struts-config.xml.
  • إذا كان طلب العميل يحتوي على معلمات نموذج، فإن وحدة التحكم تضعها في كائن Bean. يتم تخزين كائنات Bean التي تم إنشاؤها بمرور الوقت في الجلسة أو طلب العميل. هذا السلوك قابل للتكوين. لا يلزم إعادة إنشائها إذا كانت قد تم إنشاؤها بالفعل.
  • في ملف التكوين struts-config.xml، يرتبط كل عنوان URL المراد معالجته برمجيًا (وبالتالي لا يتوافق مع عرض JSP يمكن طلبه مباشرة) بمعلومات معينة:
    • اسم فئة Action المسؤولة عن معالجة الطلب. هنا مرة أخرى، يمكن تخزين كائن Action الذي تم إنشاء مثيل له في الجلسة أو الطلب.
    • إذا كان عنوان URL المطلوب معلمًا (كما هو الحال عند إرسال نموذج إلى وحدة التحكم)، يتم تحديد اسم bean المسؤول عن تخزين بيانات النموذج.
  • وباستخدام هذه المعلومات الواردة في ملف التكوين الخاص بها، تصبح وحدة التحكم قادرة، عند تلقي طلب URL من العميل، على تحديد ما إذا كان يلزم إنشاء كائن Bean وأي كائن بالضبط. وبمجرد إنشاء مثيل الكائن، يمكن للكائن التحقق مما إذا كانت البيانات المخزنة لديه — والتي تأتي من النموذج — صالحة أم لا. يتم استدعاء طريقة في الكائن تسمى `validate` تلقائيًا بواسطة وحدة التحكم. يتم إنشاء الكائن بواسطة المطور. لذلك، يضع المطور الكود الذي يتحقق من صحة بيانات النموذج داخل طريقة validate. إذا تبين أن البيانات غير صالحة، فلن تواصل وحدة التحكم العمل. وستمرر التحكم إلى عرض يجد اسمه في ملف التكوين الخاص به. عندئذٍ يكتمل التفاعل. لاحظ أن المطور يمكنه اختيار عدم التحقق من صحة النموذج. ويتم ذلك أيضًا في ملف struts-config.xml. في هذه الحالة، لا تستدعي وحدة التحكم طريقة validate الخاصة بالبيان.
  • إذا كانت بيانات الكائن صحيحة، أو إذا لم يكن هناك تحقق من الصحة، أو إذا لم يكن هناك كائن، فإن وحدة التحكم تمرر التحكم إلى كائن Action المرتبط بعنوان URL. وتقوم بذلك عن طريق استدعاء طريقة execute الخاصة بهذا الكائن، وتمرر له المرجع إلى الكائن الذي ربما يكون قد أنشأه. وهنا يقوم المطور بما يجب القيام به: فقد يحتاج إلى استدعاء فئات الأعمال أو فئات الوصول إلى البيانات. في نهاية المعالجة، يعيد كائن Action إلى وحدة التحكم اسم العرض الذي يجب أن ترسله استجابةً للعميل.
  • في ملف التكوين الخاص بها، ستجد وحدة التحكم عنوان URL المرتبط باسم العرض الذي طُلب منها عرضه. ثم ترسل العرض. وبذلك يكتمل التفاعل مع العميل.

في تطبيقنا، لن نستخدم كائنات [Bean] ككائنات عازلة بين العميل وفئات [Action]. سيسترد كائن [Action] مباشرة معلمات طلب العميل من كائن [HttpServletRequest] الذي يتلقاه. وهذا يسهل نقل تطبيقنا الأولي. وبالتالي، ستكون البنية النهائية لتطبيقنا كما يلي:

M=النموذج
فئات الأعمال، وفئات الوصول إلى البيانات، وقاعدة البيانات
V = طرق العرض
صفحات JSP
C = وحدة التحكم
البرنامج الخادم لمعالجة طلبات العميل، كائنات [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>

ماذا يقول هذا الملف؟

  • الصفحة الرئيسية للتطبيق هي [views/index.jsp] (ملف الترحيب)
  • سيتم إعادة توجيه طلبات 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
لعرض قائمة المقالات
info.do
لعرض معلومات حول عنصر معين
purchase.do
لشراء عنصر معين
cart.do
لعرض سلة التسوق
remove-purchase.do
لإزالة عملية شراء من سلة التسوق
confirmcart.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 المرتبط بخيار القائمة
  • link: نص القائمة

ستستخدم طرق العرض الأخرى للتطبيق العنوان المحدد بواسطة [entete.jsp] باستخدام علامة JSP التالية:

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

عند تنفيذها، ستقوم هذه العلامة بتضمين الكود من صفحة [entete.jsp] في صفحة JSP التي تحتوي عليها. وبما أن عنوان URL للصفحة هو عنوان 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. list.jsp

تعرض هذه الصفحة قائمة العناصر المتاحة للبيع:

يتم عرضها بعد إرسال طلب إلى /main.do أو /validerpanier.do. معلمات طلب وحدة التحكم هي كما يلي:

actions
كائن Hashtable[] - مصفوفة خيارات القائمة
listarticles
ArrayList من الكائنات من النوع [Item]
رسالة
كائن String - الرسالة المراد عرضها في أسفل الصفحة

يحتوي كل رابط [Info] في جدول المقالات بتنسيق HTML على عنوان URL بالصيغة [/infos.do?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 عندما تكون الكمية المشتراة غير صحيحة. عناصر طلب وحدة التحكم هي كما يلي:

الإجراءات
object Hashtable[] - مصفوفة خيارات القائمة
العنصر
كائن من النوع [Article] - العنصر المراد عرضه
msg
كائن سلسلة - الرسالة المراد عرضها في حالة حدوث خطأ في الكمية
qte
كائن سلسلة - القيمة المراد عرضها في حقل الإدخال [الكمية]

يتم استخدام حقول [msg] و [qte] في حالة حدوث خطأ في الإدخال يتعلق بالكمية:

Image

تحتوي هذه الصفحة على نموذج يتم إرساله عبر زر [شراء]. عنوان 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. cart.jsp

تعرض هذه الصفحة محتويات سلة التسوق:

Image

يتم عرضها بعد إرسال طلب إلى /panier.do أو /retirerachat.do?id=ID. معلمات طلب وحدة التحكم هي كما يلي:

الإجراءات
كائن Hashtable[] - مصفوفة خيارات القائمة
cart
كائن من النوع [ShoppingCart] - عربة التسوق المراد عرضها

يحتوي كل رابط [إزالة] في مصفوفة عربة التسوق HTML على عنوان URL بالصيغة [removeitem.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. emptycart.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. errors.jsp

يتم عرض هذه الصفحة في حالة حدوث أخطاء:

Image

يتم عرضها بعد أي طلب ينتج عنه خطأ، باستثناء عملية الشراء بكمية غير صحيحة، والتي يتم التعامل معها من خلال عرض [INFOS]. عناصر طلب وحدة التحكم هي كما يلي:

الإجراءات
كائن Hashtable[] - مصفوفة خيارات القائمة
الأخطاء
مجموعة 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 fields
  private ArrayList erreurs = new ArrayList();
  private IArticlesDomain articlesDomain = null;
  private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
  private final String[] parameters = { SPRING_CONFIG_FILENAME };
  private ServletConfig config;
  private final String ACTION_LISTE = "liste.do";
  private final String ACTION_PANIER = "panier.do";
  private final String ACTION_ACHAT = "achat.do";
  private final String ACTION_INFOS = "infos.do";
  private final String ACTION_RETIRER_ACHAT = "retirerachat.do";
  private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
  private String urlActionListe;
  private final String lienActionListe = "Liste des articles";
  private String urlActionPanier;
  private final String lienActionPanier = "Voir le panier";
  private String urlActionValidationPanier;
  private final String lienActionValidationPanier = "Valider le panier";
  private Hashtable hActionListe = new Hashtable(2);
  private Hashtable hActionPanier = new Hashtable(2);
  private Hashtable hActionValidationPanier = new Hashtable(2);

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

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

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

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

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

  public void init() throws ServletException{

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

     // it's over
    return;
  }
}

تعليقات:

  • تكمن قيمة الفئة في طريقة [init] الخاصة بها وحقولها الخاصة
  • تقوم طريقة [init] بنفس ما تقوم به طريقة [init] في وحدة التحكم من الإصدار السابق:
    • فهي تتحقق من وجود معلمات تكوين معينة
    • تقوم بإنشاء مثيل لخدمة للوصول إلى مجال التطبيق باستخدام Spring
    • تقوم بإعداد قائمة أخطاء للإشارة إلى أي أخطاء في التهيئة
  • قبل بدء العمل، تستدعي طريقة [init] طريقة [init] للفئة الأم [ActionServlet]. ستقوم هذه الطريقة بمعالجة ملف تكوين Struts [struts-config.xml].
  • يتم تعريف عدد من الحقول الخاصة مع أدوات الوصول الخاصة بها:
    • errors: قائمة الأخطاء التي اكتشفتها [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، list.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 {

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

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

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

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

تعليقات:

  • تتكون كتابة كود فئة [Action] أساسًا من كتابة كود طريقة [execute] الخاصة بها
  • تم تخزين قدر معين من المعلومات في مثيل وحدة التحكم. نسترد مرجعًا إليها باستخدام:
     // the control servlet
    MainServlet mainServlet = (MainServlet) this.getServlet();
  • نسترد قائمة أخطاء التهيئة المخزنة بواسطة وحدة التحكم. إذا لم تكن هذه القائمة فارغة، يتم إرسال عرض [ERRORS] إلى العميل:
1
2
3
4
5
6
7
8
     // initialization errors?
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

يتم توفير العرض الذي سيتم إرساله فعليًا إلى العميل بواسطة [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] كسمات.

  • مرة أخرى، بفضل وحدة التحكم، يمكن للإجراء استرداد الكائن الذي يوفر الوصول إلى نموذج التطبيق (طبقة المجال):
     // domain access object
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • بمجرد الانتهاء من ذلك، يمكننا طلب قائمة المقالات:
    // la liste des erreurs
    ArrayList erreurs = new ArrayList();
    // on demande la liste des articles
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
      // on mémorise l'erreur
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • في حالة حدوث أخطاء، يتم إرسال عرض [ERRORS]:
1
2
3
4
5
6
7
8
     // mistakes?
    if (erreurs.size() != 0) {
       // the error page is displayed
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • وإلا، يتم إرسال عرض [LIST]:
1
2
3
4
5
6
     // displays the list of items
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");

يتم توفير العرض الذي سيتم إرساله فعليًا إلى العميل بواسطة [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

يُستخدم هذا الإجراء لتوفير معلومات حول أحد العناصر المعروضة في عرض [LIST]:

يتم تكوين هذا الإجراء على النحو التالي في [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 {

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

تعليقات:

  • بداية طريقة [execute] مطابقة لتلك التي تمت مناقشتها سابقًا. وسيكون هذا هو الحال أيضًا بالنسبة للإجراءات الأخرى.
  • تسترد الطريقة المعلمة [id]، التي يجب أن تكون موجودة عادةً في عنوان URL. يجب أن يكون عنوان URL بالفعل بالصيغة [/infos.do?id=X]. يتم إجراء فحوصات متنوعة للتحقق من وجود وصحة المعلمة [id]. إذا كانت هناك مشكلة، يتم عرض طريقة العرض [ERRORS].
  • إذا كان [id] صالحًا، يتم طلب العنصر المقابل من طبقة [domain]. إذا أدى ذلك إلى حدوث استثناء أو إذا لم يتم العثور على العنصر، يتم إرسال عرض [ERRORS] مرة أخرى.
  • إذا سارت الأمور على ما يرام، يتم تخزين العنصر المسترد في الجلسة. هذه نقطة قابلة للنقاش. هنا، نفترض أن العميل قد يشتري هذا العنصر. إذا فعل ذلك، فسنسترده من الجلسة بدلاً من طلبه مرة أخرى من طبقة [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. purchase.do

يُستخدم هذا الإجراء لشراء العنصر المعروض في عرض [INFOS] السابق:

بمجرد شراء العنصر، يتم إعادة عرض عرض [LIST] (العرض الأيمن). إذا نظرنا إلى كود 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 {

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

تعليقات:

  • بداية طريقة [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] إلى المستخدم مع رسالة خطأ:
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
     // the quantity purchased is recovered
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // wrong qty
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
  • يتم استرداد العنصر الذي تم شراؤه من الجلسة. قد تكون الجلسة قد انتهت صلاحيتها. في هذه الحالة، يتم عرض عرض [ERRORS]:
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
  • إذا لم تنته صلاحية الجلسة، يتم إضافة العنصر إلى سلة التسوق، والتي يتم استردادها أيضًا من الجلسة:
     // retrieve the client session
    HttpSession session = request.getSession();
     // we retrieve the article placed in session
    Article article = (Article) session.getAttribute("article");
     // session expired?
    if(article==null){
       // the error page is displayed
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // create the new purchase
    Achat achat = new Achat(article, qté);
     // the purchase is added to the customer's basket
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • أخيرًا، نرسل عرض [LIST]:
    // on revient à la liste des articles
    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. cart.do

يُستخدم هذا الإجراء لعرض جميع مشتريات العميل. وهو متاح عبر خيار القائمة [عرض سلة التسوق]:

فيما يلي كود HTML المرتبط برابط [عرض سلة التسوق]:

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

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

تعليقات:

  • بداية طريقة [execute] مطابقة لتلك التي تمت مناقشتها سابقًا.
  • يتم استرداد سلة التسوق من الجلسة حيث توجد عادةً. قد تكون الجلسة قد انتهت صلاحيتها، وفي هذه الحالة لا توجد سلة تسوق. لا نتعامل مع هذا على أنه خطأ، بل نفترض ببساطة أن سلة التسوق فارغة.
  • إذا كانت عربة التسوق فارغة، يتم عرض طريقة العرض [EMPTY CART]
  • وإلا، يتم عرض طريقة العرض [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. checkout.do

تقوم هذه العملية بإزالة عنصر من سلة التسوق:

بعد الإجراء [retirerachat.do]، يتم إعادة عرض سلة التسوق (الطريقة في أعلى اليمين). إذا نظرنا إلى كود HTML الخاص برابط [Confirm Cart] في الطريقة الموجودة أعلى اليسار، نرى ما يلي:

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

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

تعليقات:

  • بداية طريقة [execute] مطابقة لتلك التي تمت دراستها سابقًا.
  • يتحقق الكود التالي من وجود المعلمة [id] وصحتها. إذا كانت غير صحيحة، يتم إرسال عرض [ERRORS].
  • وإلا، تتم إزالة عملية الشراء من سلة التسوق:
    // on enlève l'achat
    panier.enlever(id);
  • ثم يتم إعادة عرض سلة التسوق:
    // on affiche de nouveau le panier
    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>

يمكننا أن نرى أنه، من حيث طرق العرض، فإن الإجراء [/cart.do] هو الذي سيتم تشغيله. وقد تم وصف ذلك بالفعل. سيعرض طريقة العرض [CART] أو [EMPTY CART] اعتمادًا على حالة سلة التسوق.

3.6.6.7. confirmCart.do

يُستخدم هذا الإجراء لتأكيد مشتريات العميل. عمليًا، يتضمن هذا إجراءً واحدًا: يتم خصم الكميات المشتراة من مستويات مخزون العناصر المشتراة في قاعدة البيانات. يأتي هذا الإجراء من القائمة التالية:

 

فيما يلي كود HTML لرابط [تأكيد سلة التسوق]:

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

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

تعليقات:

  • بداية طريقة [execute] مطابقة لتلك التي درسناها سابقًا.
  • نسترد سلة التسوق من الجلسة. إذا انتهت صلاحية الجلسة، نعرض عرض [ERRORS]:
     // the list of errors on this action
    ArrayList erreurs = new ArrayList();
         // the buyer has confirmed his basket
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // session expired
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • نقوم بمعالجة المشتريات الموجودة في سلة التسوق. قد تحدث أخطاء إذا كانت مستويات المخزون غير كافية لتلبية المشتريات. في هذه الحالة، نعرض عرض [ERRORS]:
         // validate basket
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // not normal
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
        }
         // recover any errors
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
  • إذا سارت الأمور على ما يرام، نعرض قائمة العناصر مرة أخرى:
        // tout semble OK - on affiche la liste des articles
    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] هو الذي سيتم تشغيله. وقد تم وصف ذلك بالفعل. وسيعرض عرض [LIST].

3.7. بنية MVC مع Spring

3.7.1. البنية العامة للتطبيق

دعونا نعيد النظر في بنية MVC للتطبيق:

في الإصدار الأول:

  • تمت معالجة وحدة التحكم بواسطة سيرفلت
  • تمت معالجة العروض بواسطة صفحات JSP
  • تم التعامل مع النموذج بواسطة مجموعة من ثلاثة ملفات .jar

في إصدار Struts:

  • تم التعامل مع وحدة التحكم بواسطة سيرفلت مشتق من [ActionServlet] العام في Struts
  • تمت معالجة العروض بواسطة نفس صفحات JSP كما في إصدار [Struts]
  • تم التعامل مع النموذج بواسطة نفس الأرشيفات الثلاثة

في إصدار Spring:

  • سيتم التعامل مع وحدة التحكم بواسطة سيرفلت مقدم من Spring [DispatcherServlet]
  • سيتم التعامل مع طرق العرض بواسطة نفس صفحات JSP كما في السابق، مع بعض الاختلافات الطفيفة
  • سيتم التعامل مع النموذج بواسطة نفس الملفات الثلاثة

سنجد أن ترحيل تطبيقنا من Struts إلى Spring أمر بسيط إذا كنا على استعداد للتخلي عن استخدام جميع العناصر الموصى بها لهندسة Spring MVC القياسية. التغييرات الرئيسية هي كما يلي:

  • الأعمال التي كانت تُعالج سابقًا في طرق محددة في السيرفلت/وحدة التحكم، أو بواسطة مثيلات فئات مشتقة من فئة [Action] في Struts، تُعالج الآن بواسطة مثيلات فئات تُنفذ واجهة Spring [Controller]
  • ملفات التكوين المطلوبة هي كما يلي:
    • [web.xml] لأن هذا تطبيق ويب. يحتوي هذا الملف على مستمع يستخدم، عند تهيئة التطبيق، ملف [applicationContext.xml]
    • [applicationContext.xml] لإنشاء الحبوب التي يتطلبها التطبيق، ولا سيما حبة خدمة الوصول إلى النموذج
  • ستكون طرق عرض JSP مطابقة لتلك الموجودة في Struts. سنحتاج إلى إنشاء واحدة جديدة.

دعونا نستذكر بنية STRUTS MVC المستخدمة في الإصدار السابق:

M = النموذج
فئات الأعمال، وفئات الوصول إلى البيانات، وقاعدة البيانات
V = طرق العرض
صفحات JSP
C = وحدة التحكم
البرنامج الخادم لمعالجة طلبات العميل، كائنات [Action]

مع Spring، نستخدم نفس البنية:

M=النموذج
فئات الأعمال، وفئات الوصول إلى البيانات، وقاعدة البيانات
V=طرق العرض
صفحات JSP
C = وحدة التحكم
البرنامج الخادم الذي يعالج طلبات العملاء، والكائنات التي تنفذ واجهة [Controller]
  • تعد وحدة التحكم قلب التطبيق. تمر جميع طلبات العميل من خلالها. وهي عبارة عن سيرفلت عام يوفره SPRING. وهي من نوع [DispatcherServlet]. من الآن فصاعدًا، سنشير إلى وحدة التحكم هذه باسم وحدة التحكم [Spring].
  • ستقوم وحدة التحكم [Spring] بتوجيه طلب العميل إلى إحدى مثيلات [Controller]. سيكون هناك مثيل واحد لكل إجراء يتم معالجته. يتم تحديد ذلك في عنوان URL المطلوب، تمامًا كما هو الحال مع Struts. وبالتالي، سنعرف أن الإجراء المطلوب هو إجراء [list] لأن عنوان URL المطلوب هو [list.do]
  • إذا كان C هو سياق التطبيق، فإن وحدة التحكم [Spring] تستخدم ملف [C-servlet.xml] الذي يؤدي نفس دور ملف التكوين struts-config.xml في إصدار Struts. لكل إجراء سيتم معالجته بواسطة التطبيق، نربط اسم فئة نوع وحدة التحكم المسؤولة عن معالجة الطلب.
  • تسلم وحدة التحكم زمام الأمور إلى الكائن من نوع وحدة التحكم المرتبط بالإجراء. وتقوم بذلك عن طريق استدعاء طريقة handleRequest الخاصة بهذا الكائن وتمرير طلب العميل إليه. وهنا يقوم المطور بالمهام الضرورية: فقد يحتاج إلى استدعاء فئات منطق الأعمال أو فئات الوصول إلى البيانات. في نهاية المعالجة، يعيد كائن وحدة التحكم إلى وحدة التحكم اسم العرض الذي يجب أن ترسله ردًا على العميل.
  • في ملف التكوين الخاص بها، ستجد وحدة التحكم عنوان 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>
     <!-- application spring context loader -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- the servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- url mapping -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- entry document -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

ماذا يقول هذا الملف؟

  • الصفحة الرئيسية للتطبيق هي [/vues/index.jsp] (ملف الترحيب)
  • سيتم إعادة توجيه طلبات 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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

بعض العناصر مألوفة، في حين أن البعض الآخر أقل شيوعًا. سيتم إنشاء ثلاث حبات أثناء تهيئة التطبيق:

  • articlesDao: خدمة توفر الوصول إلى طبقة [dao]
  • articlesDomain: خدمة توفر الوصول إلى النموذج
  • config: عنصر سنجمع فيه المعلومات التي يجب أن يتشاركها جميع العملاء. سيؤدي هذا العنصر الدور الذي يؤديه عادةً سياق التطبيق، ولكن بمعلومات مكتوبة بدلاً من غير مكتوبة.

يحدد الملف الأخير [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>
     <!-- stock managers = controllers -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

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

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

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

ماذا يقول ملف التكوين هذا؟

  • أن وحدة التحكم الخاصة بنا ستتعامل مع عناوين URL التالية:
main.do
لعرض قائمة المقالات
liste.do
لعرض قائمة المقالات
info.do
لعرض معلومات حول عنصر معين
purchase.do
لشراء عنصر معين
cart.do
لعرض سلة التسوق
remove-purchase.do
لإزالة عملية شراء من سلة التسوق
confirmcart.do
لتأكيد سلة التسوق
  • تتوافق الإجراءات المذكورة أعلاه بشكل مباشر مع الإجراءات التي كان يتولى معالجتها وحدة التحكم في الإصدارات السابقة. بالنسبة لكل إجراء، يتم تحديد اسم الفئة المسؤولة عن معالجته. لنأخذ مثال الإجراء [/panier.do]:
  • يجب أن يتم معالجته بواسطة حبة [VoirPanierController]. هذا الاسم تعسفي. إنه مجرد مفتاح.
                <prop key="/panier.do">VoirPanierController</prop>
  • المفتاح [VoirPanierController] هو اسم bean محدد في نفس ملف التكوين:
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
  • يحدد bean [VoirPanierController]:
  • الفئة التي سيتم إنشاء مثيل لها [istia.st.articles.web.spring.VoirPanierController] لمعالجة الإجراء
  • كيفية إنشاء مثيل لها. هنا، يتم توفير bean [config] المحدد بواسطة [applicationContext.xml] والذي تم إنشاء مثيل له عند بدء تشغيل التطبيق كمعلمة. سيتم تنفيذ ذلك لجميع إجراءات [Controller]. وبالتالي، سيكون لكل منها، في حقل خاص، كائن [config] الذي سيحتوي على جميع المعلومات المشتركة بين جميع العملاء.
  • كيفية تحليل أسماء العروض:
    <!-- gestionnaire de vues -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

كما هو الحال مع Struts، فإن مثيل [Controller] الذي يعالج إجراءً ما سيعيد، بعد المعالجة، مفتاحًا إلى وحدة التحكم Spring للإشارة إلى العرض الذي يجب أن تعرضه. بناءً على هذا المفتاح، قد تكون هناك استراتيجيات مختلفة لتوليد العرض المرتبط بالمفتاح. الاستراتيجية المستخدمة هي تلك التي يحددها bean [viewResolver]. هنا، يرتبط هذا bean بفئة [org.springframework.web.servlet.view.InternalResourceViewResolver] مع معلمات تهيئة متنوعة. دون الخوض في التفاصيل، يحدد bean [viewResolver] هنا أنه إذا كان مفتاح العرض هو "XX"، فإن العرض الذي سيتم إنشاؤه سيكون [/views/XX.jsp]. يمكن تغيير نوع العروض المرسلة إلى العميل بطرق متنوعة:

  • عن طريق تغيير فئة التنفيذ لـ bean [viewResolver]
  • عن طريق تغيير معلمات التهيئة لفئة التنفيذ

وبالتالي، يمكنك التبديل من عرض HTML إلى عرض XML ببساطة عن طريق تغيير قيمة bean [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>
     <!-- application spring context loader -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- the servlet -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- url mapping -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- entry document -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  • إذا كان هذا هو الطلب الأول الموجه إلى التطبيق، فقد تم تشغيل عدد من الأمور:
    • تم تحميل المستمع [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>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- the business class -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- web application configuration-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>
  • تم إنشاء العناصر أعلاه في سياق التطبيق
  • ثم تم استخدام ملف [springwebarticles-servlet.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- stock managers = controllers -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

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

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

     <!-- message file -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>
  • كما تم إنشاء حبوب [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>
     <!-- stock managers = controllers -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

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

</beans>
  • يخبر هذا الملف أنه يجب معالجة الإجراء [/main.do] بواسطة مكون [ListController].
  • يتم تمرير طلب العميل إلى طريقة [handleRequest] الخاصة بـ bean [ListController]. تقوم هذه الطريقة بعملها وتُرجع مفتاح العرض المراد عرضه إلى وحدة التحكم. هنا، إذا سارت الأمور على ما يرام، سيكون هذا المفتاح هو [list].
  • يستخدم وحدة التحكم Spring حبة [viewResolver] من ملف التكوين [springwebarticles-servlet.xml] لتحديد العرض المرتبط بهذا المفتاح. هنا، سيكون العرض هو [/vues/liste.jsp]
  • يتم إرسال العرض [/vues/liste.jsp] إلى العميل

3.7.6. تهيئة تطبيق Spring

ذكرنا أنه عند بدء تشغيل التطبيق، يتم إنشاء مثيلات للـ beans الموجودة في ملف [applicationContext.xml]:

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

نحن على دراية بالفئات [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 fields
    private IArticlesDomain articlesDomain = null;
    private final String ACTION_LISTE = "liste.do";
    private final String ACTION_PANIER = "panier.do";
    private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
    private final String lienActionListe = "Liste des articles";
    private final String lienActionPanier = "Voir le panier";
    private final String lienActionValidationPanier = "Valider le panier";
    private Hashtable hActionListe = new Hashtable(2);
    private Hashtable hActionPanier = new Hashtable(2);
    private Hashtable hActionValidationPanier = new Hashtable(2);

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

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

    public Hashtable getHActionListe() {
        return hActionListe;
    }

    public Hashtable getHActionPanier() {
        return hActionPanier;
    }

    public Hashtable getHActionValidationPanier() {
        return hActionValidationPanier;
    }

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

         // it's over
        return;
    }
}

تؤدي هذه الفئة نفس وظيفة طريقة [init] في سيرفلت تطبيق الويب. فهي تقوم بتهيئة التطبيق. وهنا، يتم ذلك على النحو التالي:

  • لأن حبة [config] محددة على النحو التالي في [applicationContext.xml]:
    <!-- la configuration de l'application web-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>

عند إنشائه، يتم تهيئة حقل [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] بواسطة:

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

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

وهي مرتبطة بفئة [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 {

     // web app configuration
    Config config;

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

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

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

}

تعليقات:

  • تحتوي الفئة على حقل خاص [config]. تم تهيئة هذا الحقل بواسطة Spring عند إنشاء مثيل bean [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]، أي مثيل لـ [istia.st.articles.web.spring.Config] الموصوف أعلاه.

  • تتضمن كتابة الكود لفئة [Controller] بشكل أساسي كتابة الكود لطريقة [handleRequest] الخاصة بها
  • نطلب قائمة المقالات من النموذج. يمكن الوصول إلى هذه القائمة عبر [config.getArticlesDomain()]. في حالة حدوث استثناء، يتم عرض طريقة العرض [ERRORS]. يجب أن تكون النتيجة التي تعيدها [handleRequest] من النوع [ModelAndView]. يمكن إنشاء مثيل لهذه الفئة بطرق مختلفة. هنا، وسيكون هذا هو الحال دائمًا، نقوم بإنشاء مثيل لـ [ModelAndView] عن طريق تمرير مفتاح العرض المراد عرضه إليه. تذكر أنه، بناءً على تكوين حبة [viewResolver]، سيؤدي طلب العرض بمفتاح XX إلى إرسال العرض [/vues/XX.jsp].
        // on demande la liste des articles
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
            // on mémorise l'erreur
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
            // on affiche la page des erreurs
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
            // envoyer la vue erreurs
            return new ModelAndView("erreurs");
        }
  • في حالة عدم وجود أخطاء، يتم إرسال عرض [LIST]:
1
2
3
4
5
6
7
        // on affiche la liste des articles
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
        // envoyer la vue
        return new ModelAndView("liste");

3.7.7.3. infos.do

يُستخدم هذا الإجراء لتقديم معلومات حول أحد العناصر المعروضة في عرض [LIST]:

يتم تعريف هذا الإجراء في ملف [springwebarticles-servlet.xml] على النحو التالي:

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

فيما يلي كود فئة [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 {

     // web app configuration
    Config config;

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

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

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

تعليقات:

  • تسترد طريقة [handleRequest] المعلمة [id]، التي يجب أن تكون موجودة عادةً في عنوان URL. يجب أن يكون عنوان URL بالفعل بالصيغة [/infos.do?id=X]. يتم إجراء فحوصات متنوعة للتحقق من وجود وصحة المعلمة [id]. إذا كانت هناك مشكلة، يتم عرض طريقة العرض [ERRORS].
  • إذا كان [id] صالحًا، يتم طلب العنصر المقابل من طبقة [domain]. إذا أدى ذلك إلى حدوث استثناء أو إذا لم يتم العثور على العنصر، يتم إرسال عرض [ERROR] مرة أخرى.
  • إذا سارت الأمور على ما يرام، يتم تخزين العنصر المسترد في الجلسة. هذه نقطة قابلة للنقاش. هنا، نفترض أن العميل قد يشتري هذا العنصر. إذا فعل ذلك، فسنسترده من الجلسة بدلاً من طلبه مرة أخرى من طبقة [domain].
  • أخيرًا، يتم عرض عرض [INFO].

3.7.7.4. purchase.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]:

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

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

فيما يلي كود فئة [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 {

   // web app configuration
  Config config;

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

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

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

تعليقات:

  • دعونا نراجع تنسيق النموذج المرسل إلى وحدة التحكم:
        <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] إلى المستخدم مع رسالة خطأ:
    // la liste des erreurs sur cette action
    ArrayList erreurs = new ArrayList();
    // on récupère la quantité achetée
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
      // qté erronée
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
  • يتم استرداد العنصر الذي تم شراؤه من الجلسة. قد تكون الجلسة قد انتهت صلاحيتها. في هذه الحالة، يتم إرسال عرض [ERRORS]:
    // on récupère la session du client
    HttpSession session = request.getSession();
    // on récupère l'article mis en session
    Article article = (Article) session.getAttribute("article");
    // session expirée ?
    if (article == null) {
      // on affiche la page des erreurs
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • إذا لم تنته صلاحية الجلسة، يتم إضافة العنصر إلى سلة التسوق، والتي يتم استردادها أيضًا من الجلسة:
1
2
3
4
5
6
7
8
9
    // on crée le nouvel achat
    Achat achat = new Achat(article, qté);
    // on ajoute l'achat au panier du client
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • أخيرًا، نرسل عرض [LIST]:
    // on revient à la liste des articles
    return new ModelAndView("index");
  • في الأعلى، نرسل العرض [/views/index.jsp]. نعلم أن هذا العرض يوجه متصفح العميل لإعادة التوجيه إلى عنوان URL [/main.do]. هذه الإعادة للتوجيه هي التي ستعرض قائمة العناصر.

3.7.7.5. cart.do

يُستخدم هذا الإجراء لعرض جميع مشتريات العميل. وهو متاح عبر القائمة:

Image

فيما يلي كود HTML المرتبط بالرابط أعلاه:

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

الصفحة التي يعرضها هذا الرابط هي كما يلي:

Image

يتم تكوين هذا الإجراء على النحو التالي في [springwebarticles-servlet.xml]:

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

فيما يلي كود فئة [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 {

   // web app configuration
  Config config;

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

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

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

تعليقات:

  • يتم استرداد سلة التسوق من الجلسة التي يتم تخزينها فيها عادةً. قد تكون الجلسة قد انتهت صلاحيتها، وفي هذه الحالة لن تكون هناك سلة تسوق. لا نتعامل مع هذا على أنه خطأ، بل نفترض ببساطة أن سلة التسوق فارغة.
  • إذا كانت عربة التسوق فارغة، يتم عرض طريقة العرض [EMPTY CART]
  • وإلا، يتم عرض عرض [العربة]

3.7.7.6. removePurchase.do

يُستخدم هذا الإجراء لإزالة عملية شراء من سلة التسوق:

Image

إذا نظرنا إلى كود HTML للرابط أعلاه، نرى ما يلي:

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

وبالتالي، يتلقى الإجراء [retirerachat.do]، كمعلمة، معرف العنصر المراد إزالته من سلة التسوق. يتم تكوين هذا الإجراء على النحو التالي في [springwebarticles-servlet.xml]:

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

فيما يلي كود فئة [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 {

   // web app configuration
  Config config;

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

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

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

تعليقات:

  • يتحقق الكود من وجود المعلمة [id] وصحتها. إذا كانت غير صحيحة، يتم إرسال عرض [ERRORS].
  • وإلا، يتم إزالة العنصر من سلة التسوق:
    // on enlève l'achat
    panier.enlever(id);
  • ثم يتم إعادة تحميل سلة التسوق:
    // on affiche de nouveau le panier
    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. confirmcart.do

يُستخدم هذا الإجراء لتأكيد مشتريات العميل. عمليًا، يتضمن هذا إجراءً واحدًا: يتم خصم الكميات المشتراة من مستويات مخزون العناصر المشتراة في قاعدة البيانات. يأتي هذا الإجراء من القائمة التالية:

Image

رمز HTML لرابط [Confirm Cart] هو كما يلي:

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

عند النقر على هذا الرابط، يتم تحديث مستويات المخزون، ويتم عرض قائمة العناصر مرة أخرى.

يتم تكوين هذا الإجراء على النحو التالي في [springwebarticles-servlet.xml]:

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

فيما يلي كود فئة [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 {

   // web app configuration
  Config config;

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

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

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

تعليقات:

  • نسترد سلة التسوق من الجلسة. إذا انتهت صلاحية الجلسة، نعرض عرض [ERRORS]:
    // la liste des erreurs sur cette action
    ArrayList erreurs = new ArrayList();
    // l'acheteur a confirmé son panier
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
      // session expirée
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • نقوم بمعالجة المشتريات الموجودة في سلة التسوق. قد تحدث أخطاء إذا كانت مستويات المخزون غير كافية لتلبية المشتريات. في هذه الحالة، نعرض عرض [ERRORS]:
    // on valide le panier
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
      // pas normal
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
    // on récupère les éventuelles erreurs
    erreurs = config.getArticlesDomain().getErreurs();
    if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe(), config.getHActionPanier() });
      return new ModelAndView("erreurs");
    }
  • إذا سارت الأمور على ما يرام، نعرض قائمة العناصر مرة أخرى:
    // tout semble OK - on affiche la liste des articles
    return new ModelAndView("index");

نعلم أن العرض [/vues/index.jsp] يعيد توجيه العميل إلى الإجراء [/main.do]. سيقوم هذا الإجراء بعرض العرض [LISTE].