Skip to content

3. المادة 2 - أمثلة على بنى الويب ثلاثية الطبقات

أهداف المقالة:

  • البنى ثلاثية الطبقات
  • بنية الويب الأساسية MVC
  • بنية Struts MVC
  • بنية Spring MVC

الأدوات المستخدمة:

  • Spring: http://www.springframework.org/
  • Ibatis SqlMap: http://www.ibatis.com/
  • Junit: http://www.junit.org/index.htm
  • Eclipse: http://www.eclipse.org/
  • Struts: http://struts.apache.org/
  • Firebird: http://firebird.sourceforge.net/: SGBD، برنامج التشغيل JDBC. في الواقع، أي مصدر JDBC يفي بالغرض.
  • IBExpert، الإصدار الشخصي: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (مارس 2005). يتيح IBExpert إدارة SGBD Firebird بشكل رسومي.
  • Tomcat: http://jakarta.apache.org/tomcat/
  • مكون إضافي لـ Tomcat لـ Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. انظر أيضًا الوثيقة https://tahe.developpez.com/java/eclipse/

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

  • لغة Java: [https://tahe.developpez.com/java/cours]
  • البرمجة على الويب بلغة Java: [https://tahe.developpez.com/java/web/]
  • برمجة الويب باستخدام Java و Eclipse و Tomcat: [https://tahe.developpez.com/java/eclipse/]
  • برمجة الويب باستخدام Struts: [https://tahe.developpez.com/java/struts/]
  • استخدام مظهر Spring: [https://tahe.developpez.com/java/springioc]
  • مكتبة العلامات: JSTL (جزئيًا)
  • وثائق Ibatis SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
  • Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (مارس 2005).

تعود أفكار هذا المستند إلى كتاب قرأته خلال صيف 2004، وهو عمل رائع لرود جونسون: J2EE Development without EJB الصادر عن دار نشر Wrox.


3.1. تطبيق webarticles

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

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

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

  • الواجهة [LISTE] التي تعرض قائمة بالمنتجات المعروضة للبيع

Image

  • الطريقة [INFOS] التي تقدم معلومات إضافية عن منتج ما:

Image

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

Image

  1. عرض [PANIERVIDE] في حالة كون سلة التسوق فارغة

Image

  1. عرض [ERREURS] الذي يشير إلى أي خطأ في التطبيق

Image

3.2. الهيكل العام للتطبيق

نرغب في إنشاء تطبيق له البنية ثلاثية الطبقات التالية:

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

سنفترض هنا أن الطبقتين [domain] و [dao] قد تم إنشاؤهما. لن نهتم سوى بالطبقة [web] التي نقترح إنشاؤها بعدة طرق:

  • باستخدام تقنية Servlet Controller التقليدية - صفحات JSP
  • باستخدام تقنية Struts MVC
  • باستخدام تقنية Spring MVC

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

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

  1. يقدم العميل طلبًا إلى وحدة التحكم. وحدة التحكم هذه هي خدمة صغيرة (servlet) تمر عبرها جميع طلبات العملاء. وهي بوابة الدخول إلى التطبيق. وهي الحرف 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
);


/* القيود */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');

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

في الاختبارات التالية، تم استخدام قاعدة [Firebird]. [Firebird] هو "مصدر مفتوح" لـ SGBD. يتم وضع برنامج التشغيل JDBC [firebirdsql-full.jar] في مجلد [WEB-INF/lib] الخاص بتطبيق الويب.

3.3.2. حزم النموذج

يتم توفير النموذج M هنا في شكل ثلاثة أرشيفات:

  • istia.st.articles.dao: يحتوي على فئات الوصول إلى بيانات الطبقة [dao]
  • istia.st.articles.exception: يحتوي على فئة استثناء لإدارة هذه العناصر
  • istia.st.articles.domain: يحتوي على فئات الأعمال الخاصة بطبقة [domain]
archive
contenu
rôle
istia.st.articles.dao
- يحتوي على الحزمة [istia.st.articles.dao] التي تحتوي بدورها على العناصر التالية:
- [IArticlesDao]: واجهة الوصول إلى طبقة Dao وهي الواجهة الوحيدة التي تراها طبقة [domain]. ولا ترى أي واجهة أخرى.
- [Article]: فئة تحدد مقالًا
- [ArticlesDaoSqlMap]: فئة تنفيذ واجهة [IArticlesDao] باستخدام أداة SqlMap
طبقة الوصول إلى البيانات – موجودة بالكامل في الطبقة [dao] للبنية ثلاثية الطبقات لتطبيق الويب
istia.st.articles.domain
- تحتوي على الحزمة [istia.st.articles.domain] التي تحتوي بدورها على العناصر التالية:
- [IArticlesDomain]: واجهة الوصول إلى الطبقة [domain]. هذه هي الواجهة الوحيدة التي تراها طبقة الويب. ولا ترى أي واجهة أخرى.
- [AchatsArticles]: فئة تنفذ [IArticlesDomain]
- [Achat]: فئة تمثل عملية شراء أحد العملاء
- [Panier]: فئة تمثل مجموع مشتريات العميل
تمثل نموذج عمليات الشراء على الويب - توجد بالكامل في الطبقة [domain] من بنية التطبيق الويب ثلاثية الطبقات
istia.st.articles.exception
- يحتوي على الحزمة [istia.st.articles.exception] التي تحتوي بدورها على العناصر التالية:
- [UncheckedAccessArticlesException]: فئة تُعرِّف استثناءً من النوع [RuntimeException]. يتم إطلاق هذا النوع من الاستثناءات من قبل الطبقة [dao] فور حدوث مشكلة في الوصول إلى البيانات.
 

3.3.3. الحزمة [istia.st.articles.dao]

الفئة التي تحدد عنصرًا هي التالية:

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

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

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

  public Article(int id, String nom, double prix, int stockActuel,
      int stockMinimum) {
     // تعيين سمات المثيل
    setId(id);
    setNom(nom);
    setPrix(prix);
    setStockActuel(stockActuel);
    setStockMinimum(stockMinimum);
  }

     // المنشورات - المغيرات
  public int getId() {
    return id;
  }

  public void setId(int id) {
     // هل المعرف صالح؟
    if (id < 0)
      throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
    this.id = id;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
     // اسم صالح؟
    if(nom==null || nom.trim().equals("")){
      throw new UncheckedAccessArticlesException("Le nom est [null] ou vide");
    }
    this.nom = nom;
  }

  public double getPrix() {
    return prix;
  }

  public void setPrix(double prix) {
     // سعر صالح؟
    if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
    this.prix = prix;
  }

  public int getStockActuel() {
    return stockActuel;
  }

  public void setStockActuel(int stockActuel) {
     // مخزون صالح؟
    if (stockActuel < 0)
      throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
    this.stockActuel = stockActuel;
  }

  public int getStockMinimum() {
    return stockMinimum;
  }

  public void setStockMinimum(int stockMinimum) {
     // مخزون صالح؟
    if (stockMinimum < 0)
      throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
    this.stockMinimum = stockMinimum;
  }

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

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

  1. منشئًا يسمح بتحديد المعلومات الخمس الخاصة بالمادة
  2. وحدات وصول تُسمى غالبًا getters/setters تُستخدم لقراءة وكتابة المعلومات الخمس. تتبع أسماء هذه الطرق المعيار JavaBean. يعد استخدام كائنات JavaBean في الطبقة DAO للتفاعل مع بيانات SGBD أمرًا شائعًا.
  3. التحقق من البيانات التي تم إدخالها في المادة. في حالة وجود بيانات خاطئة، يتم إصدار استثناء.
  4. طريقة toString التي تسمح بالحصول على قيمة عنصر في شكل سلسلة أحرف. وغالبًا ما يكون ذلك مفيدًا لتصحيح أخطاء التطبيق.

يتم تعريف واجهة [IArticlesDao] على النحو التالي:

package istia.st.articles.dao;

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

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

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

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

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

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

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

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

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

دور الطرق المختلفة للواجهة هو كما يلي:

getAllArticles
يعرض جميع العناصر الموجودة في الجدول ARTICLES في قائمة كائنات [Article]
clearAllArticles
يفرغ الجدول ARTICLES
getArticleById
يعرض الكائن [Article] المحدد بواسطة مفتاحه الأساسي
ajouteArticle
يسمح بإضافة عنصر إلى الجدول ARTICLES
modifieArticle
يسمح بتعديل عنصر من الجدول [ARTICLES]
supprimerArticle
يسمح بحذف عنصر من الجدول [ARTICLES]
changerStockArticle
يسمح بتعديل مخزون عنصر من الجدول [ARTICLES]

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

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

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

package istia.st.articles.dao;

// الواردات
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;

public class ArticlesDaoSqlMap implements IArticlesDao {

   // الحقول
  private SqlMapClient sqlMap;

   // المُنشِئات
  public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }

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

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

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

  • الوصول إلى SGBD الذي توجد فيه المقالات
  • إدارة مجموعة من الاتصالات
  • إدارة المعاملات

في مثالنا، سيُسمى هذا الملف [sqlmap-config-firebird.xml] وسيحدد الوصول إلى قاعدة بيانات Firebird:

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

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

يسمح ملف التكوين [articles.xml] المشار إليه أعلاه بتحديد كيفية إنشاء مثيل لفئة [istia.st.articles.dao.Article] من سطر في الجدول [ARTICLES] التابع لـ SGBD. كما يحدد الاستعلامات SQL التي ستسمح للطبقة [dao] بالحصول على البيانات من مصدر البيانات Firebird.

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

     <!-- اسم مستعار للفئة istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- التعيين ORM :  سطر الجدول ARTICLES - مثيل فئة Article -->
  <resultMap id="article" class="article">
    <result property="id" column="ID"/>
    <result property="nom" column="NOM"/>
    <result property="prix" column="PRIX"/>
    <result property="stockActuel" column="STOCKACTUEL"/>
    <result property="stockMinimum" column="STOCKMINIMUM"/>
  </resultMap>

     <!-- الاستعلام SQL للحصول على جميع المقالات -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- الاستعلام SQL لحذف جميع المقالات -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- الاستعلام SQL لإدراج مادة -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#name#,#price#,#currentStock#,#minStock#)
</statement>

     <!-- الاستعلام SQL لحذف منتج معين -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- الاستعلام SQL لتعديل منتج معين -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#الاسم#,
    prix=#السعر#،المخزون_الحالي=#المخزون_الحالي#،المخزون_الحد_الأدنى=#المخزون_الحد_الأدنى# حيث
    id=#id#
</statement>

     <!-- الاستعلام SQL للحصول على منتج معين -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- الاستعلام SQL لتعديل مخزون منتج معين -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#حركة#
    where id=#id# و stockActuel+#mouvement#&gt;=0
</statement>
</sqlMap>

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

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

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

 package istia.st.articles.domain;

// الواردات
import java.util.ArrayList;
import java.util.List;

public abstract interface IArticlesDomain {

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

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

package istia.st.articles.domain;

// الاستيرادات
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.List;

public class AchatsArticles implements IArticlesDomain {

   // الحقول
  private IArticlesDao articlesDao;
  private ArrayList erreurs;

   // المنشئون
  public AchatsArticles(IArticlesDao articlesDao) { }

   // الأساليب
  public ArrayList getErreurs() {}
  public List getAllArticles() {}
  public Article getArticleById(int id) {}
  public void acheter(Panier panier) { }
}

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

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

لإنشاء مثيل للفئة، يجب توفير الكائن الذي يسمح بالوصول إلى بيانات SGBD:

public AchatsArticles(IArticlesDao articlesDao)
المنشئ

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

package istia.st.articles.domain;

public class Achat {

   // الحقول
  private Article article;
  private int qte;

  // المنشئون
  public Achat(Article article, int qte) { }

  // الطرق
  public double getTotal() {}
  public Article getArticle() {}
  public void setArticle(Article article) { }
  public int getQte() {}
  public void setQte() { }
  public String toString() {}
}

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

article
المنتج الذي تم شراؤه
qte
الكمية المشتراة
double getTotal()
يعرض مبلغ الشراء
String toString()
سلسلة هوية الكائن

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

package istia.st.articles.domain;

// الاستيرادات
import java.util.ArrayList;

public class Panier {

  // الحقول
  private ArrayList achats;

   // المنشئون
  public Panier() { }

   // الأساليب
  public ArrayList getAchats() {}
  public void ajouter(Achat unAchat) { }
  public void enlever(int idAchat) { }
  public double getTotal() {}
  public String toString() { }
}

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

achats
قائمة مشتريات العميل - قائمة كائنات من النوع [Achat]
void ajouter(Achat unAchat)
إضافة عملية شراء إلى قائمة المشتريات
void enlever(int idArticle)
إزالة المشتريات من العنصر idArticle
double getTotal()
يعرض المبلغ الإجمالي للمشتريات
String toString()
يعرض سلسلة معرف سلة التسوق
ArrayList getAchats()
يعرض قائمة المشتريات

سيتم العثور على رمز الحزمة [domain] في الملحق.

3.3.5. الحزمة [istia.st.articles.exception]

تحتوي هذه الحزمة على الفئة التي تحدد الاستثناء الذي تطلقه الطبقة [dao] عند مواجهة مشكلة في الوصول إلى مصدر البيانات:

package istia.st.articles.exception;

public class UncheckedAccessArticlesException
    extends RuntimeException {

  public UncheckedAccessArticlesException() {
    super();
  }

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

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

3.3.6. اختبارات النموذج

تم اختبار النموذج M في Eclipse باستخدام التكوين التالي:

Image

تعليقات:

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

// اختبار الفئة ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {

     // مثيل للفئة التي يتم اختبارها
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // استرداد مثيل للوصول إلى البيانات
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }

    public void testGetAllArticles() {
         // يعرض المقالات
        listArticles();
    }

    public void testClearAllArticles() {
         // يفرغ جدول المقالات
        articlesDao.clearAllArticles();
         // قراءة الجدول ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
    }

    public void testAjouteArticle() {
         // حذف محتوى ARTICLES
        articlesDao.clearAllArticles();
         // قراءة الجدول ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // إدراج
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // قراءة الجدول ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         //يعرضها
        listArticles();
    }

    public void testSupprimeArticle() {
         // حذف محتوى ARTICLES
        articlesDao.clearAllArticles();
         // يقرأ الجدول ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // إدراج
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // قراءة الجدول ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // حذف
        articlesDao.supprimeArticle(4);
         // يقرأ الجدول ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(1, articles.size());
         // يعرض الجدول
        listArticles();
    }

    public void testModifieArticle() {
         // حذف محتوى ARTICLES
        articlesDao.clearAllArticles();
         // يقرأ الجدول ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // إدراج
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // قراءة الجدول ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
         // تعديل
        articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
         // getById
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getPrix(), 44, 1e-6);
         // يعرض الجدول
        listArticles();
    }

    public void testGetArticleById() {
         // حذف محتوى ARTICLES
        articlesDao.clearAllArticles();
         // قراءة الجدول ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // إدراج
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // قراءة الجدول ARTICLES
        articles = articlesDao.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

    private void listArticles() {
         // يقرأ الجدول ARTICLES
        List articles = articlesDao.getAllArticles();
         // يتم عرض المقالات التي تمت قراءتها
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

    public void testChangerStockArticle() throws InterruptedException {
         // حذف محتوى ARTICLES
        articlesDao.clearAllArticles();
         // إدراج
        int nbArticles = articlesDao.ajouteArticle(new Article(3, "article3",
                30, 101, 3));
        assertEquals(nbArticles, 1);
        nbArticles = articlesDao.ajouteArticle(new Article(4, "article4", 40,
                40, 4));
        assertEquals(nbArticles, 1);
         // إنشاء 100 مؤشر ترابط لتحديث مخزون المادة 3
        Thread[] taches = new Thread[100];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
            taches[i].start();
        }
         // في انتظار انتهاء سلاسل العمليات
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // استرداد المادة 3 والتحقق من مخزونها
        Article unArticle = articlesDao.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        assertEquals(1, unArticle.getStockActuel());
         // تعديل مخزون المادة 4
        boolean erreur = false;
        int nbLignes = articlesDao.changerStockArticle(4, -100);
        assertEquals(0, nbLignes);
         // عرض الجدول
        listArticles();
    }
}

تعليقات:

  • تقوم فئة الاختبار، بفضل أسلوبها setUp، بتخزين مثيل للفئة المراد اختبارها:
1
2
3
4
5
6
7
8
     // مثيل للفئة التي تم اختبارها
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // استرداد مثيل الوصول إلى البيانات
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao.xml"))).getBean("articlesDao");
    }
  • يتم توفير الكائن المراد اختباره بواسطة [Spring]. أعلاه، يتم طلب حبة Spring المسماة [articlesDao]. يتم تعريف هذه الحبة في ملف تكوين Spring [spring-config-test-dao.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- فئة الوصول إلى البيانات -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
</beans>

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

تقوم الطريقة [testChangerStockArticle] بإنشاء 100 مؤشر ترابط مكلفة بتخفيض مخزون عنصر معين. يتعلق الأمر هنا باختبار الوصول المتزامن إلى SGBD. نظرًا لأن الطريقة [changerStockArticle] للفئة [istia.st.articles.dao.ArticlesDaoSqlMap] قد تمت مزامنتها، فإن هذا الاختبار ينجح. إذا أزلنا المزامنة، فلن ينجح الاختبار. الفئة المسؤولة عن تحديث المخزون هي التالية:

package istia.st.articles.tests;

import istia.st.articles.dao.IArticlesDao;

public class ThreadMajStock extends Thread {

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

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

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

    /**
     * décrémente le stock de l'article 3 d'une unité fait un suivi écran des
     * opérations
     */
    public void run() {
         // المتابعة
        System.out.println(name + " lancé");
         // تعديل مخزون المادة 3
        articlesDao.changerStockArticle(3, -1);
         // المتابعة
        System.out.println(name + " terminé");
    }
}
  • تقوم الفئة المذكورة أعلاه بخصم 1 من مخزون المادة رقم 3

3.3.6.2. اختبارات الطبقة [domain]

فئة الاختبار JUnit للطبقة [domain] هي التالية:

package istia.st.articles.tests.domain;

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

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

// اختبار الفئة ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {

     // مثيل لفئة الوصول إلى المجال
    private IArticlesDomain articlesDomain;

     // مثيل لفئة الوصول إلى البيانات
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // استرداد مثيل الوصول إلى المجال
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // استرداد مثيل الوصول إلى البيانات
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }

     // استرداد مقال معين
    public void testGetArticleById() {
         // حذف محتوى ARTICLES
        articlesDao.clearAllArticles();
         // قراءة الجدول ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // إدراج
        articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
        articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
         // قراءة الجدول ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // getById
        Article unArticle = articlesDomain.getArticleById(3);
        assertEquals(unArticle.getNom(), "article3");
        unArticle = articlesDao.getArticleById(4);
        assertEquals(unArticle.getNom(), "article4");
    }

     // عرض الشاشة
    private void listArticles() {
         // يقرأ الجدول ARTICLES
        List articles = articlesDomain.getAllArticles();
         // يتم عرض العناصر التي تمت قراءتها
        for (int i = 0; i < articles.size(); i++) {
            System.out.println(((Article) articles.get(i)).toString());
        }
    }

     // شراء المقالات
    public void testAchatPanier(){
         // حذف محتوى ARTICLES
        articlesDao.clearAllArticles();
         // قراءة الجدول ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // إدراج
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // قراءة الجدول ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // إنشاء سلة تسوق تحتوي على مشتريين
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // التحقق
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // تأكيد سلة التسوق
        articlesDomain.acheter(panier);
         // التحقق
        assertEquals(0,articlesDomain.getErreurs().size());
        assertEquals(0,panier.getAchats().size());
         // البحث عن المنتج رقم 3
        article3=articlesDomain.getArticleById(3);
        assertEquals(20,article3.getStockActuel());
         // البحث عن المنتج رقم 4
        article4=articlesDomain.getArticleById(4);
        assertEquals(30,article4.getStockActuel());
         // سلة تسوق جديدة
        panier.ajouter(new Achat(article3,100));
         // تأكيد سلة التسوق
        articlesDomain.acheter(panier);
         // التحقق - تم شراء أكثر من اللازم
         // يجب أن يكون هناك خطأ
        assertEquals(1,articlesDomain.getErreurs().size());
         // البحث عن المنتج رقم 3
        article3=articlesDomain.getArticleById(3);
         // لا بد أن المخزون لم يتغير
        assertEquals(20,article3.getStockActuel());    
    }

     // إلغاء المشتريات
    public void testRetirerAchats(){
         // حذف محتوى ARTICLES
        articlesDao.clearAllArticles();
         // قراءة الجدول ARTICLES
        List articles = articlesDao.getAllArticles();
        assertEquals(0, articles.size());
         // إدراج
        Article article3=new Article(3, "article3", 30, 30, 3);
        articlesDao.ajouteArticle(article3);
        Article article4=new Article(4, "article4", 40, 40, 4);
        articlesDao.ajouteArticle(article4);
         // قراءة الجدول ARTICLES
        articles = articlesDomain.getAllArticles();
        assertEquals(2, articles.size());
         // إنشاء سلة تسوق تحتوي على مشتريين
        Panier panier=new Panier();
        panier.ajouter(new Achat(article3,10));
        panier.ajouter(new Achat(article4,10));
         // التحقق
        assertEquals(700.0,panier.getTotal(),1e-6);
        assertEquals(2,panier.getAchats().size());
         // إضافة منتج تم شراؤه مسبقًا
        panier.ajouter(new Achat(article3,10));
         // عمليات التحقق
         // يجب رفع المجموع إلى 1000
        assertEquals(1000.0,panier.getTotal(),1e-6);
         // لا يزال هناك منتجان في السلة
        assertEquals(2,panier.getAchats().size());
         // يجب أن تكون كمية المنتج 3 قد تغيرت إلى 20
        Achat achat=(Achat)panier.getAchats().get(0);
        assertEquals(20,achat.getQte());
         // إزالة المنتج 3 من سلة التسوق
        panier.enlever(3);
         // التحقق
         // يجب أن يكون المجموع قد تغير إلى 400
        assertEquals(400.0,panier.getTotal(),1e-6);
         // عنصر واحد فقط في سلة التسوق
        assertEquals(1,panier.getAchats().size());
         // يجب أن يكون هذا المنتج رقم 4
        assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
    }
}

تعليقات:

  • تقوم فئة الاختبار، بفضل طريقتها setUp، بتخزين مثيل للفئة المراد اختبارها بالإضافة إلى مثيل لفئة الوصول إلى البيانات. هذه النقطة الأخيرة مثيرة للجدل. من الناحية النظرية، لا ينبغي أن تحتاج فئة الاختبار إلى الوصول إلى الطبقة [dao] التي لا يُفترض بها حتى أن تعرفها. هنا، تجاوزنا هذه "الأخلاقيات" التي، لاحترامها، كانت ستجبرنا على إنشاء طرق جديدة في واجهتنا [IArticlesDomain].
     // مثيل لفئة الوصول إلى المجال
    private IArticlesDomain articlesDomain;

     // مثيل لفئة الوصول إلى البيانات
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
         // يسترد مثيل الوصول إلى المجال
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");
         // يسترد مثيل الوصول إلى البيانات
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");
    }
  • يتم توفير الكائن المراد اختباره بواسطة [Spring]. أعلاه، نطلب حبة Spring المسماة [articlesDomain]. يتم تعريف هذه الحبة في ملف تكوين Spring [spring-config-test-domain.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- فئة الوصول إلى البيانات -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- فئة الأعمال -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

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

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

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

1
2
3
4
         // يسترد مثيل الوصول إلى المجال
        articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
                new ClassPathResource("spring-config-test-domain.xml")))
                .getBean("articlesDomain");

ثم يتم استرداد bean [articlesDomain]. عند الاستدعاء الثاني لـ Spring:

1
2
3
         // يسترد مثيل الوصول إلى البيانات
        articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-domain.xml"))).getBean("articlesDao");

يكتفي [Spring] بإرجاع مرجع إلى bean [articlesDao] الذي تم إنشاؤه بالفعل عند الاستدعاء السابق. هذا هو مبدأ singleton. إذا طلبنا bean من Spring، فإنه يقوم بإنشاء مثيل له إذا لم يكن موجودًا بالفعل، وإلا فإنه يرجع مرجعًا إلى bean الموجود.

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 القائمة على وحدة تحكم servlet وصفحات JSP

ستكون بنية التطبيق MVC كما يلي:

M=modèle
فئات الأعمال، وفئات الوصول إلى البيانات، وقاعدة البيانات
V=vues
الصفحات JSP
C=contrôleur
برنامج الخدمة لمعالجة طلبات العملاء

3.5.1. النموذج

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

3.5.2. طرق العرض

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

LISTE
liste.jsp
تم تجميع العروض في المجلد [vues] الخاص بالتطبيق
INFOS
infos.jsp
PANIER
panier.jsp
PANIERVIDE
paniervide.jsp
ERREURS
erreurs.jsp

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

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

requête
المعنى
إجراء وحدة التحكم
الردود المحتملة
action=liste
يريد العميل قائمة
المنتجات
- يطلب قائمة المواد من الطبقة
الأعمال
- [LISTE]
- [ERREURS]
action=infos
يطلب العميل
معلومات عن أحد العناصر المعروضة في العرض
[LISTE]
- يطلب العنصر من طبقة الأعمال
- [INFOS]
- [ERREURS]
action=achat
العميل يشتري منتجًا
- يطلب المنتج من الطبقة التشغيلية و
يضيفها إلى سلة التسوق الخاصة به
- [INFOS] في حالة وجود خطأ في الكمية
- [LISTE] في حالة عدم وجود خطأ
action=retirerachat
يريد العميل حذف
من سلة التسوق
- استرداد سلة التسوق من الجلسة وتعديلها
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=panier
يريد العميل عرض
سلة التسوق
- استرداد سلة التسوق من الجلسة
- [PANIER]
- [PANIERVIDE]
- [ERREURS]
action=validationpanier
أنهى العميل عملية الشراء
وينتقل إلى مرحلة الدفع
- يقوم بتحديث قاعدة بيانات المخزون
المنتجات المشتراة
- يفرغ سلة التسوق الخاصة بالعميل من المنتجات التي
تم تأكيد شرائها
- [LISTE]
- [ERREURS]

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

سنحاول تكوين التطبيق بحيث يكون أكثر مرونة قدر الإمكان تجاه التغييرات مثل:

  1. تغيير عناوين URL لمختلف العروض
  2. تغيير الفئات التي تنفذ واجهات [IArticlesDao] و [IArticlesDomain]
  3. تغيير SGBD، وقاعدة البيانات، وجدول المقالات

3.5.5. تغييرات عناوين URL

سيتم وضع أسماء عناوين URL للعروض في ملف [web.xml] الخاص بتكوين التطبيق مع بعض المعلمات الأخرى:

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

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

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

يوجد في [web.xml]

  • عناوين URL لمختلف طرق عرض التطبيق
  • اسم ملف تكوين Spring [springConfigFileName] الذي سيسمح بإنشاء كائنات singleton للوصول إلى طبقات الأعمال و Dao
  • الطريقة [/vues/index.jsp] التي سيتم عرضها عندما يكون عنوان URL المطلوب من قبل العميل هو /<context> حيث <context> هو سياق التطبيق

3.5.6. تغيير فئات تنفيذ الواجهات

وفقًا لفلسفة البنى ثلاثية الطبقات، يجب أن تكون الطبقات منفصلة عن بعضها البعض. يتم تحقيق هذا الانفصال بالطريقة التالية:

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

في تطبيقنا، قد يكون ملف تكوين Spring كما يلي:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- فئة الوصول إلى البيانات -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- فئة الأعمال -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
</beans>

للوصول إلى الطبقة [métier]، يمكن لفئة من الطبقة [Interface utilisateur, UI] أن تطلب bean [articlesDomain]. سيقوم Spring عندئذٍ بإنشاء مثيل لكائن من النوع [istia.st.articles.domain.AchatsArticles]. ولهذا الإنشاء، يحتاج إلى bean من النوع [articlesDao]، أي كائن من النوع [istia.st.articles.dao.ArticlesDaoSqlMap]. سيقوم Spring بعد ذلك بإنشاء مثيل لهذا الكائن. سيتم هذا الإنشاء استنادًا إلى المعلومات الموجودة في الملف [sqlmap-config-firebird.xml]، وهو ملف تكوين للوصول إلى البيانات بواسطة SqlMap. في نهاية العملية، تمتلك الفئة [UI] التي طلبت bean [articlesDomain] السلسلة الكاملة التي تربطها ببيانات SGBD:

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

يتم ضمان استقلالية تطبيق الويب عن التغييرات المتعلقة بـ SGBD أو قاعدة البيانات هنا من خلال ملفات تكوين SqlMap. وهناك ملفان:

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

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

يشير هذا الملف هنا إلى قاعدة بيانات Firebird. يكفي تغيير اسم برنامج التشغيل JDBC للعمل مع برنامج تشغيل آخر SGBD.

  1. الملف [articles.xml] الذي يجمع التعليمات المختلفة SQL اللازمة للتطبيق:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">

     <!-- اسم مستعار لفئة istia.st.articles.dao.Article -->
  <typeAlias alias="article" type="istia.st.articles.dao.Article"/>

     <!-- التعيين ORM :  سطر الجدول ARTICLES - مثيل فئة Article -->
  <resultMap id="article" class="article">
    <result property="id" column="ID"/>
    <result property="nom" column="NOM"/>
    <result property="prix" column="PRIX"/>
    <result property="stockActuel" column="STOCKACTUEL"/>
    <result property="stockMinimum" column="STOCKMINIMUM"/>
  </resultMap>

     <!-- الاستعلام SQL للحصول على جميع المقالات -->
  <statement id="getAllArticles" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum from ARTICLES
</statement>

     <!-- الاستعلام SQL لحذف جميع المقالات -->
  <statement id="clearAllArticles">delete from ARTICLES</statement>

     <!-- الاستعلام SQL لإدراج مادة -->
  <statement id="insertArticle">
    insert into ARTICLES (id, nom, prix,
    stockactuel, stockminimum) values
    (#id#,#name#,#price#,#currentStock#,#minStock#)
</statement>

     <!-- الاستعلام SQL لحذف منتج معين -->
  <statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>

     <!-- الاستعلام SQL لتعديل منتج معين -->
  <statement id="modifyArticle">
    update ARTICLES set nom=#الاسم#,
    prix=#السعر#،المخزون_الحالي=#المخزون_الحالي#،المخزون_الحد_الأدنى=#المخزون_الحد_الأدنى# حيث
    id=#id#
</statement>

     <!-- الاستعلام SQL للحصول على منتج معين -->
  <statement id="getArticleById" resultMap="article">
    select id, nom, prix,
    stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>

     <!-- الاستعلام SQL لتعديل مخزون منتج معين -->
  <statement id="changerStockArticle">
    update ARTICLES set
    stockActuel=stockActuel+#حركة#
    where id=#id# و stockActuel+#حركة#&gt;=0
</statement>
</sqlMap>

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

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

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

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

3.5.9. طرق العرض JSP

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

3.5.9.1. entete.jsp

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

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

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

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

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

عند التنفيذ، سيؤدي هذا العلامة إلى تضمين كود الصفحة JSP التي تحتوي عليها، كود الصفحة [entete.jsp]. ونظرًا لأن عنوان URL للصفحة هو عنوان URL نسبي (لا يحتوي على /)، فسيتم البحث عن الصفحة [entete.jsp] في نفس المجلد الذي توجد فيه الصفحة التي تحتوي على العلامة <jsp:include>.

الرمز:

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

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

3.5.9.2. liste.jsp

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

يتم عرضها بعد طلب /main?action=liste أو /main?action=validationpanier. عناصر طلب وحدة التحكم هي كما يلي:

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

كل رابط [Infos] في مصفوفة HTML للمقالات له عنوان URL بالصيغة [?action=infos&id=ID] حيث ID هو حقل معرف المقالة المعروضة.

الكود:

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

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

3.5.9.3. infos.jsp

تعرض هذه الصفحة معلومات عن أحد المنتجات وتتيح أيضًا شرائه:

Image

يتم عرضها بعد طلب /main?action=infos&id=ID أو طلب /main?action=achat&id=ID عندما تكون الكمية المشتراة خاطئة. عناصر طلب وحدة التحكم هي كما يلي:

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

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

Image

تحتوي هذه الصفحة على نموذج يتم إرساله عبر الزر [Acheter]. عنوان URL الهدف لـ POST هو [?action=achat&id=ID] حيث ID هو معرف المنتج الذي تم شراؤه.

الرمز:

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

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

3.5.9.4. panier.jsp

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

Image

يتم عرضها بعد طلب /main?action=panier أو /main?action=retirerachat&id=ID. عناصر طلب وحدة التحكم هي كما يلي:

actions
كائن Hashtable[] - جدول خيارات القائمة
panier
كائن من النوع [Panier] - سلة التسوق المراد عرضها

كل رابط [Retirer] في جدول HTML لمشتريات سلة التسوق له عنوان URL بالصيغة [?action=retirerachat&id=ID] حيث ID هو الحقل [id]المنتج الذي نريد إزالته من سلة التسوق.

الرمز:

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

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

3.5.9.5. paniervide.jsp

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

Image

يتم عرضها بعد طلب /main?action=panier أو /main?action=retirerachat&id=ID. عناصر طلب وحدة التحكم هي كما يلي:

actions
كائن Hashtable[] - جدول خيارات القائمة

الرمز:

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

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

3.5.9.6. erreurs.jsp

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

Image

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

actions
كائن Hashtable[] - جدول خيارات القائمة
erreurs
ArrayList كائنات String تمثل رسائل الأخطاء المراد عرضها

الرمز:

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

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

3.5.9.7. index.jsp

يتم تعريف هذه الصفحة كصفحة رئيسية للتطبيق في ملف [web.xml] الخاص بالتطبيق:

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

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

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

تكتفي طريقة العرض [index.jsp] بإعادة توجيه العميل إلى نقطة دخول التطبيق:

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

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

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

يبقى كتابة قلب تطبيق الويب الخاص بنا، وهو وحدة التحكم. وتتمثل مهمتها في:

  • استلام طلب العميل،
  • معالجة الإجراء المطلوب من قبله باستخدام فئات الأعمال،
  • إرسال العرض المناسب كرد.

3.5.10.1. تهيئة وحدة التحكم

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

  • الحقول الخاصة لوحدة التحكم
  • سياق تنفيذ التطبيق (ServletContext)

ستقوم الطريقة [init] للتطبيق [webarticles] بالخطوات التالية:

  • التحقق من وجود المعلمات اللازمة للتشغيل السليم للتطبيق في الملف [web.xml]. وقد تم وصف هذه المعلمات في الفقرة 3.5.5.
  • تعيين حقل خاص [ArrayList erreurs] بقائمة الأخطاء المحتملة. ستكون هذه القائمة فارغة في حالة عدم وجود أخطاء، ولكنها ستظل موجودة على أي حال.
  • في حالة وجود أخطاء، تتوقف الطريقة [init] عند هذا الحد. وإلا، فإنها تنشئ كائنًا من النوع [IArticlesDomain] والذي سيكون الكائن الوظيفي الذي سيستخدمه المتحكم لتلبية احتياجاته. كما تم شرحه في 3.5.6، سيطلب وحدة التحكم من إطار عمل Spring الحبة التي يحتاجها. قد تؤدي عملية الإنشاء هذه إلى أخطاء مختلفة. إذا كان الأمر كذلك، فسيتم، مرة أخرى، تخزينها في الحقل [erreurs] في وحدة التحكم.

3.5.10.2. الطريقتان doGet و doPost

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

  • سيتم التحقق من الحقل [erreurs]. إذا كان غير فارغ، فهذا يعني أنه حدثت أخطاء أثناء تهيئة التطبيق وأنه لا يمكن تشغيله. عندئذٍ سيتم إرسال العرض [ERREURS] كرد.
  • سيتم استرداد المعلمة [action] من الطلب والتحقق منها. إذا لم تتطابق مع إجراء معروف، يتم إرسال العرض [ERREURS] مع رسالة خطأ مناسبة.
  • إذا كان المعلمة [action] صالحة، يتم تمرير طلب العميل إلى إجراء خاص بالإجراء للمعالجة. سيكون توقيع الإجراء الذي يعالج الإجراء [uneAction] كما يلي:
1
2
3
4
5
6
7
8
/**
   * @param request la requête du client
   * @param response la réponse au client
   * @throws IOException
   * @throws ServletException
   */
  private void doUneAction(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException; 

3.5.10.3. معالجة الإجراءات المختلفة

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

الطريقة
طلب
المعالجة
الردود المحتملة
doListe
GET /main?action=liste
- طلب قائمة المواد
من فئة المهنة
- عرضها
[LISTE] أو [ERREURS]
doInfos
GET /main?action=infos&id=ID
- طلب المقالة ذات الرقم التعريفي id=ID من
فئة الأعمال
- عرضه
[INFOS] أو [ERREURS]
doAchat
POST /main?action=achat&id=ID
- الكمية المشتراة جزء من المعلمات المرسلة
- طلب المنتج ذي الرقم التعريفي id=ID من
فئة الأعمال
- إدراجها في سلة التسوق في
جلسة عمل العميل
[LISTE] أو [INFOS]
أو [ERREURS]
doRetirerAchat
GET /main?action=retirerachat&id=ID
- سحب المنتج ذي الرقم التعريفي id=ID من
قائمة مشتريات سلة
جلسة عمل العميل
[PANIER]
doPanier
GET /main?action=panier
- عرض سلة التسوق الخاصة بـ
جلسة العميل
[PANIER] أو [PANIERVIDE]
doValidationPanier
GET /main?action=validationpanier
- تخفيض المخزون في قاعدة البيانات
مخزون جميع المنتجات
الموجودة في سلة
العميل
[LISTE] أو [ERREURS]

3.5.10.4. الرمز

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

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

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

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

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

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

    public void init() {
         // يتم استرداد معلمات التهيئة للخادم الصغير
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // يتم حفظ الخطأ
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // أخطاء؟
        if (erreurs.size() != 0) {
            return;
        }
         // يتم إنشاء كائن IArticlesDomain للوصول إلى طبقة الأعمال
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // يتم تسجيل الخطأ
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // يتم حفظ بعض عناوين URL الخاصة بالتطبيق
        hActionListe.put("href", "?action=" + ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", "?action=" + ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put(
            "href",
            "?action=" + ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // انتهى الأمر
        return;
    }


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

         // نتحقق من كيفية سير عملية تهيئة السيرفليت
        if (erreurs.size() != 0) {
             // هل لدينا عنوان URL لصفحة الأخطاء؟
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // نقوم بعرض صفحة الأخطاء
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // النهاية
            return;
        }
         // معالجة الإجراء
        String action = request.getParameter("action");
        if (action == null) {
             // قائمة المقالات
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // قائمة المقالات
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // معلومات عن عنصر
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // شراء منتج
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // عرض سلة التسوق
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // حذف منتج من سلة التسوق
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // تأكيد سلة التسوق
            doValidationPanier(request, response);
            return;
        }
         // إجراء غير معروف
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // يتم عرض صفحة الأخطاء
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // النهاية
        return;
    }


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

         // أكد المشتري سلة التسوق
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // تتم المصادقة على سلة التسوق هذه
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // غير طبيعي
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // يتم استرداد الأخطاء
        ArrayList erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
            request.setAttribute(
                "actions",
                new Hashtable[] { hActionListe, hActionPanier });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // عرض قائمة المنتجات
        request.setAttribute("message", "Votre panier a été validé");
        doListe(request, response);
         // النهاية
        return;
    }


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

         // إزالة منتج من سلة التسوق
        try {
            Panier panier =
                (Panier) request.getSession().getAttribute("panier");
            String strIdAchat = request.getParameter("id");
            panier.enlever(Integer.parseInt(strIdAchat));
        } catch (NumberFormatException ignored) {
        } catch (NullPointerException ignored) {
        }
         // عرض سلة التسوق
        doPanier(request, response);
    }


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

         // عرض سلة التسوق
        Panier panier = (Panier) request.getSession().getAttribute("panier");
         // سلة التسوق فارغة؟
        if (panier == null || panier.getAchats().size() == 0) {
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
                .forward(request, response);
             // نهاية
            return;
        }
         // هناك شيء ما في سلة التسوق
        request.setAttribute("panier", panier);
        request.setAttribute(
            "actions",
            new Hashtable[] { hActionListe, hActionValidationPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_PANIER))
            .forward(request, response);
         // نهاية
        return;
    }


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

         // شراء منتج
         // استرداد الكمية
        int qté = 0;
        try {
            qté = Integer.parseInt(request.getParameter("qte"));
            if (qté <= 0)
                throw new NumberFormatException();
        } catch (NumberFormatException ex) {
             // الكمية خاطئة
            request.setAttribute("msg", "Quantité incorrecte");
            request.setAttribute("qte", request.getParameter("qte"));
            String url =
                config.getInitParameter(URL_MAIN)
                    + "?action=infos&id="
                    + request.getParameter("id");
            getServletContext().getRequestDispatcher(url).forward(
                request,
                response);
             // نهاية
            return;
        }
         // استرداد جلسة عمل العميل
        HttpSession session = request.getSession();
         // إنشاء عملية الشراء
        Article article = (Article) session.getAttribute("article");
        Achat achat = new Achat(article, qté);
         // إضافة عملية الشراء إلى سلة التسوق الخاصة بالعميل
        Panier panier = (Panier) session.getAttribute("panier");
        if (panier == null) {
            panier = new Panier();
            session.setAttribute("panier", panier);
        }
        panier.ajouter(achat);
         // العودة إلى قائمة المنتجات
        String url = config.getInitParameter(URL_MAIN) + "?action=liste";
        getServletContext().getRequestDispatcher(url).forward(
            request,
            response);
         // النهاية
        return;
    }


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

         // عرض قائمة المنتجات
        request.setAttribute("infos", infos);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_DEBUG))
            .forward(request, response);
         // النهاية
        return;
    }


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

         // كما في get
        doGet(request, response);
    }


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

         // قائمة الأخطاء
        ArrayList erreurs = new ArrayList();
         // استرداد المعرف المطلوب
        String strId = request.getParameter("id");
         // شيء ما؟
        if (strId == null) {
             // غير طبيعي
            erreurs.add("action incorrecte([infos,id=null]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // تحويل strId بالكامل
        int id = 0;
        try {
            id = Integer.parseInt(strId);
        } catch (Exception ex) {
             // غير طبيعي
            erreurs.add("action incorrecte([infos,id=" + strId + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // يُطلب عنصر معرف المفتاح
        Article article = null;
        try {
            article=articlesDomain.getArticleById(id);
        } catch (UncheckedAccessArticlesException ex) {
             // غير طبيعي
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
        if (article == null) {
             // غير طبيعي
            erreurs.add("Article de clé [" + id + "] inexistant");
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            afficheErreurs(request, response, erreurs);
            return;
        }
         // يتم وضع العنصر في الجلسة
        request.getSession().setAttribute("article", article);
         // يتم عرض صفحة المعلومات
        request.setAttribute("actions", new Hashtable[] { hActionListe });
         /   / request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_INFOS))
            .forward(request, response);
         // نهاية
        return;
    }

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

         // يتم عرض صفحة الأخطاء
        request.setAttribute("erreurs", erreurs);
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
            .forward(request, response);
         // نهاية
        return;
    }


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

         // قائمة الأخطاء
        ArrayList erreurs = new ArrayList();
         // يتم طلب قائمة العناصر
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // يتم حفظ الخطأ
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // هل توجد أخطاء؟
        if (erreurs.size() != 0) {
             // عرض صفحة الأخطاء
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // النهاية
            return;
        }
         // عرض قائمة المنتجات
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // النهاية
        return;
    }

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

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

3.5.10.5. اختبارات التطبيق

دعونا نعرض بعض لقطات الشاشة للاختبارات. أولاً، الصفحة الرئيسية للتطبيق:

Image

كان عنوان URL المطلوب في الواقع هو [http://localhost:8080/webarticles]. سيلاحظ القارئ أننا نحدد في الملف [web.xml] صفحة رئيسية للتطبيق:

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

يتم تعريف العرض [index.jsp] على النحو التالي:

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

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

لذلك تم إعادة التوجيه إلى عنوان URL [http://localhost:8080/webarticles/main?action=liste]، وهو ما يظهره عنوان URL للمتصفح في لقطة الشاشة. وبالتالي تم طلب عنوان URL [/main?action=liste]. وفي [web.xml] أيضًا، يرتبط عنوان URL /main بخدمة [webarticles]:

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

وفي [web.xml] أيضًا، ترتبط الخدمة [webarticles] بالخدمة [ istia.st.articles.web.WebArticles]:

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

وبالتالي، يتم تحميل الخدمة [ istia.st.articles.web.WebArticles] بواسطة حاوية الخدمات Tomcat إذا لم تكن قد تم تحميلها بعد، ويتم تنفيذ طريقتها [init]:

    public void init() {
         // استرداد معلمات تهيئة السيرفلت
        config = getServletConfig();
        String param = null;
        for (int i = 0; i < parameters.length; i++) {
            param = config.getInitParameter(parameters[i]);
            if (param == null) {
                 // يتم حفظ الخطأ
                erreurs.add(
                    "Paramètre ["
                        + parameters[i]
                        + "] absent dans le fichier [web.xml]");
            }
        }
         // أخطاء؟
        if (erreurs.size() != 0) {
            return;
        }
         // يتم إنشاء كائن IArticlesDomain للوصول إلى طبقة الأعمال
        try {
            articlesDomain =
                (IArticlesDomain)
                    (
                        new XmlBeanFactory(
                            new ClassPathResource(
                                (String) config.getInitParameter(
                                    SPRING_CONFIG_FILENAME)))).getBean(
                    "articlesDomain");
        } catch (Exception ex) {
             // يتم تخزين الخطأ
            erreurs.add(
                "Erreur de configuration de l'accès aux données : "
                    + ex.toString());
            return;
        }
         // يتم حفظ بعض عناوين URL الخاصة بالتطبيق
        hActionListe.put("href", "?action=" + ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", "?action=" + ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put(
            "href",
            "?action=" + ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // انتهى الأمر
        return;
    }

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

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

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

    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
         // التحقق من سير عملية تهيئة السيرفليت
        if (erreurs.size() != 0) {
             // هل لدينا عنوان URL لصفحة الأخطاء؟
            if (config.getInitParameter(URL_ERREURS) == null) {
                throw new ServletException(erreurs.toString());
            }
             // نقوم بعرض صفحة الأخطاء
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] {
            });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // النهاية
            return;
        }
         // معالجة الإجراء
        String action = request.getParameter("action");
        if (action == null) {
             // قائمة المقالات
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_LISTE)) {
             // قائمة المقالات
            doListe(request, response);
            return;
        }
        if (action.equals(ACTION_INFOS)) {
             // معلومات عن عنصر
            doInfos(request, response);
            return;
        }
        if (action.equals(ACTION_ACHAT)) {
             // شراء منتج
            doAchat(request, response);
            return;
        }
        if (action.equals(ACTION_PANIER)) {
             // عرض سلة التسوق
            doPanier(request, response);
            return;
        }
        if (action.equals(ACTION_RETIRER_ACHAT)) {
             // حذف منتج من سلة التسوق
            doRetirerAchat(request, response);
            return;
        }
        if (action.equals(ACTION_VALIDATION_PANIER)) {
             // تأكيد سلة التسوق
            doValidationPanier(request, response);
            return;
        }
         // إجراء غير معروف
        ArrayList erreurs = new ArrayList();
        erreurs.add("action [" + action + "] inconnue");
         // يتم عرض صفحة الأخطاء
        request.setAttribute("actions", new Hashtable[] { hActionListe });
        afficheErreurs(request, response, erreurs);
         // نهاية
        return;
    }
  • تبدأ الطريقة [doGet] بالتحقق مما إذا كانت هناك أخطاء في التهيئة عند انتهاء الطريقة [init]. إذا كان الأمر كذلك، فإنها تعرض طريقة العرض [ERREURS] وينتهي الأمر.
  • وإلا، فإنها تسترد المعلمة [action] من طلب العميل. تجدر الإشارة إلى أن التطبيق تم تصميمه للاستجابة للطلبات التي تحتوي بالضرورة على المعلمة [action].
  • يقوم بتنفيذ الطريقة المرتبطة بالإجراء. هنا ستكون الطريقة [doListe].

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

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

         // قائمة الأخطاء
        ArrayList erreurs = new ArrayList();
         // يتم طلب قائمة العناصر
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // يتم حفظ الخطأ
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
         // هل توجد أخطاء؟
        if (erreurs.size() != 0) {
             // عرض صفحة الأخطاء
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // النهاية
            return;
        }
         // عرض قائمة المنتجات
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);
         // النهاية
        return;
    }
  • تذكر أن الطريقة [init] قد حفظت خدمة الوصول إلى نموذج التطبيق (طبقة المجال) في حقل خاص في السيرفلت:
     // الحقول الخاصة
    private IArticlesDomain articlesDomain = null;
  • من خلال خدمة الوصول هذه، يمكن طلب قائمة المقالات:
         // قائمة الأخطاء
        ArrayList erreurs = new ArrayList();
         // طلب قائمة المواد
        List articles = null;
        try {
            articles = articlesDomain.getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // تسجيل الخطأ
            erreurs.add(
                "Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
        }
  • في حالة حدوث أخطاء، يتم إرسال العرض [ERREURS]:
         // أخطاء؟
        if (erreurs.size() != 0) {
             // عرض صفحة الأخطاء
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { hActionListe });
            getServletContext()
                .getRequestDispatcher(config.getInitParameter(URL_ERREURS))
                .forward(request, response);
             // النهاية
            return;
        }
  • وإلا يتم إرسال العرض [LISTE]:
1
2
3
4
5
6
7
         // عرض قائمة المنتجات
        request.setAttribute("listarticles", articles);
        request.setAttribute("message","");
        request.setAttribute("actions", new Hashtable[] { hActionPanier });
        getServletContext()
            .getRequestDispatcher(config.getInitParameter(URL_LISTE))
            .forward(request, response);

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

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

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

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

يمكن للمشتري شراء المنتج رقم 3 هنا. لنقم بخطأ في إدخال الكمية:

تم الإبلاغ عن الخطأ. الآن، لنشتري بعض المنتجات:

تم تسجيل عملية الشراء وإعادة عرض قائمة المنتجات. دعونا نتحقق من سلة التسوق:

العملية موجودة بالفعل في سلة التسوق. لنقم بإزالتها:

تمت إزالة المنتج من سلة التسوق وإعادة عرضها. هنا، هي فارغة.

لنشتري 100 قطعة من المنتج رقم 3 و2 قطعة من المنتج رقم 4:

تبين أن شراء المنتج رقم 3 غير ممكن لأننا أردنا شراء 100 قطعة ولم يكن هناك سوى 30 قطعة في المخزون. بقي هذا الشراء في سلة التسوق:

أما المنتج رقم 4 فقد تم شراؤه كما يظهر من رصيده الجديد البالغ 39 (40-1):

3.6. بنية MVC مع Struts

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

لنعد إلى بنية التطبيق MVC:

في الإصدار السابق:

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

في إصدار Struts:

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

سنكتشف أن الانتقال إلى Struts من التطبيق السابق يتكون من المهام التالية:

  • يتم الآن معالجة الإجراءات التي كانت تُعالج في أساليب معينة في السيرفلت/المحرك من خلال مثيلات لفئات مشتقة من فئة [Action] في Struts
  • اكتب ملفات التكوين [web.xml] و [struts-config.xml]
  • إجراء بعض التعديلات على الصفحات JSP

لنتذكر البنية العامة MVC المستخدمة بواسطة STRUTS:

M=modèle
فئات الأعمال، وفئات الوصول إلى البيانات، وقاعدة البيانات
V=vues
الصفحات JSP
C=contrôleur
برنامج الخدمة لمعالجة طلبات العملاء، والكائنات [Action] والبنات [ActionForm] المرتبطة بالنماذج.
  • وحدة التحكم هي قلب التطبيق. تمر جميع طلبات العميل عبرها. وهي عبارة عن سيرفلت عامة مقدمة من STRUTS. قد نضطر في بعض الحالات إلى اشتقاقه. في الحالات البسيطة، لا يكون ذلك ضروريًا. تأخذ هذه الخدمة العامة المعلومات التي تحتاجها من ملف يُسمى غالبًا struts-config.xml.
  • إذا احتوى طلب العميل على معلمات نموذج، يتم وضعها بواسطة وحدة التحكم في كائن Bean. يتم تخزين كائنات Bean التي يتم إنشاؤها بمرور الوقت في جلسة العمل أو طلب العميل. هذه النقطة قابلة للتكوين. لا يلزم إعادة إنشائها إذا كانت قد تم إنشاؤها بالفعل.
  • في ملف التكوين struts-config.xml، لكل URL يجب معالجته برمجيًا (وبالتالي لا يتطابق مع عرض JSP يمكن طلبه مباشرةً) نربط بعض المعلومات:
    • اسم فئة النوع Action المسؤولة عن معالجة الطلب. وهنا أيضًا، يمكن الاحتفاظ بكائن Action الذي تم إنشاء مثيل له في الجلسة أو الطلب.
    • إذا تم تكوين URL المطلوب (في حالة إرسال نموذج إلى وحدة التحكم)، يتم الإشارة إلى اسم bean المسؤول عن حفظ معلومات النموذج.
  • بفضل هذه المعلومات المقدمة من ملف التكوين الخاص به، عند استلام طلب URL من قبل عميل، يكون وحدة التحكم قادرة على تحديد ما إذا كان هناك bean يجب إنشاؤه وأي واحد. بمجرد إنشائه، يمكن لـ bean التحقق من صحة البيانات التي خزنها والتي تأتي من النموذج. يتم استدعاء طريقة في الكائن تسمى validate تلقائيًا بواسطة وحدة التحكم. يتم إنشاء الكائن بواسطة المطور. لذلك يضع المطور في طريقة validate الكود الذي يتحقق من صحة بيانات النموذج. إذا تبين أن البيانات غير صالحة، فلن تواصل وحدة التحكم العملية. ستقوم بتسليم زمام الأمور إلى عرض ستجد اسمه في ملف التكوين الخاص بها. عندئذٍ ينتهي التبادل. تجدر الإشارة إلى أنه يمكن للمطور أن يطلب عدم التحقق من صحة النموذج. ويقوم بذلك أيضًا في الملف struts-config.xml. في هذه الحالة، لا يستدعي وحدة التحكم طريقة validate الخاصة بالبيان.
  • إذا كانت بيانات الكائن صحيحة، أو إذا لم يكن هناك تحقق، أو إذا لم يكن هناك كائن، فإن وحدة التحكم تمرر المهمة إلى الكائن من نوع Action المرتبط بـ URL. ويقوم بذلك عن طريق طلب تنفيذ طريقة execute لهذا الكائن، حيث يمرر إليه مرجع الكائن الذي قام بإنشائه في حالة وجوده. وهنا يقوم المطور بما عليه القيام به: قد يضطر إلى استدعاء فئات الأعمال أو فئات الوصول إلى البيانات. في نهاية المعالجة، يعيد كائن Action إلى وحدة التحكم اسم العرض الذي يجب إرساله كاستجابة للعميل.
  • في ملف التكوين الخاص به، سيجد وحدة التحكم كائن URL المرتبط باسم العرض الذي طُلب منه عرضه. ثم يرسل هذا العرض. وبذلك ينتهي التبادل مع العميل.

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

M=modèle
فئات الأعمال، وفئات الوصول إلى البيانات، وقاعدة البيانات
V=vues
الصفحات JSP
C=contrôleur
خدمة معالجة طلبات العملاء، الكائنات [Action]

3.6.2. النموذج

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

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

3.6.3.1. البنية العامة

الهندسة العامة لمشروع Eclipse هي كما يلي:

Image

3.6.3.2. تكوين الوصول إلى البيانات

نظرًا لأن واجهة الوصول إلى البيانات لا تتغير، فإن ملفات التكوين المرتبطة بها هي نفسها الموجودة في الإصدار السابق. وهي محددة في [WEB-INF/src]:

Image

في لقطة الشاشة أعلاه، ملفات [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] هي ملفات الإصدار السابق.

3.6.3.3. دليل الأرشيفات

في [WEB-INF/lib]، نجد نفس الأرشيفات الموجودة في الإصدار السابق، بالإضافة إلى تلك اللازمة لـ Struts:

Image

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

يتم تكوين التطبيق باستخدام ملفين: [web.xml, struts-config.xml] في المجلد [WEB-INF]:

Image

الملف [web.xml] هو التالي:

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

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

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

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

  • الصفحة الرئيسية للتطبيق هي [vues/index.jsp] (ملف الترحيب)
  • سيتم إعادة توجيه طلبات عناوين URL التي تتخذ صيغة *.do إلى السيرفلت [strutswebarticles] (تعيين السيرفلت)
  • الخادم [strutswebarticles] هو مثيل لفئة [ istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
  • تقبل هذه الخدمة معلمتين للتهيئة
    • اسم ملف تكوين Struts (config)
    • اسم ملف تكوين Spring (springConfigFileName)

ملف [struts-config.xml] هو التالي:

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

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

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

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

  • أن وحدة التحكم الخاصة بنا ستعالج عناوين URL التالية:
main.do
لعرض قائمة المقالات
liste.do
لعرض قائمة المقالات
infos.do
لعرض معلومات عن منتج معين
achat.do
لشراء منتج معين
panier.do
لعرض سلة التسوق
retirerachat.do
لإزالة منتج من سلة التسوق
validerpanier.do
لتأكيد سلة التسوق
  • تتوافق الإجراءات السابقة واحدة تلو الأخرى مع الإجراءات التي عالجتها الخدمة في الإصدار السابق. ويتم تحديد المعلومات التالية لكل منها:
  • اسم الفئة المسؤولة عن معالجة هذا الإجراء
  • الاستجابات (= العروض) الممكنة بعد معالجة الإجراء. سيتم اختيار واحدة منها فقط بواسطة وحدة التحكم.
  • اسم ملف الرسائل الخاص بالتطبيق (message-resources). هنا، سيكون الملف موجودًا ولكنه فارغ. ولن يتم استخدامه. يجب وضعه في [ClassPath] الخاص بالتطبيق. هنا سيتم وضعه في [WEB-INF/classes]. في Eclipse، نحصل على هذه النتيجة بوضعه في [WEB-INF/src]:

Image

3.6.4. طرق العرض JSP

طرق العرض JSP المستخدمة هي أيضًا تلك الموجودة في الإصدار السابق. لم يتم تغيير سوى القليل جدًا: يتعلق الأمر بعناوين URL من النوع [?action=XX?id=YY& ...] التي أصبحت [/XX.do?id=YY&....]. نعيد هنا الشروحات التي سبق تقديمها لتجنب عودة المستخدم إلى الوراء. من المهم فهم أن المعلومات التي يرسلها وحدة التحكم إلى العرض هي نفسها تمامًا في كلا الإصدارين. لم يتغير شيء في هذا الصدد.

3.6.4.1. entete.jsp

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

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

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

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

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

عند التنفيذ، سيؤدي هذا العلامة إلى تضمين كود الصفحة JSP التي تحتوي عليها، كود الصفحة [entete.jsp]. نظرًا لأن عنوان URL للصفحة هو عنوان URL نسبي (لا يحتوي على /)، فسيتم البحث عن الصفحة [entete.jsp] في نفس المجلد الذي توجد فيه الصفحة التي تحتوي على العلامة <jsp:include>.

الكود:

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

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

تعليقات: لا توجد تغييرات مقارنة بالإصدار السابق

3.6.4.2. liste.jsp

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

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

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

كل رابط [Infos] في مصفوفة HTML للمقالات له عنوان URL بالصيغة [/infos.do?id=ID] حيث ID هو حقل معرف المقالة المعروضة.

الكود:

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

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

تعليقات: تغيير (موضح أعلاه)

3.6.4.3. infos.jsp

تعرض هذه الصفحة معلومات عن منتج ما وتتيح أيضًا شرائه:

Image

يتم عرضها بعد طلب /infos.do?id=ID أو طلب /achat.do?id=ID عندما تكون الكمية المشتراة خاطئة. عناصر طلب وحدة التحكم هي كما يلي:

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

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

Image

تحتوي هذه الصفحة على نموذج يتم إرساله عبر الزر [Acheter]. عنوان URL الهدف لـ POST هو [/achat.do?id=ID] حيث ID هو معرف المنتج الذي تم شراؤه.

الرمز:

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

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

تعليقات: تغيير واحد (موضح أعلاه)

3.6.4.4. panier.jsp

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

Image

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

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

كل رابط [Retirer] في جدول HTML لمشتريات سلة التسوق له عنوان URL بالصيغة [retirerachat.do?id=ID] حيث ID هو الحقل [id]المنتج الذي نريد إزالته من سلة التسوق.

الرمز:

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

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

تعليقات: تغيير (موضح أعلاه)

3.6.4.5. paniervide.jsp

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

Image

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

actions
كائن Hashtable[] - جدول خيارات القائمة

الرمز:

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

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

تعليقات: لا توجد تغييرات.

3.6.4.6. erreurs.jsp

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

Image

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

actions
كائن Hashtable[] - جدول خيارات القائمة
erreurs
ArrayList كائنات String تمثل رسائل الأخطاء المراد عرضها

الكود:

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

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

تعليقات: لا توجد تغييرات.

3.6.4.7. index.jsp

تم تعريف هذه الصفحة كصفحة رئيسية للتطبيق في ملف [web.xml] الخاص بالتطبيق:

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

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

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

تكتفي طريقة العرض [index.jsp] بإعادة توجيه العميل إلى نقطة دخول التطبيق:

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

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

تعليقات: تغيير واحد (موضح أعلاه)

3.6.5. وحدة التحكم Struts

Struts من وحدة تحكم عامة تسمى [ActionServlet]. من المعروف أن السيرفلت يحتوي على طريقة [init] تسمح بتهيئة التطبيق عند بدء تشغيله. إذا استخدمنا وحدة التحكم العامة لـ Struts [ActionServlet]، فلن نتمكن من الوصول إلى طريقتها [init]. هنا، لدينا مهام يجب القيام بها عند بدء تشغيل التطبيق، وهي أساسًا إنشاء مثيل لكائن الوصول إلى النموذج. لذلك نحتاج إلى طريقة [init]. لذا نقوم بتشتيت الفئة [Actionservlet] في الفئة [MainServlet] التالية:

package istia.st.articles.web.struts;

import istia.st.articles.domain.IArticlesDomain;

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

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

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

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

     // مُستردات - مُعيّنات
  public IArticlesDomain getArticlesDomain() {
    return articlesDomain;
  }
  public void setArticlesDomain(IArticlesDomain articlesDomain) {
    this.articlesDomain = articlesDomain;
  }

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

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

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

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

  public void init() throws ServletException{

     // تهيئة الفئة الأصلية
    super.init();
     // استرداد معلمات تهيئة السيرفلت
    config = getServletConfig();
    String param = null;
    for (int i = 0; i < parameters.length; i++) {
      param = config.getInitParameter(parameters[i]);
      if (param == null) {
         // يتم تسجيل الخطأ
        erreurs.add("Paramètre [" + parameters[i]
            + "] absent dans le fichier [web.xml]");
      }
    }
     // أخطاء؟
    if (erreurs.size() != 0) {
      return;
    }
     // يتم إنشاء كائن IArticlesDomain للوصول إلى طبقة الأعمال
    try {
      articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
          new ClassPathResource((String) config
              .getInitParameter(SPRING_CONFIG_FILENAME))))
          .getBean("articlesDomain");
    } catch (Exception ex) {
       // يتم تخزين الخطأ
      erreurs.add("Erreur de configuration de l'accès aux données : "
          + ex.toString());
      return;
    }
     // يتم حفظ بعض عناوين URL الخاصة بالتطبيق
    hActionListe.put("href", ACTION_LISTE);
    hActionListe.put("lien", lienActionListe);
    hActionPanier.put("href", ACTION_PANIER);
    hActionPanier.put("lien", lienActionPanier);
    hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
    hActionValidationPanier.put("lien", lienActionValidationPanier);

     // انتهى الأمر
    return;
  }
}

تعليقات:

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

يتم إنشاء مثيل هذا المتحكم بواسطة الملف [web.xml]:

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

سيتم معالجة كل URL التي تنتهي بـ .do بواسطة مثيل من فئة [istia.st.articles.web.struts.MainServlet]

3.6.6. إجراءات تطبيق Struts

3.6.6.1. مقدمة

سيكون لكل إجراء Struts فئة خاصة به. في الإصدار السابق، كان لكل إجراء طريقة خاصة به في وحدة التحكم في التطبيق. غالبًا ما يتكون كتابة الفئة [Action] من:

  • نسخ ولصق الطريقة التي تم استخدامها في الإصدار السابق
  • تكييف الكود مع قواعد Struts

3.6.6.2. main.do، liste.do

هذان الإجراءان متطابقان ومحددان في [struts-config.xml] بواسطة:

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

عند تنفيذ إحدى هذه الإجراءات [main.do, liste.do] في متصفح، نحصل على النتيجة التالية:

Image

رمز الفئة [ListeArticlesAction] هو التالي:

package istia.st.articles.web.struts;

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

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

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

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

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

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

     // برنامج الخدمة للتحكم
    MainServlet mainServlet = (MainServlet) this.getServlet();

     // أخطاء في التهيئة؟
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // يتم عرض صفحة الأخطاء
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }

     // كائن الوصول إلى المجال
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();

     // قائمة الأخطاء
    ArrayList erreurs = new ArrayList();
     // يتم طلب قائمة العناصر
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // يتم حفظ الخطأ
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
     // الأخطاء؟
    if (erreurs.size() != 0) {
       // عرض صفحة الأخطاء
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // يتم عرض قائمة المقالات
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");
  }
}

تعليقات:

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

يتم توفير العرض الذي سيتم إرساله فعليًا إلى العميل بواسطة [struts-config.xml]:

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

إنها طريقة العرض [/vues/erreurs.jsp]. يُطلب من القارئ التحقق مما تتوقعه طريقة العرض هذه. يتم توفير هذه المعلومات هنا بواسطة الإجراء في الكائن [request] كسمات.

  • وبفضل وحدة التحكم أيضًا، يمكن للإجراء استرداد كائن الوصول إلى نموذج التطبيق (طبقة المجال):
     // كائن الوصول إلى المجال
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
  • وبعد ذلك، يمكن طلب قائمة المقالات:
     // قائمة الأخطاء
    ArrayList erreurs = new ArrayList();
     // يتم طلب قائمة العناصر
    List articles = null;
    try {
      articles = articlesDomain.getAllArticles();
    } catch (UncheckedAccessArticlesException ex) {
       // يتم حفظ الخطأ
      erreurs.add("Erreur lors de l'obtention de tous les articles : "
          + ex.toString());
    }
  • في حالة حدوث أخطاء، يتم إرسال العرض [ERREURS]:
1
2
3
4
5
6
7
8
     // الأخطاء؟
    if (erreurs.size() != 0) {
       // عرض صفحة الأخطاء
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • وإلا يتم إرسال العرض [LISTE]:
1
2
3
4
5
6
     // يتم عرض قائمة المقالات
    request.setAttribute("listarticles", articles);
    request.setAttribute("message", "");
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionPanier() });
    return mapping.findForward("afficherListeArticles");

يتم توفير العرض الذي سيتم إرساله فعليًا إلى العميل من خلال [struts-config.xml]:

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

وهي عبارة عن العرض [/vues/liste.jsp]. يُطلب من القارئ التحقق مما يتوقعه هذا العرض. يتم توفير هذه المعلومات هنا من خلال الإجراء في الكائن [request] كسمات.

3.6.6.3. infos.do

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

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

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

رمز الفئة [InfosArticleAction] هو كما يلي:

package istia.st.articles.web.struts;

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

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

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

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

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

     // برنامج الخدمة للتحكم
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // أخطاء التهيئة؟
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // يتم عرض صفحة الأخطاء
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // كائن الوصول إلى المجال
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // قائمة الأخطاء
    ArrayList erreurs = new ArrayList();
     // استرداد المعرف المطلوب
    String strId = request.getParameter("id");
     // شيء ما؟
    if (strId == null) {
       // غير طبيعي
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // تحويل strId بالكامل
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // غير طبيعي
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // يُطلب عنصر معرف المفتاح
    Article article = null;
    try {
      article = articlesDomain.getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // غير طبيعي
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
    if (article == null) {
       // غير طبيعي
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // يتم وضع العنصر في الجلسة
    request.getSession().setAttribute("article", article);
     // يتم عرض صفحة المعلومات
    request.setAttribute("actions", new Hashtable[] { mainServlet
        .getHActionListe() });
    return mapping.findForward("afficherInfosArticle");
  }
}

تعليقات:

  • بداية الطريقة [execute] مطابقة لتلك التي تمت دراستها سابقًا. وسيكون الأمر كذلك بالنسبة للإجراءات الأخرى.
  • تسترد الطريقة المعلمة [id] التي يجب أن توجد عادةً في عنوان URL. ويجب أن تكون هذه المعلمة بالفعل بالصيغة [/infos.do?id=X]. يتم إجراء اختبارات مختلفة للتحقق من وجود المعلمة [id] وصحتها. في حالة وجود مشكلة، يتم إرسال العرض [ERREURS].
  • إذا كان [id] صالحًا، يتم طلب العنصر المقابل من الطبقة [domain]. إذا أطلقت هذه الطبقة استثناءً أو إذا لم يتم العثور على العنصر، يتم إرسال العرض [ERREURS] مرة أخرى.
  • إذا سارت الأمور على ما يرام، يتم وضع العنصر الذي تم الحصول عليه في الجلسة. هذه نقطة قابلة للنقاش. هنا، ننطلق من مبدأ أن العميل قد يشتري هذا العنصر. إذا فعل ذلك، فسنبحث عنه في الجلسة، بدلاً من طلبه مرة أخرى من الطبقة [domain].
  • أخيرًا، يتم عرض العرض [INFOS]. العرض الذي سيتم إرساله فعليًا إلى العميل يتم توفيره لنا بواسطة [struts-config.xml]:
        <action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
        </action>

هذه هي طريقة العرض [/vues/infos.jsp]. يُطلب من القارئ التحقق مما تتوقعه طريقة العرض هذه. يتم توفير هذه المعلومات هنا من خلال الإجراء في الكائن [request] كسمات.

3.6.6.4. achat.do

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

بمجرد شراء المنتج، يتم إعادة عرض العرض [LISTE] (العرض الأيمن). إذا نظرنا إلى الكود HTML في العرض الأيسر أعلاه، نجد أن العلامة <form> محددة على النحو التالي:

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

نرى إذن أن النموذج يتم إرساله إلى وحدة التحكم باستخدام الإجراء [achat.do].

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

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

رمز الفئة [AchatArticleAction] هو كما يلي:

package istia.st.articles.web.struts;

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

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

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

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

     // برنامج الخدمة للتحكم
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // أخطاء في التهيئة؟
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // يتم عرض صفحة الأخطاء
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // قائمة الأخطاء في هذا الإجراء
    ArrayList erreurs = new ArrayList();
     // استرداد الكمية المشتراة
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // الكمية خاطئة
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
     // استرداد جلسة عمل العميل
    HttpSession session = request.getSession();
     // استرداد المنتج المضاف إلى الجلسة
    Article article = (Article) session.getAttribute("article");
     // هل انتهت صلاحية الجلسة؟
    if(article==null){
       // عرض صفحة الأخطاء
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // يتم إنشاء عملية شراء جديدة
    Achat achat = new Achat(article, qté);
     // إضافة عملية الشراء إلى سلة التسوق الخاصة بالعميل
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // العودة إلى قائمة المنتجات
    return mapping.findForward("afficherListeArticles");
  }
}

تعليقات:

  • بداية الطريقة [execute] مطابقة لتلك التي تمت دراستها سابقًا.
  • لنتذكر شكل النموذج المرسل إلى وحدة التحكم:
1
2
3
4
5
6
7
8
9
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>
  • هناك معلمتان في الطلب: [id]: رقم المنتج الذي تم شراؤه، [qte]: الكمية التي تم شراؤها.
  • يتم اختبار وجود وصلاحية المعلمة [qte]. إذا تم التعرف على هذه المعلمة على أنها غير صحيحة، يتم إرجاع العرض [INFOS] إلى المستخدم مصحوبًا برسالة خطأ:
     // قائمة الأخطاء المتعلقة بهذا الإجراء
    ArrayList erreurs = new ArrayList();
     // استرداد الكمية المشتراة
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // الكمية خاطئة
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherInfosArticle");
    }
  • يتم استرداد العنصر الذي تم شراؤه من الجلسة. قد تكون هذه الجلسة قد انتهت صلاحيتها. في هذه الحالة، يتم إرسال العرض [ERREURS]:
     // استرداد جلسة عمل العميل
    HttpSession session = request.getSession();
     // استرداد المنتج المضاف إلى الجلسة
    Article article = (Article) session.getAttribute("article");
     // هل انتهت صلاحية الجلسة؟
    if(article==null){
       // عرض صفحة الأخطاء
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
  • إذا لم تنته صلاحية الجلسة، يتم وضع المنتج في سلة التسوق، التي يتم استردادها أيضًا من الجلسة:
     // استرداد جلسة عمل العميل
    HttpSession session = request.getSession();
     // استرداد العنصر الموجود في الجلسة
    Article article = (Article) session.getAttribute("article");
     // انتهت صلاحية الجلسة؟
    if(article==null){
       // يتم عرض صفحة الأخطاء
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");      
    }
     // يتم إنشاء عملية شراء جديدة
    Achat achat = new Achat(article, qté);
     // إضافة عملية الشراء إلى سلة التسوق الخاصة بالعميل
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • وأخيرًا، يتم إرسال العرض [LISTE]:
     // العودة إلى قائمة المنتجات
    return mapping.findForward("afficherListeArticles");
  • يتم توفير العرض الذي سيتم إرساله فعليًا إلى العميل بواسطة [struts-config.xml]:
        <action 
            path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
            <forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
            <forward name="afficherListeArticles" path="/main.do"/>
        </action>

هذه هي طريقة العرض [/main.do]. هذه الطريقة ليست طريقة عرض بل إجراء. وبالتالي، سيتم تنفيذ الإجراء [/main.do] الموصوف سابقًا وعرض قائمة المقالات.

3.6.6.5. panier.do

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

الرمز HTML المرتبط بالرابط [Voir le panier] هو التالي:

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

تم تكوين الإجراء [panier.do] على النحو التالي في [struts-config.xml]:

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

رمز الفئة [VoirPanierAction] هو التالي:

package istia.st.articles.web.struts;

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

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

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

     // برنامج الخدمة للتحكم
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // أخطاء في التهيئة؟
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // يتم عرض صفحة الأخطاء
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // عرض سلة التسوق
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // سلة التسوق فارغة
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    } else {
       // هناك شيء ما في سلة التسوق
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(), mainServlet.getHActionValidationPanier() });
      return mapping.findForward("afficherPanier");
    }
  }
}

تعليقات:

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

يتم تحديد العروض التي يتم إرسالها فعليًا إلى العميل من خلال الإجراء:

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

3.6.6.6. retirerachat.do

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

بعد الإجراء [retirerachat.do]، يتم إعادة عرض سلة التسوق (الرسم البياني الأيمن أعلاه). إذا نظرنا إلى الرمز HTML للرابط [Valider le panier] في الرسم البياني الأيسر أعلاه، نحصل على ما يلي:

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

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

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

رمز الفئة [RetirerAchatAction] هو التالي:

package istia.st.articles.web.struts;

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

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

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

     // برنامج الخدمة للتحكم
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // أخطاء في التهيئة؟
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // يتم عرض صفحة الأخطاء
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // قائمة الأخطاء في هذه العملية
    ArrayList erreurs = new ArrayList();
     // استرداد سلة التسوق
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // انتهت صلاحية الجلسة
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherPanierVide");
    }
     // استرداد معرف المنتج المراد حذفه
    String strId = request.getParameter("id");
     // شيء ما؟
    if (strId == null) {
       // غير طبيعي
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // تحويل strId بالكامل
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // غير طبيعي
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
     // نحذف عملية الشراء
    panier.enlever(id);
     // إعادة عرض سلة التسوق
    return mapping.findForward("afficherPanier");
  }
}

تعليقات:

  • بداية الطريقة [execute] مطابقة لتلك التي تمت دراستها سابقًا.
  • يهدف الرمز التالي إلى التحقق من وجود وصلاحية المعلمة [id]. إذا تبين أنها غير صحيحة، يتم إرسال العرض [ERREURS].
  • وإلا، يتم إزالة الشراء من سلة التسوق:
     // إزالة عملية الشراء
    panier.enlever(id);
  • ثم يتم إعادة عرض سلة التسوق:
     // إعادة عرض سلة التسوق
    return mapping.findForward("afficherPanier");

يتم تحديد العرض الذي يتم إرساله فعليًا إلى العميل بواسطة الإجراء:

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

نرى أنه في الواقع، سيتم تشغيل الإجراء [/panier.do]. وقد تم وصف هذا الإجراء سابقًا. وسيقوم بعرض العرض [PANIER] أو [PANIERVIDE] وفقًا لحالة سلة التسوق.

3.6.6.7. validerpanier.do

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

 

الرمز HTML للرابط [Valider le panier] هو كما يلي:

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

عند تنشيط هذا الرابط، يتم خصم المخزون وإعادة عرض قائمة العناصر.

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

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

رمز الفئة [ValiderPanierAction] هو التالي:

package istia.st.articles.web.struts;

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

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

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

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

     // برنامج الخدمة للتحكم
    MainServlet mainServlet = (MainServlet) this.getServlet();
     // أخطاء في التهيئة؟
    ArrayList erreursInit = mainServlet.getErreurs();
    if (erreursInit.size() != 0) {
       // عرض صفحة الأخطاء
      request.setAttribute("erreurs", erreursInit);
      request.setAttribute("actions", new Hashtable[] {});
      return mapping.findForward("afficherErreurs");
    }
     // كائن الوصول إلى المجال
    IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
     // قائمة الأخطاء في هذا الإجراء
    ArrayList erreurs = new ArrayList();
         // أكد المشتري سلة التسوق
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // انتهت صلاحية الجلسة
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
         // يتم التحقق من سلة التسوق
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // غير طبيعي
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
        }
         // استرداد الأخطاء المحتملة
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
         // يبدو أن كل شيء على ما يرام OK - يتم عرض قائمة المنتجات
    return mapping.findForward("afficherListeArticles");
  }
}

تعليقات:

  • بداية الطريقة [execute] مطابقة لتلك التي تمت دراستها سابقًا.
  • يتم استرداد سلة التسوق من الجلسة. إذا انتهت صلاحية الجلسة، يتم إرسال العرض [ERREURS]:
     // قائمة الأخطاء المتعلقة بهذه العملية
    ArrayList erreurs = new ArrayList();
         // أكد المشتري سلة التسوق
        Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // انتهت صلاحية الجلسة
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
     request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
    }
  • يتم تأكيد مشتريات سلة التسوق. قد تحدث أخطاء إذا كانت المخزونات غير كافية لتلبية المشتريات. في هذه الحالة، يتم إرسال العرض [ERREURS]:
         // يتم تأكيد سلة التسوق
        try {
            articlesDomain.acheter(panier);
        } catch (UncheckedAccessArticlesException ex) {
             // غير طبيعي
            erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe() });
      return mapping.findForward("afficherErreurs");
        }
         // استرداد الأخطاء المحتملة
         erreurs = articlesDomain.getErreurs();
        if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { mainServlet
          .getHActionListe(),mainServlet.getHActionPanier() });
      return mapping.findForward("afficherErreurs");
        }
  • إذا سارت الأمور على ما يرام، يتم إعادة عرض قائمة العناصر:
         // يبدو أن كل شيء على ما يرام OK - يتم عرض قائمة المنتجات
    return mapping.findForward("afficherListeArticles");

يتم تحديد العرض الذي يتم إرساله فعليًا إلى العميل من خلال الإجراء:

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

نرى أنه في الواقع، سيتم تشغيل الإجراء [/main.do]. وقد تم وصف هذا الإجراء سابقًا. وسيقوم بعرض العرض [LISTE].

3.7. بنية MVC مع Spring

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

لنعد إلى بنية التطبيق MVC:

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

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

في إصدار Struts:

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

في إصدار Spring:

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

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

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

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

M=modèle
فئات الأعمال، وفئات الوصول إلى البيانات، وقاعدة البيانات
V=vues
الصفحات JSP
C=contrôleur
برنامج الخدمة لمعالجة طلبات العملاء، الكائنات [Action]

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

M=modèle
فئات الأعمال، وفئات الوصول إلى البيانات وقاعدة البيانات
V=vues
الصفحات JSP
C=contrôleur
الخادم الصغير لمعالجة طلبات العملاء، والكائنات التي تنفذ الواجهة [Controller]
  • وحدة التحكم هي قلب التطبيق. تمر جميع طلبات العميل عبرها. وهي عبارة عن سيرفلت عامة مقدمة من SPRING. وهي من النوع [DispatcherServlet]. سنطلق على وحدة التحكم هذه من الآن فصاعدًا اسم وحدة التحكم [Spring].
  • ستقوم وحدة التحكم [Spring] بتوجيه طلب العميل إلى إحدى مثيلات النوع [Controller]. سيكون هناك مثيل واحد لكل إجراء يجب معالجته. يتم تعريف هذا المثيل في عنوان URL المطلوب كما هو الحال مع Struts. وبالتالي، سنعرف أن الإجراء المطلوب هو الإجراء [liste] لأن عنوان URL المطلوب هو [liste.do]
  • إذا كان C هو سياق التطبيق، فإن وحدة التحكم [Spring] تستخدم ملف [C-servlet.xml] الذي يؤدي دور ملف التكوين struts-config.xml، في إصدار Struts. مع كل إجراء يجب أن تعالجه التطبيق، نربط اسم فئة من نوع Controller المسؤولة عن معالجة الطلب.
  • يقوم وحدة التحكم بتسليم زمام الأمور إلى الكائن من نوع Controller المرتبط بالإجراء. ويقوم بذلك عن طريق طلب تنفيذ الطريقة handleRequest الخاصة بهذا الكائن، حيث يقوم بإرسال طلب العميل إليها. وهنا يقوم المطور بما عليه القيام به: فقد يضطر إلى الاستعانة بفئات الأعمال أو فئات الوصول إلى البيانات. في نهاية المعالجة، يعيد كائن Controller إلى وحدة التحكم اسم العرض الذي يجب إرساله كاستجابة للعميل.
  • في ملف التكوين الخاص به، سيجد وحدة التحكم URL المرتبطة باسم العرض الذي طُلب منه عرضه. ثم يرسل هذا العرض. وبذلك ينتهي التبادل مع العميل.

3.7.2. النموذج

هو نفسه كما في النسختين السابقتين. ويتكون من أرشيفات Java [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].

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

3.7.3.1. البنية العامة

الهندسة العامة لمشروع Eclipse هي كما يلي:

Image

3.7.3.2. تكوين الوصول إلى البيانات

نظرًا لأن واجهة الوصول إلى البيانات لا تتغير، فإن ملفات التكوين المرتبطة بها هي نفسها الموجودة في الإصدار السابق. وهي محددة في [WEB-INF/src]:

Image

في لقطة الشاشة أعلاه، ملفات [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] هي ملفات الإصدارات السابقة.

3.7.3.3. دليل الأرشيفات

في [WEB-INF/lib]، نجد نفس الأرشيفات الموجودة في الإصدار السابق، باستثناء أرشيفات Struts التي لم تعد ضرورية:

Image

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

يتم تكوين التطبيق باستخدام ثلاثة ملفات: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] في المجلد [WEB-INF]:

Image

الملف [web.xml] هو التالي:

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

<!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
     <!-- محمل سياق Spring للتطبيق -->
    <listener>
        <listener-class> 
            org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
     <!-- الخادم -->
    <servlet>
        <servlet-name>springwebarticles</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
     <!-- تعيين عناوين URL -->
    <servlet-mapping>
        <servlet-name>springwebarticles</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
     <!-- وثيقة الإدخال -->
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

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

  • الصفحة الرئيسية للتطبيق هي [/vues/index.jsp] (ملف الترحيب)
  • سيتم إعادة توجيه طلبات URL التي تتخذ شكل *.do إلى السيرفلت [springwebarticles] (servlet-mapping)
  • الخادم [springwebarticles] هو مثيل لفئة [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class) التي يوفرها Spring.
  • سيتم تشغيل المستمع [org.springframework.web.context.ContextLoaderListener] عند بدء تشغيل التطبيق. وستكون مهمته الرئيسية هي إنشاء مثيلات لفئات Spring المحددة في الملف [applicationContext.xml]

الملف [applicationContext.xml] هو التالي:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- فئة الوصول إلى البيانات -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- فئة الأعمال -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- تكوين تطبيق الويب-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

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

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

يحدد الملف الأخير [springwebarticles.xml] الإجراءات التي يقبلها التطبيق بطريقة تشبه إلى حد كبير تلك المستخدمة في ملف Struts [struts-config.xml]. ومحتواه كما يلي:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- مديرو الإجراءات = وحدات التحكم -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="AchatController" 
        class="istia.st.articles.web.spring.AchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

     <!-- تعيين التطبيق-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

     <!-- مدير العروض -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

     <!-- ملف الرسائل -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>

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

  • أن وحدة التحكم الخاصة بنا ستعالج عناوين URL التالية:
main.do
لعرض قائمة المقالات
liste.do
لعرض قائمة المقالات
infos.do
لعرض معلومات عن منتج معين
achat.do
لشراء منتج معين
panier.do
لعرض سلة التسوق
retirerachat.do
لإزالة منتج من سلة التسوق
validerpanier.do
لتأكيد سلة التسوق
  • تتوافق الإجراءات السابقة واحدة تلو الأخرى مع الإجراءات التي يعالجها وحدة التحكم في الإصدارات السابقة. ويتم تحديد اسم الفئة المسؤولة عن معالجة كل منها. لنأخذ مثال الإجراء [/panier.do]:
  • يجب معالجتها بواسطة bean [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] لمعالجة الإجراء
  • كيفية إنشاء مثيل لها. هنا، يتم توفير الفول [config] المحدد بواسطة [applicationContext.xml] والذي تم إنشاء مثيل له عند بدء تشغيل التطبيق كمعلمة. سيتم القيام بذلك لجميع الإجراءات [Controller]. وبالتالي، سيتوفر لكل منها في حقل خاص، الكائن [config] الذي ستجد فيه جميع المعلومات المشتركة بين جميع العملاء.
  • الطريقة التي يجب بها حل أسماء العروض:
     <!-- مدير العروض -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

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

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

     <!-- تعيين التطبيق-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

     <!-- مدير العروض -->
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="prefix">
            <value>/vues/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

     <!-- ملف الرسائل -->
    <bean id="messageSource" 
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename">
            <value>messages</value>
        </property>
    </bean>
</beans>
  • كما تم إنشاء حبات [Controller] المحددة بواسطة هذا الملف
  • كل شيء جاهز الآن لمعالجة طلب العميل. وكان هذا الطلب هو: [http://localhost:8080/springwebarticles]. هنا، لا يُطلب URL من السياق بل السياق نفسه. عندئذ يتم استخدام القسم [welcome-file] من الملف [web.xml].
    <welcome-file-list>
        <welcome-file>/vues/index.jsp</welcome-file>
    </welcome-file-list>
  • تبدو طريقة العرض [index.jsp] كما يلي:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/main.do"/>
  • وبالتالي يُطلب من العميل إعادة التوجيه إلى عنوان URL [http://localhost:8080/springwebarticles/main.do]. ويقوم بذلك.
  • وبالتالي يتلقى وحدة التحكم Spring طلبًا جديدًا. ويستخدم ملفه [springwebarticles-servlet.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- مديرو الإجراءات = وحدات التحكم -->
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>
....
    </bean>

     <!-- تعيين التطبيق-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
                <prop key="/infos.do">InfosController</prop>
                <prop key="/achat.do">AchatController</prop>
                <prop key="/panier.do">VoirPanierController</prop>
                <prop key="/retirerachat.do">RetirerAchatController</prop>
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>

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

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

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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- فئة الوصول إلى البيانات -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
        <constructor-arg index="0">
            <value>sqlmap-config-firebird.xml</value>
        </constructor-arg>
    </bean>
     <!-- فئة الأعمال -->
    <bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
        <constructor-arg index="0">
            <ref bean="articlesDao"/>
        </constructor-arg>
    </bean>
     <!-- تكوين تطبيق الويب-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>
</beans>

نحن نعرف حبات [articlesDao, articlesDomain] ولكننا لا نعرف حبة [config]. يتم تعريف هذه الحبة بواسطة فئة Java التالية:

package istia.st.articles.web.spring;

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

/**
 * @author ST - ISTIA
 */

public class Config {

     // الحقول الخاصة
    private IArticlesDomain articlesDomain = null;
    private final String ACTION_LISTE = "liste.do";
    private final String ACTION_PANIER = "panier.do";
    private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
    private final String lienActionListe = "Liste des articles";
    private final String lienActionPanier = "Voir le panier";
    private final String lienActionValidationPanier = "Valider le panier";
    private Hashtable hActionListe = new Hashtable(2);
    private Hashtable hActionPanier = new Hashtable(2);
    private Hashtable hActionValidationPanier = new Hashtable(2);

     // مُستقبلات-مُعيّنات
    public IArticlesDomain getArticlesDomain() {
        return articlesDomain;
    }

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

    public Hashtable getHActionListe() {
        return hActionListe;
    }

    public Hashtable getHActionPanier() {
        return hActionPanier;
    }

    public Hashtable getHActionValidationPanier() {
        return hActionValidationPanier;
    }

     // تهيئة تطبيق الويب
    public void init() {
         // نقوم بتخزين بعض عناوين URL الخاصة بالتطبيق
        hActionListe.put("href", ACTION_LISTE);
        hActionListe.put("lien", lienActionListe);
        hActionPanier.put("href", ACTION_PANIER);
        hActionPanier.put("lien", lienActionPanier);
        hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
        hActionValidationPanier.put("lien", lienActionValidationPanier);

         // انتهى
        return;
    }
}

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

  • لأن bean [config] محدد بالطريقة التالية في [applicationContext.xml]:
     <!-- تكوين تطبيق الويب-->
    <bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
        <property name="articlesDomain">
            <ref bean="articlesDomain"/>
        </property>
    </bean>

عند إنشائه، تم تهيئة حقل [articlesDomain] الخاص به

  • بعد ذلك، وبسبب السمة [init-method="init"] للبيان أعلاه، يتم تنفيذ الطريقة [init] للفئة المرتبطة بالبيان. وهنا، تقوم بتهيئة القواميس الثلاثة [hActionListe, hActionPanier, hActionValidationPanier] المستخدمة لتوليد الروابط الثلاثة الممكنة في القائمة المعروضة للمستخدم.
  • يتم إنشاء واصلات وصول عامة لجعل هذه الحقول الخاصة متاحة لمثيلات من النوع [Controller] التي ستعالج الإجراءات.

3.7.7. إجراءات [Controller] لتطبيق Spring

3.7.7.1. مقدمة

ستكون كل عملية Spring موضوعًا لفئة من النوع [Controller]. في إصدار Struts، كانت كل عملية موضوعًا لفئة من النوع [Action]. غالبًا ما يتم كتابة الفئة [Controller] عن طريق:

  • نسخ ولصق الفئة [Action] التي كانت مستخدمة في إصدار Struts
  • تكييف الكود مع قواعد Spring

3.7.7.2. main.do، liste.do

هذان الإجراءان متطابقان ومحددان في [springwebarticles-servlet.xml] بواسطة:

     <!-- تعيين التطبيق-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/liste.do">ListController</prop>
                <prop key="/main.do">ListController</prop>
...
            </props>
        </property>
    </bean>

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

وهما مرتبطتان بالفئة [ istia.st.articles.web.spring.ListController] التي سنفصلها لاحقًا. عند تنفيذ إحدى هاتين العمليتين في متصفح، نحصل على النتيجة التالية:

Image

رمز الفئة [istia.st.articles.web.spring.ListController] هو التالي:

package istia.st.articles.web.spring;

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

public class ListController implements Controller {

     // تكوين تطبيق الويب
    Config config;

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

     // معالجة الطلب
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

         // طلب قائمة المقالات
        List articles = null;
        try {
            articles = config.getArticlesDomain().getAllArticles();
        } catch (UncheckedAccessArticlesException ex) {
             // يتم تسجيل الخطأ
            ArrayList erreurs = new ArrayList();
            erreurs.add("Erreur lors de l'obtention de tous les articles : "
                    + ex.toString());
             // يتم عرض صفحة الأخطاء
            request.setAttribute("erreurs", erreurs);
            request.setAttribute("actions", new Hashtable[] { config
                    .getHActionListe() });
             // إرسال عرض الأخطاء
            return new ModelAndView("erreurs");
        }
         // يتم عرض قائمة المقالات
        request.setAttribute("listarticles", articles);
        request.setAttribute("message", "");
        request.setAttribute("actions", new Hashtable[] { config
                .getHActionPanier() });
         // إرسال العرض
        return new ModelAndView("liste");
    }

}

تعليقات:

  • تحتوي الفئة على حقل خاص [config]. تم تهيئة هذا الحقل بواسطة Spring عند إنشاء مثيل bean [ListController]:
    <bean id="ListController" class="istia.st.articles.web.spring.ListController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

نرى أعلاه أن الحقل [config] في [ListController] يتم تهيئته باستخدام bean [config]. ما هو هذا؟ إنه bean [config] المحدد في [applicationContext.xmlc.a.d. مثيل لـ [istia.st.articles.web.spring.Config] الموصوف أعلاه.

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

3.7.7.3. infos.do

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

يتم تعريف هذا الإجراء في [springwebarticles-servlet.xml] بواسطة:

     <!-- تعيين التطبيق-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/infos.do">InfosController</prop>
...
            </props>
        </property>
    </bean>
...
    <bean id="InfosController" 
        class="istia.st.articles.web.spring.InfosController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

رمز الفئة [InfosController] هو التالي:

package istia.st.articles.web.spring;

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

public class InfosController implements Controller {

     // تكوين تطبيق الويب
    Config config;

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

     // معالجة الطلب
    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

     // قائمة الأخطاء
    ArrayList erreurs = new ArrayList();
     // استرداد المعرف المطلوب
    String strId = request.getParameter("id");
     // شيء ما؟
    if (strId == null) {
       // غير طبيعي
      erreurs.add("action incorrecte([infos,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // تحويل strId بالكامل
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // غير طبيعي
      erreurs.add("action incorrecte([infos,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // يُطلب عنصر معرف المفتاح
    Article article = null;
    try {
      article = config.getArticlesDomain().getArticleById(id);
    } catch (UncheckedAccessArticlesException ex) {
       // غير طبيعي
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
    if (article == null) {
       // غير طبيعي
      erreurs.add("Article de clé [" + id + "] inexistant");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
          return new ModelAndView("erreurs");
    }
     // يتم وضع العنصر في الجلسة
    request.getSession().setAttribute("article", article);
     // يتم عرض صفحة المعلومات
    request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
        return new ModelAndView("infos");
    }
}

تعليقات:

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

3.7.7.4. achat.do

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

Image

إذا نظرنا إلى كود HTML لهذا العرض، نجد أن العلامة <form> محددة على النحو التالي:

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

ونرى إذن أن النموذج يتم إرساله إلى وحدة التحكم باستخدام الإجراء [achat.do].

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

     <!-- تعيين التطبيق-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/achat.do">AchatController</prop>
...
            </props>
        </property>
    </bean>

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

رمز الفئة [AchatController] هو التالي:

package istia.st.articles.web.spring;

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

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

public class AchatController implements Controller {

   // تكوين تطبيق الويب
  Config config;

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

   // معالجة الطلب
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // قائمة الأخطاء المتعلقة بهذا الإجراء
    ArrayList erreurs = new ArrayList();
     // استرداد الكمية المشتراة
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // الكمية خاطئة
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
     // استرداد جلسة عمل العميل
    HttpSession session = request.getSession();
     // استرداد المنتج المضاف إلى الجلسة
    Article article = (Article) session.getAttribute("article");
     // هل انتهت صلاحية الجلسة؟
    if (article == null) {
       // عرض صفحة الأخطاء
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // يتم إنشاء عملية شراء جديدة
    Achat achat = new Achat(article, qté);
     // إضافة عملية الشراء إلى سلة التسوق الخاصة بالعميل
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
     // العودة إلى قائمة المنتجات
    return new ModelAndView("index");
  }
}

تعليقات:

  • لنتذكر شكل النموذج المرسل إلى وحدة التحكم:
        <form method="post" action="achat.do?id=3"/>
            <table>
                <tr>
                    <td><input type="submit" value="Acheter"></td>
                    <td>Qte <input type="text" name="qte" size="3" value=""></td>
                    <td></td>
                </tr>
            </table>
        </form>
  • يوجد معلمتان في الاستعلام: [id]: رقم المادة المشتراة، [qte]: الكمية المشتراة.
  • يتم اختبار وجود وصلاحية المعلمة [qte]. إذا تم التعرف على هذه المعلمة على أنها غير صحيحة، يتم إرجاع العرض [INFOS] إلى المستخدم مصحوبًا برسالة خطأ:
     // قائمة الأخطاء المتعلقة بهذا الإجراء
    ArrayList erreurs = new ArrayList();
     // استرداد الكمية المشتراة
    int qté = 0;
    try {
      qté = Integer.parseInt(request.getParameter("qte"));
      if (qté <= 0)
        throw new NumberFormatException();
    } catch (NumberFormatException ex) {
       // الكمية خاطئة
      request.setAttribute("msg", "Quantité incorrecte");
      request.setAttribute("qte", request.getParameter("qte"));
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("infos");
    }
  • يتم استرداد العنصر الذي تم شراؤه من الجلسة. قد تكون هذه الجلسة قد انتهت صلاحيتها. في هذه الحالة، يتم إرسال العرض [ERREURS]:
     // استرداد جلسة عمل العميل
    HttpSession session = request.getSession();
     // استرداد المنتج المضاف إلى الجلسة
    Article article = (Article) session.getAttribute("article");
     // هل انتهت صلاحية الجلسة؟
    if (article == null) {
       // عرض صفحة الأخطاء
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • إذا لم تنته صلاحية الجلسة، يتم وضع المنتج في سلة التسوق، التي يتم استردادها أيضًا من الجلسة:
1
2
3
4
5
6
7
8
9
     // يتم إنشاء عملية شراء جديدة
    Achat achat = new Achat(article, qté);
     // إضافة عملية الشراء إلى سلة التسوق الخاصة بالعميل
    Panier panier = (Panier) session.getAttribute("panier");
    if (panier == null) {
      panier = new Panier();
      session.setAttribute("panier", panier);
    }
    panier.ajouter(achat);
  • وأخيرًا، يتم إرسال العرض [LISTE]:
     // العودة إلى قائمة المنتجات
    return new ModelAndView("index");
  • أعلاه، يتم إرسال العرض [/vues/index.jsp]. نعلم أن هذا العرض يطلب من متصفح العميل إعادة التوجيه إلى عنوان URL [/main.do]. هذه الإعادة التوجيه هي التي ستعرض قائمة المنتجات.

3.7.7.5. panier.do

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

Image

الرمز HTML المرتبط بالرابط أعلاه هو التالي:

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

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

Image

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

     <!-- تعيين التطبيق-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/panier.do">VoirPanierController</prop>
...
            </props>
        </property>
    </bean>
...
    <bean id="VoirPanierController" 
        class="istia.st.articles.web.spring.VoirPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

رمز الفئة [VoirPanierController] هو التالي:

package istia.st.articles.web.spring;

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

public class VoirPanierController implements Controller {

   // تكوين تطبيق الويب
  Config config;

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

   // معالجة الطلب
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // عرض سلة التسوق
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null || panier.getAchats().size() == 0) {
       // سلة التسوق فارغة
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("paniervide");
    } else {
       // هناك شيء ما في سلة التسوق
      request.setAttribute("actions", new Hashtable[] {
          config.getHActionListe(), config.getHActionValidationPanier() });
      return new ModelAndView("panier");
    }
  }
}

تعليقات:

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

3.7.7.6. retirerachat.do

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

Image

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

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

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

     <!-- تعيين التطبيق-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/retirerachat.do">RetirerAchatController</prop>
            </props>
        </property>
    </bean>
...
    <bean id="RetirerAchatController" 
        class="istia.st.articles.web.spring.RetirerAchatController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

رمز فئة [RetirerAchatController] هو التالي:

package istia.st.articles.web.spring;

import istia.st.articles.domain.Panier;

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

public class RetirerAchatController implements Controller {

   // تكوين تطبيق الويب
  Config config;

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

   // معالجة الطلب
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // قائمة الأخطاء المتعلقة بهذا الإجراء
    ArrayList erreurs = new ArrayList();
     // استرداد سلة التسوق
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // انتهت صلاحية الجلسة
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // استرداد معرف المنتج المراد إزالته
    String strId = request.getParameter("id");
     // شيء ما؟
    if (strId == null) {
       // غير طبيعي
      erreurs.add("action incorrecte([retirerachat,id=null]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // تحويل strId بالكامل
    int id = 0;
    try {
      id = Integer.parseInt(strId);
    } catch (Exception ex) {
       // غير طبيعي
      erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // نحذف عملية الشراء
    panier.enlever(id);
     // إعادة عرض سلة التسوق
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");
  }
}

تعليقات:

  • يتحقق الرمز من وجود وصحة المعلمة [id]. إذا تبين أنها غير صحيحة، يتم إرسال العرض [ERREURS].
  • وإلا، يتم إزالة الشراء من سلة التسوق:
     // إزالة عملية الشراء
    panier.enlever(id);
  • ثم يتم إعادة عرض سلة التسوق:
     // إعادة عرض سلة التسوق
    request.setAttribute("actions",
        new Hashtable[] { config.getHActionListe() });
    return new ModelAndView("redirpanier");

لنتذكر رمز العرض [/vues/redirpanier.jsp]:

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

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

نرى أن العميل سيتم إعادة توجيهه إلى الإجراء [/panier.do]. وقد تم وصف هذا الإجراء سابقًا. وسيقوم بعرض العرض [PANIER] أو [PANIERVIDE] وفقًا لحالة سلة التسوق.

3.7.7.7. validerpanier.do

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

Image

الرمز HTML للرابط [Valider le panier] هو التالي:

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

عند تنشيط هذا الرابط، يتم خصم المخزون وتظهر قائمة المنتجات مرة أخرى.

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

     <!-- تعيين التطبيق-->
    <bean id="urlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
...
                <prop key="/validerpanier.do">ValiderPanierController</prop>
            </props>
        </property>
    </bean>
...
    <bean id="ValiderPanierController" 
        class="istia.st.articles.web.spring.ValiderPanierController">
        <property name="config">
            <ref bean="config"/>
        </property>
    </bean>

رمز الفئة [ValiderPanierController] هو كما يلي:

package istia.st.articles.web.spring;

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

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

public class ValiderPanierController implements Controller {

   // تكوين تطبيق الويب
  Config config;

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

   // معالجة الطلب
  public ModelAndView handleRequest(HttpServletRequest request,
      HttpServletResponse response) throws Exception {

     // قائمة الأخطاء المتعلقة بهذا الإجراء
    ArrayList erreurs = new ArrayList();
     // أكد المشتري سلة التسوق
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // انتهت صلاحية الجلسة
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // تتم المصادقة على سلة التسوق
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // غير طبيعي
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // استرداد الأخطاء المحتملة
    erreurs = config.getArticlesDomain().getErreurs();
    if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe(), config.getHActionPanier() });
      return new ModelAndView("erreurs");
    }
     // يبدو أن كل شيء على ما يرام OK - يتم عرض قائمة العناصر
    return new ModelAndView("index");
  }
}

تعليقات:

  • يتم استرداد سلة التسوق من الجلسة. إذا انتهت صلاحية الجلسة، يتم إرسال العرض [ERREURS]:
     // قائمة الأخطاء المتعلقة بهذه العملية
    ArrayList erreurs = new ArrayList();
     // أكد المشتري سلة التسوق
    Panier panier = (Panier) request.getSession().getAttribute("panier");
    if (panier == null) {
       // انتهت صلاحية الجلسة
      erreurs.add("Votre session a expiré");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
  • يتم تأكيد مشتريات سلة التسوق. قد تحدث أخطاء إذا كانت المخزونات غير كافية لتلبية المشتريات. في هذه الحالة، يتم إرسال العرض [ERREURS]:
     // يتم تأكيد سلة التسوق
    try {
      config.getArticlesDomain().acheter(panier);
    } catch (UncheckedAccessArticlesException ex) {
       // غير طبيعي
      erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe() });
      return new ModelAndView("erreurs");
    }
     // استرداد الأخطاء المحتملة
    erreurs = config.getArticlesDomain().getErreurs();
    if (erreurs.size() != 0) {
      request.setAttribute("erreurs", erreurs);
      request.setAttribute("actions", new Hashtable[] { config
          .getHActionListe(), config.getHActionPanier() });
      return new ModelAndView("erreurs");
    }
  • إذا سارت الأمور على ما يرام، يتم إعادة عرض قائمة العناصر:
     // يبدو أن كل شيء على ما يرام OK - يتم عرض قائمة المنتجات
    return new ModelAndView("index");

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