3. تطبيق نموذجي – 01: rdvmedecins-jsf2-ejb
النص التالي يشير إلى الوثائق التالية:
- [ref7]: مقدمة إلى Java EE 5 (يونيو 2010) [http://tahe.developpez.com/java/javaee]. يتيح هذا المستند اكتشاف JSF 1 و EJB3.
- [ref8]: استمرارية Java من خلال الممارسة (يونيو 2007) [http://tahe.developpez.com/java/jpa]. يتيح هذا المستند اكتشاف استمرارية البيانات باستخدام JPA (استمرارية Java API).
- [ref9]: إنشاء خدمة ويب Java EE باستخدام Netbeans وخادم Glassfish (يناير 2009) [http://tahe.developpez.com/java/webservice-jee]. يدرس هذا المستند إنشاء خدمة ويب.
التطبيق النموذجي الذي سيتم دراسته مأخوذ من [ref9].
3.1. التطبيق
ترغب شركة خدمات تكنولوجيا المعلومات [ISTIA-AGI] في تقديم خدمة حجز المواعيد. السوق المستهدف الأول هو الأطباء الذين يعملون بمفردهم. هؤلاء لا يملكون عادةً سكرتارية. لذلك، يتصل العملاء الراغبون في حجز موعد مباشرة بالطبيب. وهذا يؤدي إلى إزعاج الطبيب بشكل متكرر خلال اليوم، مما يقلل من تواجده لخدمة مرضاه. ترغب شركة [ISTIA-AGI] في تقديم خدمة حجز المواعيد لهم تعمل وفقًا للمبدأ التالي:
- يقوم مكتب استشاري بحجز المواعيد لعدد كبير من الأطباء. ويمكن أن يقتصر هذا المكتب على شخص واحد. ويتم تقاسم راتبه بين جميع الأطباء الذين يستخدمون خدمة حجز المواعيد.
- يتم ربط المكتب الإداري وجميع الأطباء بالإنترنت
- يتم تسجيل المواعيد في قاعدة بيانات مركزية، يمكن الوصول إليها عبر الإنترنت، من قبل المكتب والأطباء
- عادةً ما يتم تسجيل RV بواسطة السكرتارية. ويمكن أيضًا أن يقوم الأطباء أنفسهم بذلك. وهذا هو الحال على وجه الخصوص عندما يحدد الطبيب بنفسه، في نهاية الاستشارة، موعدًا جديدًا (RV) لمريضه.
هيكل خدمة تسجيل RV هو كما يلي:
![]() |
يزداد كفاءة الأطباء إذا لم يعودوا مضطرين لإدارة RV. وإذا كان عددهم كافياً، فستكون مساهمتهم في تكاليف تشغيل السكرتارية ضئيلة.
قررت شركة [ISTIA-AGI] تطوير التطبيق في نسختين:
- نسخة JSF / EJB3 / JPA EclipseLink / خادم Glassfish:
![]() |
- ثم إصدار JSF / Spring / JPA Hibernate / خادم Tomcat:
![]() |
3.2. كيفية عمل التطبيق
سنسمي التطبيق [RdvMedecins]. نعرض أدناه لقطات شاشة توضح طريقة عمله.
الصفحة الرئيسية للتطبيق هي كما يلي:
![]() |
من هذه الصفحة الأولى، سيقوم المستخدم (الأمانة، الطبيب) بتنفيذ عدد من الإجراءات. نعرضها أدناه. تعرض الصورة اليسرى الشاشة التي يقوم المستخدم من خلالها بتقديم طلب، بينما تعرض الصورة اليمنى الرد المرسل من الخادم.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
أخيرًا، يمكن أيضًا الحصول على صفحة أخطاء:
![]() |
3.3. قاعدة البيانات
لنعد إلى بنية التطبيق المراد إنشاؤه:
![]() |
قاعدة البيانات التي سنسميها [dbrdvmedecins2] هي قاعدة بيانات MySQL5 تحتوي على أربع جداول:
![]() |
3.3.1. الجدول [MEDECINS]
تحتوي على معلومات عن الأطباء الذين تديرهم التطبيق [RdvMedecins].
![]() | ![]() |
- ID: رقم تعريف الطبيب - المفتاح الأساسي للجدول
- VERSION: رقم تعريف إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
- NOM: اسم الطبيب
- PRENOM: اسمه الأول
- TITRE: لقبه (الآنسة، السيدة، السيد)
3.3.2. الجدول [CLIENTS]
يتم تسجيل عملاء الأطباء المختلفين في الجدول [CLIENTS]:
![]() | ![]() |
- ID: رقم تعريف العميل - المفتاح الأساسي للجدول
- VERSION: رقم تعريف إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
- NOM: اسم العميل
- PRENOM: اسمه الأول
- TITRE: لقبه (الآنسة، السيدة، السيد)
3.3.3. الجدول [CRENEAUX]
تسرد هذه الجدول الفترات الزمنية التي يمكن فيها استخدام RV:
![]() |
![]() |
- ID: الرقم الذي يحدد الفترة الزمنية - المفتاح الأساسي للجدول (السطر 8)
- VERSION: الرقم الذي يحدد إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
- ID_MEDECIN: رقم يحدد الطبيب الذي ينتمي إليه هذا الموعد – مفتاح خارجي في العمود MEDECINS(ID).
- HDEBUT: وقت بدء الفترة
- MDEBUT: دقائق بداية الفترة
- HFIN: ساعة انتهاء الفترة الزمنية
- MFIN: دقائق نهاية الفترة الزمنية
تشير السطر الثاني من الجدول [CRENEAUX] (انظر [1] أعلاه) ، على سبيل المثال ، إلى أن الفترة رقم 2 تبدأ في الساعة 8:20 وتنتهي في الساعة 8:40 وتخص الطبيب رقم 1 (السيدة ماري PELISSIER).
3.3.4. الجدول [RV]
تسرد المواعيد المحددة لكل طبيب:
![]() |
- ID: رقم يحدد RV بشكل فريد – مفتاح أساسي
- JOUR: يوم RV
- ID_CRENEAU: الفترة الزمنية لـ RV - مفتاح خارجي في الحقل [ID] من الجدول [CRENEAUX] – يحدد في آن واحد الفترة الزمنية والطبيب المعني.
- ID_CLIENT: رقم العميل الذي تم الحجز لصالحه – مفتاح خارجي في الحقل [ID] من الجدول [CLIENTS]
يحتوي هذا الجدول على قاعدة بيانات ( ) مقيدة بالتفرد على قيم الأعمدة المرتبطة (JOUR، ID_CRENEAU):
إذا كان أحد صفوف الجدول [RV] يحتوي على القيمة (JOUR1، ID_CRENEAU1) للأعمدة (JOUR، ID_CRENEAU)، فلا يمكن أن توجد هذه القيمة في أي مكان آخر. وإلا، فهذا يعني أنه تم أخذ RV مرتين في نفس الوقت لنفس الطبيب. من منظور برمجة Java، يقوم برنامج التشغيل JDBC للقاعدة بإطلاق SQLException عند حدوث هذه الحالة.
السطر الذي يساوي 3 في id (انظر [1] أعلاه) يعني أنه تم حجز RV للفترة رقم 20 والعميل رقم 4 في 23/08/2006. يخبرنا الجدول [CRENEAUX] أن الموعد رقم 20 يتوافق مع الفترة الزمنية 16:20 - 16:40 وينتمي إلى الطبيب رقم 1 (السيدة ماري PELISSIER). تُظهر لنا الجدولة [CLIENTS] أن العميل رقم 4 هو الآنسة بريجيت BISTROU.
3.3.5. إنشاء قاعدة البيانات
لإنشاء الجداول وتعبئتها، يمكن استخدام البرنامج النصي [dbrdvmedecins2.sql] المتوفر على موقع الأمثلة. باستخدام [WampServer] (انظر الفقرة 1.3.3)، يمكن اتباع الخطوات التالية:
![]() |
- في [1]، انقر على أيقونة [WampServer] واختر الخيار [PhpMyAdmin] [2]،
- إلى [3]، وفي النافذة التي فتحت، حدد الرابط [Bases de données]،
![]() |
- إلى [2]، نقوم بإنشاء قاعدة بيانات أطلقنا عليها اسم [4] وترميز [5]،
- في [7]، تم إنشاء قاعدة البيانات. نضغط على الرابط الخاص بها،
![]() |
- في [8]، نستورد ملف SQL،
- الذي يتم تحديده في نظام الملفات باستخدام الزر [9]،
![]() |
- في [11]، نختار البرنامج النصي SQL وفي [12] نقوم بتنفيذه،
- في [13]، تم إنشاء الجداول الأربعة للقاعدة. نتبع أحد الروابط،
![]() |
- في [14]، محتوى الجدول.
بعد ذلك، لن نعود إلى هذه القاعدة مرة أخرى. لكن القارئ مدعو لمتابعة تطورها عبر البرامج، خاصةً عندما لا تعمل.
3.4. الطبقات [DAO] و [JPA]
لنعد إلى البنية التي يتعين علينا إنشاؤها:
![]() |
سنقوم ببناء أربعة مشاريع Maven:
- مشروع للطبقات [DAO] و [JPA]،
- مشروع للطبقة [métier]،
- مشروع للطبقة [web]،
- مشروع مؤسسي يجمع المشاريع الثلاثة السابقة.
نقوم الآن بإنشاء مشروع Maven للطبقات [DAO] و [JPA].
ملاحظة: يتطلب فهم الطبقات [métier] و [DAO] و [JPA] معرفة بلغة Java EE. ولذلك، يمكن قراءة [ref7] (انظر الفقرة 3).
3.4.1. مشروع Netbeans
وهو كما يلي:
![]() |
- في [1]، نقوم بإنشاء مشروع Maven من النوع [EJB Module] [2]،
- في [3]، نسمي المشروع،
![]() |
- في [4]، نختار خادم Glassfish كخادم،
- في [5]، المشروع الذي تم إنشاؤه.
3.4.2. إنشاء الطبقة [JPA]
لنعد إلى البنية التي يتعين علينا إنشاؤها:
![]() |
باستخدام Netbeans، يمكن إنشاء الطبقة [JPA] والطبقة [EJB] التي تتحكم في الوصول إلى الكيانات JPA التي تم إنشاؤها تلقائيًا. من المثير للاهتمام معرفة طرق الإنشاء التلقائي هذه لأن الكود الذي تم إنشاؤه يقدم إرشادات قيّمة حول كيفية كتابة كيانات JPA أو الكود EJB الذي يستخدمها.
نصف الآن بعض أدوات الإنشاء التلقائي هذه. لفهم الكود الذي تم إنشاؤه، يجب أن يكون لديك معرفة جيدة بالكيانات JPA و [ref8] و EJB و [ref7] (انظر الفقرة 3).
3.4.2.1. إنشاء اتصال Netbeans بقاعدة البيانات
- قم بتشغيل SGBD و MySQL 5 حتى يصبح BD متاحًا،
- إنشاء اتصال Netbeans بقاعدة البيانات [dbrdvmedecins2]،
![]() |
- في علامة التبويب [Services] [1]، في الفرع [Databases] [2]، حدد برنامج التشغيل JDBC MySQL [3]،
- ثم حدد الخيار [4] "Connect Using" الذي يسمح بإنشاء اتصال بقاعدة بيانات MySQL،
- في [5]، أدخل المعلومات المطلوبة. في [6]، اسم قاعدة البيانات، وفي [7] اسم المستخدم وكلمة المرور لقاعدة البيانات،
- في [8]، يمكن اختبار المعلومات التي تم تقديمها،
- في [9]، الرسالة المتوقعة عندما تكون هذه المعلومات صحيحة،
![]() |
- في [10]، يتم إنشاء الاتصال. نرى هنا الجداول الأربعة لقاعدة البيانات المتصلة.
3.4.2.2. إنشاء وحدة ثبات
لنعد إلى البنية قيد الإنشاء:
![]() |
نحن بصدد بناء الطبقة [JPA]. يتم تكوينها في ملف [persistence.xml] حيث يتم تعريف وحدات الاستمرارية. تحتاج كل وحدة منها إلى المعلومات التالية:
- خصائص JDBC للوصول إلى قاعدة البيانات (URL، المستخدم، كلمة المرور)،
- الفئات التي ستكون صورًا لجداول قاعدة البيانات،
- التنفيذ JPA المستخدم. في الواقع، JPA هو مواصفة يتم تنفيذها بواسطة منتجات متنوعة. هنا، سنستخدم EclipseLink وهو التنفيذ الافتراضي المستخدم من قبل خادم Glassfish. وهذا يوفر علينا إضافة مكتبات تنفيذ آخر إلى Glassfish.
يمكن لـ Netbeans إنشاء ملف الاستمرارية هذا باستخدام مساعد.
![]() |
- انقر بزر الماوس الأيمن على المشروع واختر إنشاء وحدة ثبات [1]،
- في [2]، قم بتسمية وحدة الاستمرارية التي يتم إنشاؤها،
- في [3]، اختر التنفيذ JPA EclipseLink (JPA 2.0)،
- في [4]، حدد أن المعاملات مع قاعدة البيانات ستدار بواسطة الحاوية EJB لخادم Glassfish،
- في [5]، حدد أن جداول BD قد تم إنشاؤها بالفعل وبالتالي لن يتم إنشاؤها،
![]() |
- في [6]، قم بإنشاء مصدر بيانات جديد لخادم Glassfish،
- في [7]، قم بتسمية JNDI (Java Naming Directory Interface)،
- في [8]، ربط هذا الاسم بالاتصال MySQL الذي تم إنشاؤه في الخطوة السابقة،
![]() |
- إلى [9]، وإنهاء المعالج،
- في [10]، المشروع الجديد،
- في [11]، تم إنشاء الملف [persistence.xml] في المجلد [META-INF]،
- في [12]، تم إنشاء مجلد [setup]،
- في [13]، تمت إضافة تبعيات جديدة إلى مشروع Maven.
الملف [META-INF/persistence.xml] الذي تم إنشاؤه هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
<jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties/>
</persistence-unit>
</persistence>
وهو يتضمن المعلومات التي تم إدخالها في المساعد:
- السطر 3: اسم وحدة الاستمرارية،
- السطر 3: نوع المعاملات مع قاعدة البيانات، وهنا معاملات JTA (Java Transaction Api) التي يديرها الحاوية EJB3 لخادم Glassfish،
- السطر 4: اسم JNDI لمصدر البيانات.
عادةً ما نجد في هذا الملف نوع التنفيذ JPA المستخدم. في المساعد، أشرنا إلى EclipseLink. وبما أن هذا هو التنفيذ JPA المستخدم افتراضيًا من قبل خادم Glassfish، فإنه لم يُذكر في الملف [persistence.xml].
في علامة التبويب [Design]، يمكننا الحصول على عرض عام لملف [persistence.xml]:
![]() |
للحصول على سجلات EclipseLink، سنستخدم الملف [persistence.xml] التالي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="eclipselink.logging.level" value="FINE"/>
</properties>
</persistence-unit>
</persistence>
- السطر 4: يُشار إلى استخدام تطبيق JPA لـ EclipseLink،
- الأسطر 7-9: تجمع خصائص تكوين المزود JPA، وهنا EclipseLink،
- السطر 8: تسمح هذه الخاصية بتسجيل أوامر SQL التي سيصدرها EclipseLink.
الملف [glassfish-resources.xml] الذي تم إنشاؤه هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
<jdbc-connection-pool allow-non-component-callers="false" ... steady-pool-size="8" validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false">
<property name="serverName" value="localhost"/>
<property name="portNumber" value="3306"/>
<property name="databaseName" value="dbrdvmedecins2"/>
<property name="User" value="root"/>
<property name="Password" value=""/>
<property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
</jdbc-connection-pool>
<jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins2" object-type="user" pool-name="mysql_dbrdvmedecins2_rootPool"/>
</resources>
يحتوي هذا الملف على المعلومات التي أدخلناها في المساعدين اللذين استخدمناهما سابقًا:
- الأسطر 5-11: خصائص JDBC لقاعدة البيانات MySQL5 [dbrdvmedecins2]،
- السطر 13: اسم JNDI لمصدر البيانات.
سيتم استخدام هذا الملف لإنشاء مصدر البيانات JNDI [jdbc/dbrdvmedecins2] لخادم Glassfish. وهو خاص تمامًا بهذا الخادم. بالنسبة لخادم آخر، سيتعين اتباع طريقة مختلفة، عادةً باستخدام أداة إدارة. وهذه الأداة موجودة أيضًا لـ Glassfish.
وأخيرًا، تمت إضافة التبعيات إلى المشروع. الملف [pom.xml] هو التالي:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>ejb</packaging>
<name>mv-rdvmedecins-ejb-dao-jpa</name>
...
<dependencies>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>javax.persistence</artifactId>
<version>2.0.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
<version>2.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
...
<repositories>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library Library[eclipselink]</name>
</repository>
</repositories>
</project>
- السطور 32-37، تتطلب طبقة [JPA] الأداة [javaee-api]،
- الأسطر 16 و 22 و 28، العناصر المطلوبة للتنفيذ JPA / EclipseLink المستخدم هنا.
- السطور 18 و24 و30 و36: جميع العناصر لها السمة provided. يُذكر أن هذا يعني أنها ضرورية للتجميع ولكنها ليست ضرورية للتنفيذ. في الواقع، أثناء التنفيذ، يتم توفيرها (provided) بواسطة خادم Glassfish،
- الأسطر 41-48: تحدد مستودعًا جديدًا لمكونات Maven، حيث يمكن العثور على المكونات EclipseLink.
3.4.2.3. إنشاء كيانات JPA
يمكن إنشاء كيانات JPA بواسطة مساعد Netbeans:
![]() |
- في [1]، يتم إنشاء كيانات JPA من قاعدة بيانات،
- في [2]، يتم تحديد مصدر البيانات [jdbc / dbrdvmedecins2] الذي تم إنشاؤه مسبقًا،
- في [3]، قائمة الجداول الخاصة بمصدر البيانات هذا،
- في [4]، نأخذها جميعًا،
![]() |
- في [5]، الجداول المحددة،
- في [6]، نسمي فئات Java المرتبطة بالجداول الأربعة،
- وكذلك اسم حزمة [7]،
- في [8]، JPA يجمع صفوف الجداول من BD في مجموعات. نختار القائمة كمجموعة،
![]() |
- في [9]، فئات Java التي أنشأها المساعد.
3.4.2.4. الكيانات JPA التي تم إنشاؤها
الكيان [Medecin] هو صورة الجدول [medecins]. فئة Java مليئة بالتعليقات التوضيحية التي تجعل الكود صعب القراءة للوهلة الأولى. إذا احتفظنا فقط بما هو ضروري لفهم دور الكيان، نحصل على الكود التالي:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "medecins")
public class Medecin implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
// المنشئات
....
// المُستقبلات والمُعيّنات
....
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- السطر 4، التعليق التوضيحي @Entity يجعل من الفئة [Medecin] كيانًا JPA، c.a.d. فئة مرتبطة بجدول BD عبر API JPA،
- السطر 5، اسم الجدول BD المرتبط بالكيان JPA. كل حقل في الجدول يمثل حقلًا في فئة Java،
- السطر 6، الفئة تنفذ واجهة Serializable. وهذا ضروري في تطبيقات العميل/الخادم، حيث يتم تسلسل الكيانات بين العميل والخادم.
- السطران 10-11: الحقل id في الفئة [Medecin] يطابق الحقل [ID] (السطر 10) في الجدول [medecins]،
- السطران 13-14: الحقل titre للفئة [Medecin] يطابق الحقل [TITRE] (السطر 13) في الجدول [medecins]،
- السطران 16-17: الحقل "الاسم" للفئة [Medecin] يتطابق مع الحقل [NOM] (السطر 16) في الجدول [medecins]،
- السطران 19-20: الحقل version في الفئة [Medecin] يتطابق مع الحقل [VERSION] (السطر 19) في الجدول [medecins]. هنا، لا يتعرف المساعد على حقيقة أن العمود هو في الواقع عمود إصدار يجب زيادته عند كل تعديل للسطر الذي ينتمي إليه. لإعطائه هذا الدور، يجب إضافة التعليق التوضيحي @Version. سنقوم بذلك في خطوة لاحقة،
- السطران 22-23: الحقل prenom في الفئة [Medecin] يتوافق مع الحقل [PRENOM] في الجدول [medecins]،
- السطران 10-11: الحقل id يطابق المفتاح الأساسي [ID] للجدول. توضح التعليقات التوضيحية في السطرين 8-9 هذه النقطة،
- السطر 8: تشير التعليقات التوضيحية @Id إلى أن الحقل المعلق عليه مرتبط بالمفتاح الأساسي للجدول،
- السطر 9: ستقوم الطبقة [JPA] بإنشاء المفتاح الأساسي للسطور التي ستقوم بإدراجها في الجدول [Medecins]. هناك عدة استراتيجيات ممكنة. هنا تشير الاستراتيجية GenerationType.IDENTITY إلى أن الطبقة JPA ستستخدم الوضع auto_increment للجدول MySQL،
- السطران 25-26: تحتوي الجدولة [creneaux] على مفتاح خارجي في الجدولة [medecins]. تنتمي كل فترة زمنية إلى طبيب واحد. وبالعكس، يرتبط كل طبيب بعدة فترات زمنية. لذلك لدينا علاقة واحد (طبيب) إلى عدة (فترات زمنية)، وهي علاقة محددة بالتعليق @OneToMany بواسطة JPA (السطر 25). سيحتوي حقل السطر 26 على جميع المواعيد المخصصة للطبيب. وذلك دون الحاجة إلى برمجة. لفهم السطر 25 بشكل كامل، يتعين علينا عرض الفئة [Creneau].
وهي كما يلي:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "MDEBUT")
private int mdebut;
@Column(name = "HFIN")
private int hfin;
@Column(name = "HDEBUT")
private int hdebut;
@Column(name = "MFIN")
private int mfin;
@Column(name = "VERSION")
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
private List<Rv> rvList;
// المنشئات
...
// الوصول والتعيين
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
نكتفي بالتعليق على التعليقات التوضيحية الجديدة:
- لقد ذكرنا أن الجدول [creneaux] يحتوي على مفتاح خارجي إلى الجدول [medecins]: حيث يرتبط موعد طبيب واحد. ويمكن أن ترتبط عدة مواعيد بنفس الطبيب. لدينا علاقة من الجدول [creneaux] إلى الجدول [medecins] والتي توصف بأنها علاقة متعددة (فترات) إلى واحد (طبيب). يستخدم التعليق التوضيحي @ManyToOne في السطر 32 لتعريف المفتاح الأجنبي،
- السطر 31 مع التعليق التوضيحي @JoinColumn يحدد علاقة المفتاح الأجنبي: العمود [ID_MEDECIN] في الجدول [creneaux] هو مفتاح خارجي في العمود [ID] في الجدول [medecins]،
- السطر 33: إشارة إلى الطبيب المالك للفتحة الزمنية. يتم الحصول عليها هنا أيضًا دون الحاجة إلى البرمجة.
وبالتالي، فإن رابط المفتاح الأجنبي بين الكيان [Creneau] والكيان [Medecin] يتجسد في شكل تعليقين:
- في الكيان [Creneau]:
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin idMedecin;
- في الكيان [Medecin]:
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Creneau> creneauList;
يعكس التعليقان نفس العلاقة: علاقة المفتاح الأجنبي للجدول [creneaux] بالجدول [medecins]. ويقال إنهما متعاكسان. العلاقة @ManyToOne هي الوحيدة الضرورية. فهي تحدد علاقة المفتاح الأجنبي بشكل لا لبس فيه. أما العلاقة @OneToMany فهي اختيارية. وإذا كانت موجودة، فإنها تكتفي بالإشارة إلى العلاقة @ManyToOne المرتبطة بها. هذا هو معنى السمة mappedBy في السطر 1 من الكيان [Medecin]. قيمة هذا السمة هي اسم حقل الكيان [Creneau] الذي يحتوي على التعليق التوضيحي @ManyToOne الذي يحدد المفتاح الأجنبي. وفي نفس السطر 1 من الكيان [Medecin]، يحدد السمة cascade=CascadeType.ALL سلوك الكيان [Medecin] تجاه الكيان [Creneau]:
- إذا تم إدراج كيان جديد [Medecin] في قاعدة البيانات، فيجب إدراج كيانات [Creneau] في حقل السطر 2 أيضًا،
- إذا تم تعديل كيان [Medecin] في قاعدة البيانات، فيجب تعديل كيانات [Creneau] في حقل السطر 2 أيضًا،
- إذا تم حذف كيان [Medecin] من قاعدة البيانات، فيجب حذف كيانات [Creneau] من حقل السطر 2 أيضًا.
نقدم رمز الكيانين الآخرين دون تعليقات خاصة لأنهما لا يقدمان أي ترميزات جديدة.
الكيان [Client]
package rdvmedecins.jpa;
...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "TITRE")
private String titre;
@Column(name = "NOM")
private String nom;
@Column(name = "VERSION")
private int version;
@Column(name = "PRENOM")
private String prenom;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
private List<Rv> rvList;
// منشئات
...
// الوصول والتعيين
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- تعكس السطران 24-25 علاقة المفتاح الخارجي بين الجدول [rv] والجدول [clients].
الكيان [Rv]:
package rdvmedecins.jpa;
...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau idCreneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client idClient;
// منشئات
...
// الوصول والتعيين
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
...
}
}
- السطر 13 يحدد الحقل "اليوم" من نوع Java Date. يُشار إلى أنه في الجدول [rv]، العمود [JOUR] (السطر 12) هو من نوع التاريخ (بدون ساعة)،
- الأسطر 16-18: تحدد علاقة المفتاح الخارجي بين الجدول [rv] والجدول [creneaux]،
- السطور 20-22: تحدد علاقة المفتاح الأجنبي التي تربط الجدول [rv] بالجدول [clients].
يتيح لنا الإنشاء التلقائي للكيانات JPA الحصول على قاعدة عمل. أحيانًا تكون كافية، وأحيانًا لا. وهذا هو الحال هنا:
- يجب إضافة التعليق التوضيحي @Version إلى الحقول المختلفة الخاصة بالإصدار في الكيانات،
- يجب كتابة طرق toString أكثر وضوحًا من تلك التي تم إنشاؤها،
- الكيانات [Medecin] و [Client] متشابهة. سنجعلها مشتقة من فئة [Personne]،
- سنقوم بإزالة العلاقات @OneToMany العكسية للعلاقات @ManyToOne. فهي ليست ضرورية وتسبب تعقيدات في البرمجة،
- سنحذف التحقق من صحة @NotNull على المفاتيح الأساسية. عندما نحفظ كيان JPA مع MySQL، يكون للكيان في البداية مفتاح أساسي null. ولا يكون للمفتاح الأساسي للعنصر المحفوظ قيمة إلا بعد حفظه في قاعدة البيانات.
مع هذه المواصفات، تصبح الفئات المختلفة كما يلي:
تُستخدم فئة Personne لتمثيل الأطباء والعملاء:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@MappedSuperclass
public class Personne implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@Size(min = 1, max = 5)
@Column(name = "TITRE")
private String titre;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 30)
@Column(name = "NOM")
private String nom;
@Basic(optional = false)
@NotNull
@Column(name = "VERSION")
@Version
private int version;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 30)
@Column(name = "PRENOM")
private String prenom;
// منشئات
...
// الوصول والتعيين
...
@Override
public String toString() {
return String.format("[%s,%s,%s,%s,%s]", id, version, titre, prenom, nom);
}
}
- السطر 8: تجدر الإشارة إلى أن الفئة [Personne] ليست كيانًا (@Entity) بحد ذاتها. ستكون الفئة الأم للكيانات. يشير التعليق التوضيحي @MappedSuperClass إلى هذه الحالة.
تغلف الكيان [Client] صفوف الجدول [clients]. وهي مشتقة من الفئة [Personne] السابقة:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "clients")
public class Client extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// منشئات
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Client[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
- السطر 6: الفئة [Client] هي كيان Jpa،
- السطر 7: وهي مرتبطة بالجدول [clients]،
- السطر 8: وهي مشتقة من الفئة [Personne].
الكيان [Medecin] الذي يغلف أسطر الجدول [medecins] يتبع نفس النموذج:
package rdvmedecins.jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne implements Serializable {
private static final long serialVersionUID = 1L;
// المنشئات
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Médecin[%s,%s,%s,%s]", getId(), getTitre(), getPrenom(), getNom());
}
}
تغلف الكيان [Creneau] صفوف الجدول [creneaux]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "creneaux")
public class Creneau implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@NotNull
@Column(name = "MDEBUT")
private int mdebut;
@Basic(optional = false)
@NotNull
@Column(name = "HFIN")
private int hfin;
@Basic(optional = false)
@NotNull
@Column(name = "HDEBUT")
private int hdebut;
@Basic(optional = false)
@NotNull
@Column(name = "MFIN")
private int mfin;
@Basic(optional = false)
@NotNull
@Column(name = "VERSION")
@Version
private int version;
@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Medecin medecin;
// المنشئات
...
// المنشورات والمُعيّنات
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
// TODO: تحذير - لن تعمل هذه الطريقة في حالة عدم تعيين حقول المعرف
...
}
@Override
public String toString() {
return String.format("Creneau [%s, %s, %s:%s, %s:%s,%s]", id, version, hdebut, mdebut, hfin, mfin, medecin);
}
}
- السطور 45-47 تمثل العلاقة "متعدد إلى واحد" الموجودة بين الجدول [creneaux] والجدول [medecins] في قاعدة البيانات: الطبيب لديه عدة مواعيد، والموعد ينتمي إلى طبيب واحد.
تغلف الكيان [Rv] الأسطر في الجدول [rv]:
package rdvmedecins.jpa;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Basic(optional = false)
@NotNull
@Column(name = "JOUR")
@Temporal(TemporalType.DATE)
private Date jour;
@JoinColumn(name = "ID_CRENEAU", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Creneau creneau;
@JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Client client;
// منشئات
...
// مُستردات ومُعيّنات
...
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object object) {
...
}
@Override
public String toString() {
return String.format("Rv[%s, %s, %s]", id, creneau, client);
}
}
- السطور 29-31 تمثل العلاقة "متعدد إلى واحد" الموجودة بين الجدول [rv] والجدول [clients] (يمكن أن يظهر عميل واحد في عدة Rv) في قاعدة البيانات، بينما تمثل الأسطر 25-27 العلاقة "متعدد إلى واحد" الموجودة بين الجدول [rv] والجدول [creneaux] (يمكن أن يظهر موعد واحد في عدة Rv).
3.4.3. فئة الاستثناء
![]() |
فئة الاستثناء [RdvMedecinsException] للتطبيق هي كما يلي:
package rdvmedecins.exceptions;
import java.io.Serializable;
import javax.ejb.ApplicationException;
@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{
// الحقول الخاصة
private int code = 0;
// منشئات
public RdvMedecinsException() {
super();
}
public RdvMedecinsException(String message) {
super(message);
}
public RdvMedecinsException(String message, Throwable cause) {
super(message, cause);
}
public RdvMedecinsException(Throwable cause) {
super(cause);
}
public RdvMedecinsException(String message, int code) {
super(message);
setCode(code);
}
public RdvMedecinsException(Throwable cause, int code) {
super(cause);
setCode(code);
}
public RdvMedecinsException(String message, Throwable cause, int code) {
super(message, cause);
setCode(code);
}
// مُستردات - مُعيّنات
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- السطر 7: الفئة مشتقة من الفئة [RuntimeException]. وبالتالي، لا يفرض المُجمِّع معالجتها باستخدام try / catch.
- السطر 6: التعليق التوضيحي @ApplicationException يجعل الاستثناء لا يتم "ابتلاعه" بواسطة استثناء من النوع [EjbException].
لفهم التعليق التوضيحي @ApplicationException، لنعد إلى البنية المستخدمة على جانب الخادم:
![]() |
سيتم إطلاق الاستثناء من النوع [RdvMedecinsException] بواسطة طرق EJB من الطبقة [DAO] داخل الحاوية EJB3 وسيتم اعتراضه بواسطة هذه الحاوية. بدون التعليق التوضيحي @ApplicationException، يقوم الحاوية EJB3 بتغليف الاستثناء الذي حدث في استثناء من النوع [EjbException] وإعادة إطلاقه. قد لا نرغب في هذا التغليف ونسمح لـ EJB3 بإخراج استثناء من النوع [RdvMedecinsException]. وهذا ما تسمح به التعليقة التوضيحية @ApplicationException. بالإضافة إلى ذلك، يشير السمة (rollback=true) لهذا التعليق التوضيحي إلى الحاوية EJB3 بأنه إذا حدثت استثناء من النوع [RdvMedecinsException] داخل طريقة يتم تنفيذها ضمن معاملة مع SGBD، فيجب إلغاء هذه المعاملة. من الناحية الفنية، يُطلق على ذلك اسم إجراء rollback للمعاملة.
3.4.4. EJB من الطبقة [DAO]
![]() |
![]() |
واجهة java [IDao] للطبقة [DAO] هي كما يلي:
package rdvmedecins.dao;
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
public interface IDao {
// قائمة العملاء
public List<Client> getAllClients();
// قائمة الأطباء
public List<Medecin> getAllMedecins();
// قائمة المواعيد المتاحة للطبيب
public List<Creneau> getAllCreneaux(Medecin medecin);
// قائمة مواعيد الطبيب في يوم معين
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// البحث عن عميل باستخدام رقم التعريف الخاص به
public Client getClientById(Long id);
// البحث عن عميل باستخدام رقم التعريف الخاص به
public Medecin getMedecinById(Long id);
// البحث عن موعد محدد برقمه التعريفي
public Rv getRvById(Long id);
// البحث عن فترة زمنية محددة برقم التعريف الخاص بها
public Creneau getCreneauById(Long id);
// إضافة RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// حذف RV
public void supprimerRv(Rv rv);
}
تم إنشاء هذه الواجهة بعد تحديد احتياجات الطبقة [web]:
- السطر 14: قائمة العملاء. سنحتاج إليها لتغذية القائمة المنسدلة للعملاء،
- السطر 16: قائمة الأطباء. سنحتاجها لتغذية القائمة المنسدلة للأطباء،
- السطر 18: قائمة المواعيد المتاحة للطبيب. سنحتاجها لعرض جدول مواعيد الطبيب ليوم معين،
- السطر 20: قائمة مواعيد الطبيب في يوم معين. وبالاقتران مع الطريقة السابقة، ستسمح لنا هذه الطريقة بعرض جدول مواعيد الطبيب في يوم معين مع الفترات التي تم حجزها بالفعل،
- السطر 22: يتيح العثور على عميل من خلال رقمه. ستسمح لنا هذه الطريقة بالعثور على عميل من خلال الاختيار من القائمة المنسدلة للعملاء،
- السطر 24: نفس الشيء بالنسبة للأطباء،
- السطر 26: يبحث عن موعد من خلال رقمه. يمكن استخدامه عند حذف موعد للتحقق مسبقًا من وجوده بالفعل،
- السطر 28: يبحث عن فترة زمنية من خلال رقمها. يسمح بتحديد الفترة التي يريد المستخدم إضافتها أو حذفها،
- السطر 30: لإضافة موعد،
- السطر 32: لحذف موعد.
تكتفي الواجهة المحلية [IDaoLocal] الخاصة بـ EJB باستنباط الواجهة السابقة [IDao]:
package rdvmedecins.dao;
import javax.ejb.Local;
@Local
public interface IDaoLocal extends IDao{
}
وينطبق الأمر نفسه على الواجهة البعيدة [IDaoRemote]:
package rdvmedecins.dao;
import javax.ejb.Remote;
@Remote
public interface IDaoRemote extends IDao{
}
تقوم EJB [DaoJpa] بتنفيذ الواجهتين، المحلية والبعيدة:
package rdvmedecins.dao;
...
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
- تشير السطر 5 إلى أن EJB البعيد يحمل الاسم "rdvmedecins.dao". بالإضافة إلى ذلك، فإن التعليق التوضيحي @Singleton (Java EE6) يعني أنه لن يتم إنشاء سوى مثيل واحد من EJB. يحدد التعليق التوضيحي @Stateless (Java EE5) كائن EJB الذي يمكن إنشاؤه في مثيلات متعددة لتغذية مجموعة من كائنات EJB،
- تشير السطر 6 إلى أن جميع أساليب EJB تتم ضمن معاملة يديرها الحاوية EJB3،
- يُظهر السطر 7 أن EJB يُنفذ الواجهات المحلية والبعيدة، كما أنه قابل للتسلسل.
الرمز الكامل لـ EJB هو كما يلي:
package rdvmedecins.dao;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import rdvmedecins.exceptions.RdvMedecinsException;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
@PersistenceContext
private EntityManager em;
// قائمة العملاء
public List<Client> getAllClients() {
try {
return em.createQuery("select rc from Client rc").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 1);
}
}
// قائمة الأطباء
public List<Medecin> getAllMedecins() {
try {
return em.createQuery("select rm from Medecin rm").getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 2);
}
}
// قائمة المواعيد المتاحة لطبيب معين
// الطبيب: الطبيب
public List<Creneau> getAllCreneaux(Medecin medecin) {
try {
return em.createQuery("select rc from Creneau rc join rc.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 3);
}
}
// قائمة مواعيد طبيب معين، في يوم معين
// medecin: الطبيب
// اليوم: اليوم
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
try {
return em.createQuery("select rv from Rv rv join rv.creneau c join c.idMedecin m where m.id=:idMedecin and rv.jour=:jour").setParameter("idMedecin", medecin.getId()).setParameter("jour", jour).getResultList();
} catch (Throwable th) {
throw new RdvMedecinsException(th, 3);
}
}
// إضافة موعد
// اليوم: يوم الموعد
// الفترة الزمنية: الفترة الزمنية للموعد
// العميل: العميل الذي تم تحديد الموعد له
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
try {
Rv rv = new Rv(null, jour);
rv.setClient(client);
rv.setCreneau(creneau);
em.persist(rv);
return rv;
} catch (Throwable th) {
throw new RdvMedecinsException(th, 4);
}
}
// حذف موعد
// الموعد: الموعد الذي تم حذفه
public void supprimerRv(Rv rv) {
try {
em.remove(em.merge(rv));
} catch (Throwable th) {
throw new RdvMedecinsException(th, 5);
}
}
// استرداد عميل معين
public Client getClientById(Long id) {
try {
return (Client) em.find(Client.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// استرداد طبيب معين
public Medecin getMedecinById(Long id) {
try {
return (Medecin) em.find(Medecin.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// استرداد موعد معين
public Rv getRvById(Long id) {
try {
return (Rv) em.find(Rv.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
// استرداد موعد محدد
public Creneau getCreneauById(Long id) {
try {
return (Creneau) em.find(Creneau.class, id);
} catch (Throwable th) {
throw new RdvMedecinsException(th, 6);
}
}
}
- السطر 22: الكائن EntityManager الذي يدير الوصول إلى سياق الاستمرارية. عند إنشاء مثيل للفئة، سيتم تهيئة هذا الحقل بواسطة الحاوية EJB بفضل التعليق التوضيحي @PersistenceContext في السطر 21،
- السطر 27: استعلام JPQL (لغة استعلامات الاستمرارية في Java) الذي يعرض جميع صفوف الجدول [clients] في شكل قائمة من الكائنات [Client]،
- السطر 36: استعلام مماثل للأطباء،
- السطر 46: استعلام JPQL يقوم بدمج الجدولين [creneaux] و [medecins]. يتم تعيينه بواسطة معرف الطبيب،
- السطر 57: استعلام JPQL يقوم بدمج الجداول [rv] و [creneaux] و [medecins] وله معلمتان: معرف الطبيب ويوم الموعد،
- السطور 69-73: إنشاء موعد ثم حفظه في قاعدة البيانات،
- السطر 83: حذف موعد من قاعدة البيانات،
- السطر 92: تنفيذ select على قاعدة البيانات للعثور على عميل معين،
- السطر 101: نفس الشيء بالنسبة للطبيب،
- السطر 110: نفس الشيء بالنسبة لموعد،
- السطر 119: نفس الشيء بالنسبة لفترة زمنية،
- جميع العمليات التي تستخدم سياق الاستمرارية em في السطر 22 قد تواجه مشكلة مع قاعدة البيانات. ولذلك، فهي جميعها محاطة بـ try / catch. يتم تغليف الاستثناء المحتمل في الاستثناء "الداخلي" RdvMedecinsException.
3.4.5. تنفيذ برنامج التشغيل JDBC من MySQL
في البنية التالية:
![]() |
يحتاج EclipseLink إلى برنامج التشغيل JDBC من MySQL. يجب تثبيت هذا البرنامج في مكتبات خادم Glassfish في المجلد <glassfish> /domains /domain1 /lib /ext حيث <glassfish> هو مجلد تثبيت خادم Glassfish. يمكن الحصول عليه بالطريقة التالية:
![]() |
المجلد الذي يجب وضع برنامج التشغيل JDBC من MySQL فيه هو <Domains folder>[1]/domain1/lib/ext [2]. هذا البرنامج التشغيلي متاح في URL [http://www.mysql.fr/downloads/connector/j/]. بمجرد تثبيته، يجب إعادة تشغيل خادم Glassfish حتى يأخذ هذه المكتبة الجديدة في الاعتبار.
3.4.6. نشر EJB من الطبقة [DAO]
لنعد إلى البنية التي تم إنشاؤها حتى الآن:
![]() |
يجب نشر مجموعة [web, métier, DAO, JPA] على خادم Glassfish. نقوم بذلك:
![]() |
- في [1]، نقوم ببناء مشروع Maven،
- في [2]، نقوم بتنفيذه،
- في [3]، تم نشره على خادم Glassfish (علامة التبويب [Services])
قد نكون فضوليين لمشاهدة سجلات Glassfish:
![]() |
في [1]، تتوفر سجلات Glassfish في علامة التبويب [Output / Glassfish Server 3+]. وهي كما يلي:
السطور المحددة بـ [Config] و [Précis] هي سجلات EclipseLink، أما تلك المحددة بـ [Infos] فهي من Glassfish.
- السطور 1-12: يعالج EclipseLink الكيانات JPA التي اكتشفها،
- الأسطر 13-17: معلومات توضح أن معالجة الكيانات JPA تمت بشكل طبيعي،
- السطر 18: EclipseLink يظهر،
- السطر 19: EclipseLink يتعرف على أنه يتعامل مع SGBD MySQL،
- الأسطر 20-24: يحاول EclipseLink الاتصال بـ BD،
- الأسطر 25-28: نجح في ذلك،
- الأسطر 29-33: يحاول إعادة الاتصال هذه المرة باستخدام منصة MySQL على وجه التحديد (السطر 30)،
- الأسطر 34-37: نجح أيضًا،
- السطر 38: تأكيد أنه تم إنشاء مثيل لوحدة الاستمرارية [dbrdvmedecins-PU]،
- السطر 39: الأسماء القابلة للنقل للواجهات البعيدة والمحلية لـ EJB [DaoJpa]، حيث تعني كلمة "قابلة للنقل" أنها معترف بها من قبل جميع خوادم تطبيقات Java EE 6،
- السطر 40: أسماء الواجهات البعيدة والمحلية لـ EJB [DaoJpa]، الخاصة بـ Glassfish. سنستخدم في الاختبار القادم الاسم "rdvmedecins.dao".
السطران 39 و 40 مهمان. عند كتابة عميل EJB على Glassfish، من الضروري معرفتهما.
3.4.7. اختبارات EJB للطبقة [DAO]
الآن بعد أن تم نشر EJB من طبقة [DAO] لتطبيقنا، يمكننا اختباره. سنقوم بذلك في إطار تطبيق عميل/خادم:
![]() |
سيقوم العميل باختبار الواجهة البعيدة لـ EJB [DAO] التي تم نشرها على خادم Glassfish.
نبدأ بإنشاء مشروع Maven جديد :
![]() |
- في [1]، نقوم بإنشاء مشروع جديد،
- في [2,3]، نقوم بإنشاء مشروع Maven من النوع [Java Application]،
- في [4]، نسميه ونضعه في نفس المجلد الذي يحتوي على EJB و [DAO]،
![]() |
- إلى [5]، المشروع الذي تم إنشاؤه،
- في [6]، تم إنشاء فئة [App.java]. سنقوم بحذفها،
- في [7]، تم إنشاء فرع [Source Packages]. لم نكن قد صادفناه بعد. يمكننا وضع اختبارات JUnit في هذا الفرع. سنقوم بذلك. لن نحتفظ بفئة الاختبار [AppTest] التي تم إنشاؤها،
- في [8]، تبعيات مشروع Maven. الفرع [Dependencies] فارغ. سنضطر إلى وضع تبعيات جديدة فيه. يجمع الفرع [Test Dependencies] التبعيات اللازمة للاختبارات. هنا، المكتبة المستخدمة هي مكتبة إطار العمل JUnit 3.8. سنضطر إلى تغييرها.
يتطور المشروع على النحو التالي:
![]() |
- إلى [1]، وهو المشروع الذي تم فيه حذف الفئتين اللتين تم إنشاؤهما بالإضافة إلى التبعية JUnit.
لنعد إلى بنية العميل/الخادم التي ستُستخدم في الاختبار:
![]() |
يحتاج العميل إلى معرفة الواجهة البعيدة التي يوفرها EJB [DAO]. علاوة على ذلك، سيتبادل مع EJB كيانات JPA. لذلك فهو بحاجة إلى تعريف هذه الكيانات. لكي يتمكن مشروع اختبار EJB من الوصول إلى هذه المعلومات، سنقوم بإضافة مشروع EJB و [DAO] كاعتماد للمشروع:
![]() |
- في [1]، نضيف تبعية إلى الفرع [Test Dependencies]،
- في [2]، نختار علامة التبويب [Open Projects]،
- في [3]، نختار مشروع Maven الخاص بـ EJB [DAO]،
![]() |
- في [4]، التبعية المضافة.
لنعد إلى بنية العميل/الخادم للاختبار:
![]() |
عند التنفيذ، يتواصل العميل والخادم عبر الشبكة TCP-IP. لن نقوم ببرمجة هذه التبادلات. لكل خادم تطبيقات، توجد مكتبة يجب دمجها في تبعيات العميل. تسمى مكتبة Glassfish [gf-client]. نضيفها:
![]() |
- في [1]، نضيف تبعية،
- في [2]، نحدد خصائص الأرتيفاكت المطلوب،
- في [3]، يتم إضافة العديد من التبعيات. سيقوم Maven بتنزيلها. قد يستغرق هذا عدة دقائق. ثم يتم تخزينها في مستودع Maven المحلي.
يمكننا الآن إنشاء الاختبار JUnit:
![]() |
- في [2]، نضغط بزر الفأرة الأيمن على [Test Packages] لإنشاء اختبار جديد JUnit،
![]() |
- إلى [3]، ثم قم بتسمية فئة الاختبار وتعيين حزمة لها [4]،
- إلى [5]، نختار إطار العمل JUnit 4.x،
- في [6]، فئة الاختبار التي تم إنشاؤها،
- في [7]، التبعيات الجديدة لمشروع Maven.
يصبح ملف [pom.xml] كما يلي:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-client-rdvmedecins-ejb-dao</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-client-rdvmedecins-ejb-dao</name>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
<id>eclipselink</id>
<layout>default</layout>
<name>Repository for library Library[eclipselink]</name>
</repository>
<repository>
<url>http://repo1.maven.org/maven2/</url>
<id>junit_4</id>
<layout>default</layout>
<name>Repository for library Library[junit_4]</name>
</repository>
</repositories>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.appclient</groupId>
<artifactId>gf-client</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
تجدر الإشارة إلى:
- السطور 32-51، تبعيات المشروع،
- الأسطر 13-26، تم تعريف مستودعين Maven، أحدهما لـ EclipseLink (الأسطر 14-19)، والآخر لـ JUnit4 (الأسطر 20-25).
وستكون فئة الاختبار كما يلي:
package rdvmedecins.tests.dao;
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import rdvmedecins.dao.IDaoRemote;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
public class JUnitTestDao {
// طبقة [dao] تم اختبارها
private static IDaoRemote dao;
// تاريخ اليوم
Date jour = new Date();
@BeforeClass
public static void init() throws NamingException {
// تهيئة بيئة JNDI
InitialContext initialContext = new InitialContext();
// إنشاء مثيل لطبقة dao
dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
}
@Test
public void test1() {
// عرض العملاء
List<Client> clients =dao.getAllClients();
display("Liste des clients :", clients);
// عرض الأطباء
List<Medecin> medecins =dao.getAllMedecins();
display("Liste des médecins :", medecins);
// عرض المواعيد المتاحة للطبيب
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = dao.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// قائمة مواعيد الطبيب في يوم معين
display(String.format("Liste des créneaux du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// إضافة RV
Rv rv = null;
Creneau creneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
rv = dao.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// إضافة RV في نفس الموعد من نفس اليوم
// يجب أن يتسبب في استثناء
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
Boolean erreur = false;
try {
rv = dao.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// يتم تسجيل الخطأ
erreur=true;
}
// نتحقق من وجود خطأ
Assert.assertTrue(erreur);
// قائمة بـ RV
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
// حذف RV
System.out.println("Suppression du Rv ajouté");
dao.supprimerRv(rv);
System.out.println("Rv supprimé");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), dao.getRvMedecinJour(medecin, jour));
}
// طريقة مساعدة - تعرض عناصر مجموعة
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- الأسطر 23-29: يتم تنفيذ الطريقة الموسومة بـ @BeforeClass قبل جميع الطرق الأخرى. هنا، يتم إنشاء مرجع على الواجهة البعيدة لـ EJB [DaoJpa]. نتذكر أننا أطلقنا عليها اسم JNDI "rdvmedecins.dao"،
- السطران 34-35: يعرضان قائمة العملاء،
- السطران 37-38: يعرضان قائمة الأطباء،
- السطور 40-42: تعرض المواعيد المتاحة للطبيب الأول،
- السطر 44: يعرض مواعيد الطبيب الأول ليوم السطر 21،
- السطور 46-51: تضيف موعدًا للطبيب الأول، في الفترة الزمنية رقم 2 و اليوم المذكور في السطر 21،
- السطر 52: يعرض مواعيد الطبيب الأول ليوم السطر 21 للتحقق منها. يجب أن يكون هناك موعد واحد على الأقل، وهو الذي تمت إضافته للتو،
- السطور 55-70: نضيف نفس الموعد. نظرًا لأن الجدول [RV] له قيد التفرد، فإن هذا الإضافة يجب أن تؤدي إلى استثناء. نتأكد من ذلك في السطر 70،
- السطر 72: نعرض للتحقق مواعيد الطبيب الأول ليوم السطر 21. يجب ألا يكون الموعد الذي أردنا إضافته موجودًا هناك،
- السطور 74-76: نحذف الموعد الوحيد الذي تمت إضافته،
- السطر 77: يعرض مواعيد الطبيب الأول ليوم السطر 21 للتحقق. يجب ألا يكون الموعد الذي تم حذفه للتو موجودًا هناك.
هذا الاختبار هو اختبار وهمي JUnit. لا يوجد فيه سوى تأكيد واحد (السطر 70). إنه اختبار بصري مع العيوب التي تصاحبه.
إذا سارت الأمور على ما يرام، يجب أن تنجح الاختبارات:
![]() |
- في [1]، نقوم بإنشاء مشروع الاختبار،
- في [2]، يتم تنفيذ الاختبار،
- في [3]، تم اجتياز الاختبار بنجاح.
لنلقِ نظرة عن كثب على نتائج الاختبار:
يُطلب من القارئ قراءة هذه السجلات جنبًا إلى جنب مع الكود الذي أنتجها. سنتوقف عند الاستثناء الذي حدث عند إضافة موعد موجود بالفعل، الأسطر 41-49. يتم عرض مكدس الاستثناءات في الأسطر 42-48. وهو أمر غير متوقع. لنعد إلى كود طريقة إضافة موعد:
// إضافة موعد
// اليوم: يوم الموعد
// الفترة الزمنية: الفترة الزمنية للموعد
// العميل: العميل الذي تم تحديد الموعد من أجله
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
try {
Rv rv = new Rv(null, jour);
rv.setClient(client);
rv.setCreneau(creneau);
System.out.println(String.format("avant persist : %s",rv));
em.persist(rv);
System.out.println(String.format("après persist : %s",rv));
return rv;
} catch (Throwable th) {
throw new RdvMedecinsException(th, 4);
}
}
لنلقِ نظرة على سجلات Glassfish عند إضافة الموعدين:
- السطر 2: قبل أول persist،
- السطر 3: بعد أول persist،
- السطر 4: الأمر INSERT الذي سيتم تنفيذه. يُلاحظ أنه لا يحدث في نفس الوقت مع العملية persist. لو كان الأمر كذلك، لظهر هذا السجل قبل السطر 2. ثم تحدث العملية INSERT عادةً في نهاية المعاملة التي يتم فيها تنفيذ الطريقة،
- السطر 6: تطلب EclipseLink من MySQL معرفة آخر مفتاح أساسي تم استخدامه. وستحصل على المفتاح الأساسي للموعد المضاف. وستُملأ هذه القيمة حقل id للكيان [Rv] الذي تم حفظه،
- السطران 7-8: الاستعلام SELECT الذي سيعرض مواعيد الطبيب،
- السطران 9-10: عروض الشاشة للاستعلام الثاني persist،
- السطران 11-12: الأمر INSERT الذي سيتم تنفيذه. يجب أن يتسبب في حدوث استثناء. يظهر هذا الاستثناء في السطرين 15-16 وهو واضح. يتم إطلاقها في البداية بواسطة برنامج التشغيل JDBC من MySQL بسبب انتهاك قيد تفرد المواعيد. نستنتج من ذلك أنه من المفترض أن نرى هذه الاستثناءات في سجلات اختبار JUnit. لكن هذا ليس هو الحال:
دعونا نذكر بنية العميل/الخادم للاختبار:
![]() |
عندما يطلق EJB [DAO] استثناءً، يجب تسلسله ليصل إلى العميل. من المحتمل أن هذه العملية هي التي فشلت لسبب لم أفهمه. وبما أن تطبيقنا الكامل لن يعمل بنظام العميل/الخادم، يمكننا تجاهل هذه المشكلة.
الآن بعد أن أصبح EJB من الطبقة [DAO] جاهزًا للعمل، يمكننا الانتقال إلى EJB من الطبقة [métier].
3.5. الطبقة [métier]
لنعد إلى بنية التطبيق قيد الإنشاء:
![]() |
سنقوم بإنشاء مشروع Maven جديد لـ EJB [métier]. كما نرى أعلاه، سيكون له تبعية على مشروع Maven الذي تم إنشاؤه للطبقات [DAO] و [JPA].
3.5.1. مشروع Netbeans
نقوم بإنشاء مشروع Maven جديد من النوع EJB. للقيام بذلك، ما عليك سوى اتباع الإجراء الذي تم استخدامه ووصفه سابقًا في الصفحة 174.
![]() |
- في [1]، مشروع Maven للطبقة [métier]،
- في [2]، نضيف تبعية،
- في [3]، نختار مشروع Maven للطبقات [DAO] و [JPA]،
- في [4]، نختار النطاق [provided]. نذكر أن هذا يعني أننا نحتاج إليه للتجميع ولكن ليس لتنفيذ المشروع. في الواقع، سيتمسيتم نشر EJB من الطبقة [métier] على خادم Glassfish مع EJB من الطبقتين [DAO] و [JPA]. لذلك عند تشغيله، سيكون EJB من الطبقات [DAO] و [JPA] موجودًا بالفعل،
![]() |
- في [6]، المشروع الجديد مع تبعياته.
لنقدم الآن أكواد المصدر للطبقة [métier]:
![]() |
سيكون لـ EJB [Metier] الواجهة التالية [IMetier]:
package rdvmedecins.metier.service;
import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
public interface IMetier {
// طبقة DAO
// قائمة العملاء
public List<Client> getAllClients();
// قائمة الأطباء
public List<Medecin> getAllMedecins();
// قائمة فترات مواعيد الطبيب
public List<Creneau> getAllCreneaux(Medecin medecin);
// قائمة مواعيد الطبيب في يوم معين
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour);
// البحث عن عميل محدد برقمه التعريفي
public Client getClientById(Long id);
// البحث عن عميل محدد برقمه التعريفي
public Medecin getMedecinById(Long id);
// البحث عن موعد محدد برقمه التعريفي
public Rv getRvById(Long id);
// البحث عن فترة زمنية محددة برقم التعريف الخاص بها
public Creneau getCreneauById(Long id);
// إضافة RV
public Rv ajouterRv(Date jour, Creneau creneau, Client client);
// حذف RV
public void supprimerRv(Rv rv);
// المهنة
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
}
لفهم هذه الواجهة، يجب تذكر بنية المشروع:
![]() |
لقد حددنا واجهة الطبقة [DAO] (الفقرة 3.4.4) وأشرنا إلى أنها تلبي احتياجات الطبقة [web]، وهي احتياجات المستخدم. لا تتواصل الطبقة [web] مع الطبقة [DAO] إلا عبر الطبقة [métier]. وهذا يفسر وجود جميع أساليب الطبقة [DAO] في الطبقة [métier]. ولن تقوم هذه الأساليب سوى بتفويض طلب الطبقة [web] إلى الطبقة [DAO]. لا شيء أكثر من ذلك.
أثناء دراسة التطبيق، ظهرت حاجة: أن نكون قادرين على عرض جدول مواعيد الطبيب ليوم معين على صفحة ويب لمعرفة المواعيد المشغولة والمتاحة في ذلك اليوم. وهذا هو الحال عادةً عندما ترد السكرتيرة على طلب عبر الهاتف. يُطلب منها تحديد موعد في يوم معين مع طبيب معين. لتلبية هذه الحاجة، توفر الطبقة [métier] الطريقة الموجودة في السطر 46.
// المهنة
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour);
قد يتساءل المرء عن مكان وضع هذه الطريقة:
- يمكن وضعها في الطبقة [DAO]. ومع ذلك، فإن هذه الطريقة لا تلبي حقًا حاجة للوصول إلى البيانات بل تلبي حاجة مهنية،
- يمكن وضعها في الطبقة [web]. لكن هذه ستكون فكرة سيئة. لأنه إذا قمنا بتغيير الطبقة [web] إلى طبقة [Swing]، فسوف نفقد هذه الطريقة في حين أن الحاجة إليها لا تزال قائمة.
تستقبل الطريقة كمعلمات اسم الطبيب واليوم الذي نريد جدول الحجوزات الخاص به. وتُرجع كائن [AgendaMedecinJour] الذي يمثل جدول الطبيب واليوم:
package rdvmedecins.metier.entites;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.jpa.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// الحقول
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// المنشئون
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// المُستردات والمُعيّنات
...
}
- السطر 12: الطبيب صاحب الجدول،
- السطر 13: يوم الجدول،
- السطر 14: المواعيد المتاحة للطبيب في هذا اليوم.
- تحتوي الفئة على منشئات (السطر 17، 21) بالإضافة إلى طريقة toString مخصصة (السطر 27).
الفئة [CreneauMedecinJour] (السطر 14) هي كما يلي:
package rdvmedecins.metier.entites;
import java.io.Serializable;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// الحقول
private Creneau creneau;
private Rv rv;
// المنشئون
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau,rv);
}
// الوصول والضبط
...
}
- السطر 12: موعد الطبيب،
- السطر 13: الموعد المرتبط، null إذا كان الموعد متاحًا.
ونرى بذلك أن الحقل creneauxMedecinJour في السطر 14 من الفئة [AgendaMedecinJour] يتيح لنا الحصول على جميع فترات الطبيب مع معلومة "مشغول" أو "متاح" لكل منها. كان هذا هو الهدف من الطريقة الجديدة [getAgendaMedecinJour] للواجهة [IMetier].
سيكون لـ Ejb [Metier] واجهة محلية وواجهة بعيدة تقتصران على اشتقاق الواجهة الرئيسية [IMetier]:
package rdvmedecins.metier.service;
import javax.ejb.Local;
@Local
public interface IMetierLocal extends IMetier{
}
package rdvmedecins.metier.service;
import javax.ejb.Remote;
@Remote
public interface IMetierRemote extends IMetier{
}
يقوم EJB [Metier] بتنفيذ هذه الواجهات بالطريقة التالية:
package rdvmedecins.metier.service;
import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.entites.CreneauMedecinJour;
@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Metier implements IMetierLocal, IMetierRemote, Serializable {
// طبقة DAO
@EJB
private IDaoLocal dao;
public Metier() {
}
@Override
public List<Client> getAllClients() {
return dao.getAllClients();
}
@Override
public List<Medecin> getAllMedecins() {
return dao.getAllMedecins();
}
@Override
public List<Creneau> getAllCreneaux(Medecin medecin) {
return dao.getAllCreneaux(medecin);
}
@Override
public List<Rv> getRvMedecinJour(Medecin medecin, Date jour) {
return dao.getRvMedecinJour(medecin, jour);
}
@Override
public Client getClientById(Long id) {
return dao.getClientById(id);
}
@Override
public Medecin getMedecinById(Long id) {
return dao.getMedecinById(id);
}
@Override
public Rv getRvById(Long id) {
return dao.getRvById(id);
}
@Override
public Creneau getCreneauById(Long id) {
return dao.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return dao.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
dao.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(Medecin medecin, Date jour) {
// قائمة المواعيد المتاحة للطبيب
List<Creneau> creneauxHoraires = dao.getAllCreneaux(medecin);
// قائمة حجوزات هذا الطبيب نفسه لهذا اليوم
List<Rv> reservations = dao.getRvMedecinJour(medecin, jour);
// يتم إنشاء قاموس من المواعيد المحجوزة
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// يتم إنشاء جدول المواعيد لليوم المطلوب
AgendaMedecinJour agenda = new AgendaMedecinJour();
// الطبيب
agenda.setMedecin(medecin);
// اليوم
agenda.setJour(jour);
// فترات الحجز
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// ملء فترات الحجز
for (int i = 0; i < creneauxHoraires.size(); i++) {
// سطر i في الجدول
creneauxMedecinJour[i] = new CreneauMedecinJour();
// معرف الفترة
creneauxMedecinJour[i].setCreneau(creneauxHoraires.get(i));
// هل الفترة متاحة أم محجوزة؟
if (hReservations.containsKey(creneauxHoraires.get(i).getId())) {
// الفترة مشغولة - يتم تسجيل الحجز
Rv resa = hReservations.get(creneauxHoraires.get(i).getId());
creneauxMedecinJour[i].setRv(resa);
}
}
// يتم إرجاع النتيجة
return agenda;
}
}
- السطر 22، الفئة [Metier] هي فئة فردية EJB،
- السطر 23، كل طريقة في EJB تتم داخل معاملة. وهذا يعني أن المعاملة تبدأ في بداية الطريقة، في الطبقة [métier]. وستقوم هذه الطبقة باستدعاء طرق من الطبقة [DAO]. وستتم هذه الطرق ضمن نفس المعاملة،
- السطر 24، حيث تقوم EJB بتنفيذ واجهاتها المحلية والبعيدة، كما أنها قابلة للتسلسل،
- السطر 27: مرجع إلى EJB من الطبقة [DAO]،
- السطر 29: سيتم إدخال هذا بواسطة الحاوية EJB لخادم Glassfish، بفضل التعليق التوضيحي @EJB. لذلك عندما يتم تنفيذ طرق الفئة [Metier]، تكون الإشارة إلى EJB من الطبقة [DAO] قد تم تهيئتها،
- الأسطر 33-81: تُستخدم هذه الإشارة لتفويض الطبقة [DAO] بالاستجابة للنداء الموجه إلى الطبقة [métier]،
- السطر 84: الطريقة getAgendaMedecinJour التي تسمح بالحصول على جدول مواعيد الطبيب ليوم معين. نترك للقارئ متابعة التعليقات.
3.5.2. نشر الطبقة [métier]
الطبقة [métier] تعتمد على الطبقة [DAO]. تم تنفيذ كل طبقة باستخدام EJB. لاختبار EJB و [métier]، يتعين علينا نشر كلا الطبقتين EJB. ولهذا الغرض، نحتاج إلى مشروع مؤسسي.
![]() |
- [1]، نقوم بإنشاء مشروع جديد،
- من نوع Maven [2] وتطبيق مؤسسي [3]،
- ونعطيه اسم [4]. سيتم إضافة اللاحقة ear تلقائيًا،
![]() |
- إلى [5]، نختار خادم Glassfish و Java EE 6،
- إلى [6]، يحتوي تطبيق المؤسسة على وحدات، وعادةً ما تكون وحدات EJB ووحدات ويب. هنا، سيحتوي تطبيق المؤسسة على وحدات النمطية الخاصة بـ EJB اللتين قمنا بإنشائهما. وبما أن هذه الوحدات موجودة، فلا نضع علامة في المربعات،
- في [7,8]، تم إنشاء مشروعين. [8] هو مشروع المؤسسة الذي سنستخدمه. [7] هو مشروع لا أعرف دوره. لم أضطر إلى استخدامه، ولأنني لم أتعمق في Maven، لا أعرف ما فائدته. لذا سنتجاهله.
الآن بعد إنشاء مشروع الشركة، يمكننا تحديد وحداته.
![]() |
- في [1]، نقوم بإنشاء تبعية جديدة،
- في [2]، نختار مشروع EJB [DAO]،
- في [3]، نعلن أنه EJB. لا تترك النوع فارغًا لأنه في هذه الحالة سيتم استخدام النوع jar وهذا النوع غير مناسب هنا،
- في [4]، يتم استخدام النطاق [compile]،
- في [5]، المشروع مع تبعيته الجديدة،
![]() |
- في [6, 7, 8]، نبدأ من جديد لإضافة EJB من الطبقة [métier]،
- إلى [9]، التبعيتان،
- في [10]، نقوم ببناء المشروع،
![]() |
- في [11]، يتم تنفيذه،
- في [12]، في علامة التبويب [Services]، نرى أن المشروع قد تم نشره على خادم Glassfish. وهذا يعني أن كلا الإي جي بي موجودان الآن على الخادم.
في سجلات خادم Glassfish، نجد معلومات حول نشر كلا EJB:
![]() |
- في [1]، علامة تبويب سجلات Glassfish.
ونجد فيها السجلات التالية:
- الأسطر 1-5: تم التعرف على كيانات JPA،
- السطر 7: يشير إلى نجاح إنشاء وحدة الاستمرارية [dbrdvmedecins2-PU] وإجراء الاتصال بقاعدة البيانات المرتبطة بها،
- السطر 8: الأسماء القابلة للنقل للواجهات البعيدة والمحلية لـ EJB [DaoJpa]. portable تعني أنها معترف بها من قبل جميع خوادم التطبيقات،
- السطر 9: نفس الشيء ولكن بأسماء خاصة بـ Glassfish،
- السطران 10-11: نفس الشيء بالنسبة لـ EJB و [Metier].
سنحتفظ بالاسم القابل للنقل للواجهة البعيدة لـ EJB [Metier]:
java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote
سنحتاج إليه أثناء اختبار الطبقة [métier].
3.5.3. اختبار الطبقة [métier]
كما فعلنا مع الطبقة [DAO]، سنقوم باختبار الطبقة [métier] في إطار تطبيق عميل/خادم:
![]() |
سيقوم العميل باختبار الواجهة البعيدة لـ EJB [Metier] التي تم نشرها على خادم Glassfish.
نبدأ بإنشاء مشروع Maven جديد. للقيام بذلك، نتبع الخطوات المستخدمة لإنشاء مشروع اختبار الطبقة [dao] (انظر الفقرة 3.4.7)، باستثناء إنشاء الاختبار JUnit. المشروع الذي تم إنشاؤه على هذا النحو هو التالي
![]() |
- في [1]، المشروع الذي تم إنشاؤه مع تبعياته: فيما يتعلق بـ EJB من الطبقة [dao]، وبـ EJB من الطبقة [métier]، من المكتبة [gf-client].
في هذه المرحلة، يكون ملف [pom.xml] الخاص بالمشروع كما يلي:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>mv-client-rdvmedecins-ejb-metier</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mv-client-rdvmedecins-ejb-metier</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.glassfish.appclient</groupId>
<artifactId>gf-client</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mv-rdvmedecins-ejb-metier</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
تأكد من وجود التبعيات الموضحة في الأسطر 17-33. سيكون الاختبار عبارة عن فئة وحدة تحكم بسيطة:
![]() |
رمز فئة [ClientRdvMedecinsMetier] هو كما يلي:
package istia.st.client;
import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.jpa.Rv;
import rdvmedecins.metier.entites.AgendaMedecinJour;
import rdvmedecins.metier.service.IMetierRemote;
public class ClientRdvMedecinsMetier {
// اسم الواجهة البعيدة لـ EJB [Metier]
private static String IDaoRemoteName = "java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
// تاريخ اليوم
private static Date jour = new Date();
public static void main(String[] args) {
try {
// سياق JNDI لخادم Glassfish
InitialContext initialContext = new InitialContext();
// مرجع على الطبقة البعيدة [metier]
IMetierRemote metier = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
// عرض العملاء
List<Client> clients = metier.getAllClients();
display("Liste des clients :", clients);
// عرض الأطباء
List<Medecin> medecins = metier.getAllMedecins();
display("Liste des médecins :", medecins);
// عرض المواعيد المتاحة للطبيب
Medecin medecin = medecins.get(0);
List<Creneau> creneaux = metier.getAllCreneaux(medecin);
display(String.format("Liste des créneaux du médecin %s", medecin), creneaux);
// قائمة مواعيد الطبيب في يوم معين
display(String.format("Liste des rendez-vous du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// عرض الأجندة
AgendaMedecinJour agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// إضافة RV
Rv rv = null;
Creneau creneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, creneau, client));
rv = metier.ajouterRv(jour, creneau, client);
System.out.println("Rv ajouté");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// عرض الأجندة
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
// حذف RV
System.out.println("Suppression du Rv ajouté");
metier.supprimerRv(rv);
System.out.println("Rv supprimé");
display(String.format("Liste des Rv du médecin %s, le [%s]", medecin, jour), metier.getRvMedecinJour(medecin, jour));
// عرض التقويم
agenda = metier.getAgendaMedecinJour(medecin, jour);
System.out.println(agenda);
} catch (Throwable ex) {
System.out.println("Erreur...");
while (ex != null) {
System.out.println(String.format("%s : %s", ex.getClass().getName(), ex.getMessage()));
ex = ex.getCause();
}
}
}
// طريقة مساعدة - تعرض عناصر مجموعة
private static void display(String message, List elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- السطر 18: تم أخذ الاسم القابل للنقل للواجهة البعيدة لـ Ejb [Metier] من سجلات Glassfish،
- الأسطر 24-27: يتم الحصول على مرجع للواجهة البعيدة لـ Ejb [Metier]،
- السطران 29-30: يعرضان العملاء،
- الأسطر 32-33: تعرض الأطباء،
- الأسطر 35-37: تعرض المواعيد المتاحة للطبيب،
- السطر 39: يعرض مواعيد الطبيب في يوم معين،
- السطران 41-42: جدول مواعيد هذا الطبيب نفسه في اليوم نفسه،
- السطور 44-49: إضافة موعد،
- السطر 50: عرض مواعيد الطبيب. يجب أن يكون هناك موعد إضافي،
- السطران 52-53: عرض جدول مواعيد الطبيب. يجب أن يظهر الموعد الذي تمت إضافته،
- السطور 55-57: حذف الموعد الذي تمت إضافته للتو،
- السطر 58: يجب أن ينعكس ذلك في قائمة مواعيد الطبيب،
- السطران 60-61: وفي جدول مواعيده.
نقوم بتنفيذ الاختبار:
![]() | ![]() |
: شاشات العرض التي تم الحصول عليها هي التالية:
Liste des clients :
Client[1,Mr,Jules,MARTIN]
Client[2,Mme,Christine,GERMAN]
Client[3,Mr,Jules,JACQUARD]
Client[4,Melle,Brigitte,BISTROU]
Liste des médecins :
Médecin[1,Mme,Marie,PELISSIER]
Médecin[2,Mr,Jacques,BROMARD]
Médecin[3,Mr,Philippe,JANDOT]
Médecin[4,Melle,Justine,JACQUEMOT]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER]
Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]]
Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]]
Liste des créneaux du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Ajout d'un Rv le [Wed May 23 16:25:26 CEST 2012] dans le créneau Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] pour le client Client[1,Mr,Jules,MARTIN]
Rv ajouté
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] Rv[252, Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
Suppression du Rv ajouté
Rv supprimé
Liste des Rv du médecin Médecin[1,Mme,Marie,PELISSIER], le [Wed May 23 16:25:26 CEST 2012]
Agenda[Médecin[1,Mme,Marie,PELISSIER],23/05/2012, [Creneau [1, 1, 8:0, 8:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [2, 1, 8:20, 8:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [3, 1, 8:40, 9:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [4, 1, 9:0, 9:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [5, 1, 9:20, 9:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [6, 1, 9:40, 10:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [7, 1, 10:0, 10:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [8, 1, 10:20, 10:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [9, 1, 10:40, 11:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [10, 1, 11:0, 11:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [11, 1, 11:20, 11:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [12, 1, 11:40, 12:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [13, 1, 14:0, 14:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [14, 1, 14:20, 14:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [15, 1, 14:40, 15:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [16, 1, 15:0, 15:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [17, 1, 15:20, 15:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [18, 1, 15:40, 16:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [19, 1, 16:0, 16:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [20, 1, 16:20, 16:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [21, 1, 16:40, 17:0,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [22, 1, 17:0, 17:20,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [23, 1, 17:20, 17:40,Médecin[1,Mme,Marie,PELISSIER]] null] [Creneau [24, 1, 17:40, 18:0,Médecin[1,Mme,Marie,PELISSIER]] null]]
- السطر 37: جدول أعمال السيدة PELISSIER، في 23 مايو 2012. لا توجد أي فترات محجوزة،
- السطر 39: إضافة موعد،
- السطر 42: التقويم الجديد للسيدة PELISSIER. تم حجز موعد الآن للسيد MARTIN،
- السطر 44: تم حذف الموعد،
- السطر 46: يُظهر جدول مواعيد السيدة PELISSIER أنه لا توجد أي فترات زمنية محجوزة.
نعتبر الآن أن الطبقتين [DAO] و [métier] جاهزتان للعمل. يبقى لنا كتابة الطبقة [web] باستخدام إطار العمل JSF. للقيام بذلك، سنستخدم المعرفة المكتسبة في بداية هذا المستند.
3.6. الطبقة [web]
لنعد إلى البنية قيد الإنشاء:
![]() |
سنقوم ببناء الطبقة الأخيرة، وهي طبقة [web].
3.6.1. مشروع Netbeans
نقوم ببناء مشروع Maven:
![]() |
- في [1]، نقوم بإنشاء مشروع جديد،
- في [2, 3]، مشروع Maven من النوع [Web Application]،
- في [4]، نسميه،
![]() |
- في [5]، نختار خادم Glassfish و Java EE 6 Web،
- في [6]، المشروع الذي تم إنشاؤه،
- في [7]، المشروع بعد حذف الصفحة [index.jsp] والحزمة الموجودة في [Source Packages]،
![]() |
- إلى [8, 9]، في خصائص المشروع، نضيف إطار عمل،
- في [10]، نختار Java Server Faces،
![]() |
- في [11]، تكوين Java Server Faces. نترك القيم الافتراضية. نلاحظ أن JSF 2 هو المستخدم،
- في [12]، يتم تعديل المشروع في نقطتين: يتم إنشاء ملف [web.xml] بالإضافة إلى صفحة [index.html].
الملف [web.xml] هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
لقد سبق أن تعرفنا على هذا الملف.
- الأسطر 7-11: تحدد السيرفلت الذي سيعالج جميع الطلبات الموجهة إلى التطبيق. إنه سيرفلت JSF،
- الأسطر 12-15: تحدد URL التي تعالجها هذه الخدمة. وهي URL بالشكل /faces/*,
- الأسطر 21-23: تحدد الصفحة [index.xhtml] كصفحة رئيسية.
هذه الصفحة هي التالية:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
Hello from Facelets
</h:body>
</html>
لقد سبق أن صادفناها. يمكننا تنفيذ هذا المشروع:
![]() |
- في [1]، نقوم بتنفيذ المشروع ونحصل على النتيجة [2] في المتصفح.
نقدم الآن المشروع الكامل لنفصل بعد ذلك العناصر المختلفة.
![]() |
- في [1]، صفحات XHTML للمشروع،
- في [2]، أكواد Java،
- في [3]، ملفات الرسائل لأن التطبيق متعدد اللغات،
![]() |
- إلى [4]، تبعيات المشروع.
3.6.2. تبعيات المشروع
لنعد إلى بنية المشروع:
![]() |
تعتمد الطبقة JSF على الطبقات [métier] و [DAO] و [JPA]. هذه الطبقات الثلاث مدمجة في مشروعي Maven اللذين قمنا بإنشائهما، وهو ما يفسر تبعيات مشروع [4]. دعونا نوضح ببساطة كيفية إضافة هذه التبعيات:
![]() |
- في [1]، سنضع ejb للإشارة إلى أن التبعية موجودة في مشروع EJB،
- في [2]، سنضع [provided]. في الواقع، سيتم نشر مشروع الويب في نفس الوقت مع مشروعي EJB. لذلك لا يحتاج إلى تضمين ملفات jar الخاصة بـ EJB.
3.6.3. تكوين المشروع
تكوين المشروع هو نفس تكوين مشاريع JSF التي درسناها في بداية هذا المستند. ندرج ملفات التكوين دون إعادة شرحها.
![]() | ![]() |
[web.xml]: تكوين تطبيق الويب.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
<error-page>
<error-code>500</error-code>
<location>/faces/exception.xhtml</location>
</error-page>
<error-page>
<exception-type>Exception</exception-type>
<location>/faces/exception.xhtml</location>
</error-page>
</web-app>
يُلاحظ في السطر 26 أن الصفحة [index.xhtml] هي الصفحة الرئيسية للتطبيق.
[faces-config.xml]: تكوين التطبيق JSF
<?xml version='1.0' encoding='UTF-8'?>
<!-- =========== FULL CONFIGURATION FILE ================================== -->
<faces-config version="2.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
<application>
<resource-bundle>
<base-name>
messages
</base-name>
<var>msg</var>
</resource-bundle>
<message-bundle>messages</message-bundle>
</application>
</faces-config>
[beans.xml]: فارغة ولكنها ضرورية للتعليق @Named
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
[styles.css]: ورقة أنماط التطبيق
.reservationsHeaders {
text-align: center;
font-style: italic;
color: Snow;
background: Teal;
}
.creneau {
height: 25px;
text-align: center;
background: MediumTurquoise;
}
.client {
text-align: left;
background: PowderBlue;
}
.action {
width: 6em;
text-align: left;
color: Black;
background: MediumTurquoise;
}
.erreursHeaders {
background: Teal;
background-color: #ff6633;
color: Snow;
font-style: italic;
text-align: center
}
.erreurClasse {
background: MediumTurquoise;
background-color: #ffcc66;
height: 25px;
text-align: center
}
.erreurMessage {
background: PowderBlue;
background-color: #ffcc99;
text-align: left
}
[messages_fr.properties]: ملف الرسائل باللغة الفرنسية
# التخطيط
layout.entete=Les M\u00e9decins Associ\u00e9s
layout.basdepage=ISTIA, universit\u00e9 d'Angers
layout.entete.langue1=Fran\u00e7ais
layout.entete.langue2=Anglais
# استثناء
exception.header=L'exception suivante s'est produite
exception.httpCode=Code HTTP de l'erreur
exception.message=Message de l'exception
exception.requestUri=Url demand\u00e9e lors de l'erreur
exception.servletName=Nom de la servlet demand\u00e9e lorsque l'erreur s'est produite
# نموذج 1
form1.titre=R\u00e9servations
form1.medecin=M\u00e9decin
form1.jour=Jour (jj/mm/aaaa)
form1.button.agenda=Agenda
form1.jour.required=date requise
form1.jour.erreur=date erron\u00e9e
# نموذج 2
form2.titre=Agenda de {0} {1} {2} le {3}
form2.titre_detail=Agenda de {0} {1} {2} le {3}
form2.creneauHoraire=Cr\u00e9neau horaire
form2.client=Client
form2.accueil=Accueil
form2.supprimer=Supprimer
form2.reserver=R\u00e9server
# النموذج 3
form3.titre=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.titre_detail=Prise de rendez-vous de {0} {1} {2}, le {3} dans le cr\u00e9neau {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.client=Client
form3.valider=Valider
form3.annuler=Annuler
# خطأ
erreur.titre=Une erreur s'est produite.
erreur.message=Message d'erreur
erreur.accueil=Page d'accueil
erreur.classe=Cause
[messages_en.properties]: ملف الرسائل باللغة الإنجليزية
# التخطيط
layout.entete=Associated Doctors
layout.basdepage=ISTIA, Angers university
layout.entete.langue1=French
layout.entete.langue2=English
# استثناء
exception.header=The following exceptions occurred
exception.httpCode=Error HTTP code
exception.message=Exception message
exception.requestUri=Url targeted when error occurred
exception.servletName=Servlet targeted's name when error occurred
# نموذج 1
form1.titre=Reservations
form1.medecin=Doctor
form1.jour=Date (dd/mm/yyyy)
form1.button.agenda=Diary
form1.jour.required=The date is required
form1.jour.erreur=The date is invalid
# نموذج 2
form2.titre={0} {1} {2}'' diary on {3}
form2.titre_detail={0} {1} {2}'' diary on {3}
form2.creneauHoraire=Time Period
form2.client=Client
form2.accueil=Welcome Page
form2.supprimer=Delete
form2.reserver=Reserve
# نموذج 3
form3.titre=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.titre_detail=Reservation for {0} {1} {2}, on {3} in the time period {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.client=Client
form3.valider=Submit
form3.annuler=Cancel
# خطأ
erreur.titre=An error occurred
erreur.message=Error message
erreur.accueil=Welcome Page
erreur.classe=Cause
3.6.4. طرق عرض المشروع
دعونا نستعرض طريقة عمل التطبيق. الصفحة الرئيسية هي كما يلي:
![]() |
من هذه الصفحة الأولى، سيقوم المستخدم (الأمانة، الطبيب) بعدد من الإجراءات. نعرضها أدناه. تعرض الواجهة اليسرى الواجهة التي يقوم المستخدم من خلالها بتقديم طلب، بينما تعرض الواجهة اليمنى الرد المرسل من الخادم.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
أخيرًا، يمكن أيضًا الحصول على صفحة أخطاء:
![]() |
يتم الحصول على هذه العروض المختلفة من خلال الصفحات التالية من مشروع الويب:
![]() |
- في [1]، تضمن الصفحات [basdepage, entete, layout] تنسيق جميع العروض،
- في [2]، العرض الذي تم إنتاجه بواسطة [layout.xhtml].
تم استخدام تقنية facelets هنا. وقد تم وصفها في الفقرة 2.11. نكتفي هنا بإعطاء كود الصفحات XHTML المستخدمة لتنسيق الصفحة:
[entete.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['layout.entete']}"/></h2>
<div align="left">
<h:commandLink value="#{msg['layout.entete.langue1']}" actionListener="#{changeLocale.setFrenchLocale}"/>
<h:outputText value=" "/>
<h:commandLink value="#{msg['layout.entete.langue2']}" actionListener="#{changeLocale.setEnglishLocale}"/>
</div>
</body>
</html>
يُلاحظ في الأسطر 10-12 وجود رابطين لتغيير لغة التطبيق.
[basdepage.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<body>
<h:outputText value="#{msg['layout.basdepage']}"/>
</body>
</html>
[layout.xhtml]
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<f:view locale="#{changeLocale.locale}">
<h:head>
<title>RdvMedecins</title>
<h:outputStylesheet library="css" name="styles.css"/>
</h:head>
<h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
<h:form id="formulaire">
<table style="width: 1200px">
<tr>
<td colspan="2" bgcolor="#ccccff">
<ui:include src="entete.xhtml"/>
</td>
</tr>
<tr>
<td style="width: 100px; height: 200px" bgcolor="#ffcccc">
</td>
<td>
<ui:insert name="contenu" >
<h2>Contenu</h2>
</ui:insert>
</td>
</tr>
<tr bgcolor="#ffcc66">
<td colspan="2">
<ui:include src="basdepage.xhtml"/>
</td>
</tr>
</table>
</h:form>
</h:body>
</f:view>
</html>
هذه الصفحة هي نموذج (قالب) الصفحة [index.xhtml]:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="layout.xhtml">
<ui:define name="contenu">
<h:panelGroup rendered="#{form.form1Rendered}">
<ui:include src="form1.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.form2Rendered}">
<ui:include src="form2.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.form3Rendered}">
<ui:include src="form3.xhtml"/>
</h:panelGroup>
<h:panelGroup rendered="#{form.erreurRendered}">
<ui:include src="erreur.xhtml"/>
</h:panelGroup>
</ui:define>
</ui:composition>
</html>
تحدد الأسطر 8-21 المنطقة المسماة "المحتوى" (السطر 8) في [layout.xhtml] (السطر 7). وهي المنطقة المركزية للعروض:
![]() |
الصفحة [index.xhtml] هي الصفحة الوحيدة في التطبيق. لذلك لن يكون هناك أي تنقل بين الصفحات. وهي تعرض إحدى الصفحات الأربع [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. يتم التحكم في هذا العرض بواسطة أربعة قيم منطقية [form1Rendered, form2Rendered, form3Rendered, erreurRendered] من bean form التي سنصفها لاحقًا.
3.6.5. فولز المشروع
![]() |
تمت بالفعل عرض فئات حزمة [utils]:
- الفئة [ChangeLocale] هي الفئة التي تضمن تغيير اللغة. وقد تمت دراستها بالفعل (الفقرة 2.4.4).
- الفئة [Messages] هي فئة تسهل تدويل رسائل التطبيق. وقد تمت دراستها في الفقرة 2.8.5.7.
3.6.5.1. البيان Application
البيان [Application] هو التالي:
package beans;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Medecin;
import rdvmedecins.metier.service.IMetierLocal;
@Named(value = "application")
@ApplicationScoped
public class Application implements Serializable{
// طبقة الأعمال
@EJB
private IMetierLocal metier;
// ذاكرة التخزين المؤقت
private List<Medecin> medecins;
private List<Client> clients;
private Map<Long, Medecin> hMedecins = new HashMap<Long, Medecin>();
private Map<Long, Client> hClients = new HashMap<Long, Client>();
// أخطاء
private List<Erreur> erreurs = new ArrayList<Erreur>();
private Boolean erreur = false;
public Application() {
}
@PostConstruct
public void init() {
// يتم وضع الأطباء والعملاء في ذاكرة التخزين المؤقت
try {
medecins = metier.getAllMedecins();
clients = metier.getAllClients();
} catch (Throwable th) {
// تسجيل الخطأ
erreur = true;
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
while (th.getCause() != null) {
th = th.getCause();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
}
return;
}
// التحقق من القوائم
if (medecins.size() == 0) {
// تدوين الخطأ
erreur = true;
erreurs.add(new Erreur("", "La liste des médecins est vide"));
}
if (clients.size() == 0) {
// تدوين الخطأ
erreur = true;
erreurs.add(new Erreur("", "La liste des clients est vide"));
}
// خطأ؟
if (erreur) {
return;
}
// القواميس
for (Medecin m : medecins) {
hMedecins.put(m.getId(), m);
}
for (Client c : clients) {
hClients.put(c.getId(), c);
}
}
// مُستردات ومُعيّنات
...
}
- السطران 15-16: الفئة [Application] هي bean ذات نطاق تطبيق. يتم إنشاؤها مرة واحدة في بداية دورة حياة التطبيق JSF ويمكن الوصول إليها من قبل جميع الطلبات لجميع المستخدمين. عادةً ما يتم وضع البيانات القابلة للقراءة فقط فيها. هنا، سنضع قائمة الأطباء وقائمة العملاء. لذا نفترض أن هاتين القائمتين لا تتغيران كثيرًا. يمكن للصفحات XHTML الوصول إليها عبر اسم التطبيق،
- السطران 20-21: سيتم إدخال مرجع على الواجهة المحلية لـ EJB [Metier] بواسطة حاوية EJB من Glassfish. لنتذكر بنية التطبيق:
![]() |
سيتم تنفيذ التطبيق JSF و EJB و [Metier] في نفس JVM (Java Virtual Machine). لذلك ستستخدم الطبقة [JSF] الواجهة المحلية لـ EJB. هنا، يستخدم bean التطبيق EJB و [Metier]. وحتى لو لم يكن الأمر كذلك، فمن الطبيعي أن نجد مرجعًا في الطبقة [métier]. فهذه معلومة يمكن مشاركتها بين جميع طلبات جميع المستخدمين، وبالتالي فهي بيانات ذات نطاق Application.
- السطور 34-35: يتم تنفيذ الطريقة init مباشرة بعد إنشاء مثيل للفئة [Application] (وجود التعليق التوضيحي @PostConstruct)،
- السطور 36-73، تقوم الطريقة بإنشاء العناصر التالية: قائمة الأطباء في السطر 23، وقائمة العملاء في السطر 24، وقاموس للأطباء مفهرس حسب معرفهم في السطر 25، ونفس الشيء بالنسبة للعملاء في السطر 26. قد تحدث أخطاء. يتم تسجيل هذه الأخطاء في القائمة الموجودة في السطر 28.
الفئة [Erreur] هي كما يلي:
package beans;
public class Erreur {
public Erreur() {
}
// الحقل
private String classe;
private String message;
// المنشئ
public Erreur(String classe, String message){
this.setClasse(classe);
this.message=message;
}
// الوصول والضبط
...
}
- السطر 9، اسم فئة الاستثناء إذا تم إطلاق استثناء،
- السطر 10: رسالة خطأ.
3.6.5.2. البيان [Form]
ورمزه هو كما يلي:
package beans;
...
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
public Form() {
}
// فئة التطبيق
@Inject
private Application application;
// نموذج
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneau;
private Medecin medecin;
private Client client;
private Long idClient;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
@PostConstruct
private void init() {
// هل تمت عملية التهيئة بنجاح؟
if (application.getErreur()) {
// استرداد قائمة الأخطاء
erreurs = application.getErreurs();
// يتم عرض قائمة الأخطاء
setForms(false, false, false, true);
}
}
// عرض
private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean erreurRendered) {
this.form1Rendered = form1Rendered;
this.form2Rendered = form2Rendered;
this.form3Rendered = form3Rendered;
this.erreurRendered = erreurRendered;
}
.................................................
}
- الأسطر 5-7: الفئة [Form] هي bean باسم form ونطاق session. نذكر أن الفئة يجب أن تكون قابلة للتسلسل.
- الأسطر 13-14: يحتوي bean form على مرجع إلى bean application. سيتم حقن هذا المرجع بواسطة حاوية servlets التي يتم فيها تشغيل التطبيق (وجود التعليق التوضيحي @Inject).
- الأسطر 17-31: نموذج الصفحات [form1.xhtml, form2.xhtml, form3.xhtml, erreur.xhtml]. يتم التحكم في عرض هذه الصفحات بواسطة القيم المنطقية في الأسطر 19-22. تجدر الإشارة إلى أنه بشكل افتراضي، يتم عرض الصفحة [form1.xhtml]،
- السطور 33-34: يتم تنفيذ الطريقة init مباشرة بعد إنشاء مثيل للفئة (وجود التعليق التوضيحي @PostConstruct)،
- الأسطر 35-41: يتم استخدام الطريقة init لمعرفة الصفحة التي يجب عرضها أولاً: عادةً ما تكون الصفحة [form1.xhtml] (السطر 19) إلا إذا حدث خطأ في تهيئة التطبيق (السطر 36)، وفي هذه الحالة سيتم عرض الصفحة [erreur.xhtml] (السطر 40).
الصفحة [erreur.xhtml] هي التالية:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['erreur.titre']}"/></h2>
<p>
<h:commandButton value="#{msg['erreur.accueil']}" actionListener="#{form.accueil()}"/>
</p>
<hr/>
<h:dataTable value="#{form.erreurs}" var="erreur" headerClass="erreursHeaders" columnClasses="erreurClasse,erreurMessage">
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.classe']}"/>
</f:facet>
<h:outputText value="#{erreur.classe}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['erreur.message']}"/>
</f:facet>
<h:outputText value="#{erreur.message}"/>
</h:column>
</h:dataTable>
</body>
</html>
وهي تستخدم علامة <h:dataTable> (الأسطر 14-27) لعرض قائمة الأخطاء. وهذا يعطي صفحة مشابهة للصفحة التالية:

سنقوم الآن بتحديد المراحل المختلفة لدورة حياة التطبيق.
3.6.6. التفاعلات بين الصفحات والنموذج
3.6.6.1. عرض الصفحة الرئيسية
إذا سارت الأمور على ما يرام، فإن أول صفحة يتم عرضها هي [form1.xhtml]. وهذا يعطي العرض التالي:
![]() |
الصفحة [form1.xhtml] هي التالية:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{msg['form1.titre']}"/></h2>
<h:panelGrid columns="3">
<h:panelGroup>
<div align="center"><h3><h:outputText value="#{msg['form1.medecin']}"/></h3></div>
</h:panelGroup>
<h:panelGroup>
<div align="center"><h3><h:outputText value="#{msg['form1.jour']}"/></h3></div>
</h:panelGroup>
<h:panelGroup/>
<h:selectOneMenu value="#{form.idMedecin}">
<f:selectItems value="#{form.medecins}" var="medecin" itemLabel="#{medecin.titre} #{medecin.prenom} #{medecin.nom}" itemValue="#{medecin.id}"/>
</h:selectOneMenu>
<h:inputText id="jour" value="#{form.jour}" required="true" requiredMessage="#{msg['form1.jour.required']}" converterMessage="#{msg['form1.jour.erreur']}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:inputText>
<h:message for="jour" styleClass="error"/>
</h:panelGrid>
<h:commandButton value="#{msg['form1.button.agenda']}" actionListener="#{form.getAgenda}"/>
</body>
</html>
يتم تشغيل هذه الصفحة بواسطة النموذج التالي:
@Named(value = "form")
@SessionScoped
public class Form implements Serializable {
// فئة التطبيق
@Inject
private Application application;
// القالب
private Long idMedecin;
private Date jour = new Date();
// قائمة الأطباء
public List<Medecin> getMedecins() {
return application.getMedecins();
}
// التقويم
public void getAgenda() {
...
}
- يقوم حقل السطر 9 بتغذية قيمة قائمة السطر 18 من الصفحة للقراءة والكتابة. عند العرض الأولي للصفحة، تحدد القيمة المحددة في القائمة المنسدلة. عند العرض الأولي، idMedecin يساوي null، وبالتالي سيتم تحديد الطبيب الأول،
- تقوم الطريقة في الأسطر 13-15 بإنشاء عناصر قائمة الأطباء المنسدلة (السطر 19 من الصفحة). سيكون لكل خيار تم إنشاؤه تسمية (itemLabel) تتضمن اللقب واسم العائلة والاسم الأول للطبيب، وقيمة (itemValue) تمثل معرف الطبيب،
- ويغذي حقل السطر 10 حقل الإدخال في السطر 21 من الصفحة بالقراءة/الكتابة. عند العرض الأولي، يتم عرض تاريخ اليوم،
- السطور 17-19: تدير الطريقة getAgenda النقر على الزر [Agenda] في السطر 26 من الصفحة. نظرًا لعدم وجود تنقل (يتم طلب الصفحة [index.html] دائمًا)، فغالبًا ما يتم استخدام السمة actionListener بدلاً من السمة action. في هذه الحالة، لا تعرض الطريقة التي يتم استدعاؤها في النموذج أي نتيجة.
عند النقر على الزر [Agenda]،
- يتم تسجيل القيم: يتم تسجيل القيمة المحددة في قائمة الأطباء المنسدلة في الحقل idMedecin في النموذج، ويتم تسجيل اليوم المحدد في حقل «اليوم»،
- يتم استدعاء الطريقة getAgenda في النموذج.
الطريقة getAgenda هي كما يلي:
// تطبيق bean
@Inject
private Application application;
// نموذج
private Long idMedecin;
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
private Medecin medecin;
private List<Erreur> erreurs;
// جدول المواعيد
public void getAgenda() {
try {
// البحث عن الطبيب
medecin = application.gethMedecins().get(idMedecin);
// عنوان النموذج 2
form2Titre = Messages.getMessage(null, "form2.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour)}).getSummary();
// جدول مواعيد الطبيب ليوم معين
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// عرض النموذج 2
setForms(false, true, false, false);
} catch (Throwable th) {
// عرض الأخطاء
prepareVueErreur(th);
}
}
// التحضير vueErreur
private void prepareVueErreur(Throwable th) {
// يتم إنشاء قائمة الأخطاء
erreurs = new ArrayList<Erreur>();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
while (th.getCause() != null) {
th = th.getCause();
erreurs.add(new Erreur(th.getClass().getName(), th.getMessage()));
}
// يتم عرض عرض الأخطاء
setForms(false, false, false, true);
}
لنتذكر ما يجب أن تعرضه الطريقة getAgenda:
![]() |
- السطر 21: يتم استرداد الطبيب المحدد من قاموس الأطباء الذي تم تخزينه في bean application. ويستخدم لهذا الغرض معرّفه الذي تم إرساله في idMedecin،
- السطر 23: يتم إعداد عنوان الصفحة [form2.xhtml] التي سيتم عرضها. يتم أخذ هذه الرسالة من ملف الرسائل حتى يتم ترجمتها. تم وصف هذه التقنية في الفقرة 2.8.5.7، الصفحة 135.
- السطر 25: يتم استدعاء الطبقة [métier] لحساب جدول مواعيد الطبيب المختار لليوم المحدد،
- السطر 27: يتم عرض [form2.xhtml]،
- السطر 28: في حالة وجود استثناء، يتم إنشاء قائمة بالأخطاء (الأسطر 37-42) ويتم عرض الصفحة [erreur.xhtml] (السطر 44).
3.6.6.2. عرض جدول مواعيد الطبيب
تتوافق الصفحة [form2.xhtml] مع العرض التالي:
![]() |
رمز الصفحة [form2.xhtml] هو التالي:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jsp/jstl/core">
<body>
<h2><h:outputText value="#{form.form2Titre}"/></h2>
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
<h:dataTable value="#{form.agendaMedecinJour.creneauxMedecinJour}" var="creneauMedecinJour" headerClass="reservationsHeaders" columnClasses="creneau,client,action">
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['form2.creneauHoraire']}"/>
</f:facet>
<h:outputText value="#{creneauMedecinJour.creneau.hdebut}:#{creneauMedecinJour.creneau.mdebut} - #{creneauMedecinJour.creneau.hfin}:#{creneauMedecinJour.creneau.mfin}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{msg['form2.client']}"/>
</f:facet>
<c:if test="#{creneauMedecinJour.rv==null}">
<h:outputText value=""/>
<c:otherwise>
<h:outputText value="#{creneauMedecinJour.rv.client.titre} #{creneauMedecinJour.rv.client.prenom} #{creneauMedecinJour.rv.client.nom}"/>
</c:otherwise>
</c:if>
</h:column>
<h:column>
<f:facet name="header"/>
<h:commandLink action="#{form.action()}" value="#{creneauMedecinJour.rv==null ? msg['form2.reserver'] : msg['form2.supprimer']}">
<f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneau}"/>
</h:commandLink>
</h:column>
</h:dataTable>
</body>
</html>
نتذكر أن الطريقة getAgenda قامت بتهيئة حقلين في النموذج:
// القالب
private String form2Titre;
private AgendaMedecinJour agendaMedecinJour;
يغذي هذان الحقلان الصفحة [form2.xhtml]:
- السطر 10، عنوان الصفحة،
- السطر 12: يتم عرض جدول مواعيد الطبيب بواسطة علامة <h:dataTable> مكونة من ثلاثة أعمدة،
- الأسطر 13-18: يعرض العمود الأول الفترات الزمنية،
- الأسطر 19-30: يعرض العمود الثاني اسم العميل الذي حجز الفترة الزمنية إن وجدت، أو لا شيء في حالة عدم وجود حجز. لإجراء هذا الاختيار، يتم استخدام علامات مكتبة JSTL Core المشار إليها في السطر 7،
- الأسطر 30-35: يعرض العمود الثالث الرابط [Réserver] إذا كان الموعد متاحًا، والرابط [Supprimer] إذا كان الموعد محجوزًا.
الروابط في العمود الثالث مرتبطة بالنموذج التالي:
// نموذج
private Long idCreneau;
// إجراء على RV
public void action() {
...
}
- يتم استدعاء الطريقة action عندما ينقر المستخدم على رابط الحجز / الحذف (السطر 32). تجدر الإشارة إلى أنه تم استخدام السمة action هنا. يجب أن يكون للطريقة التي تشير إليها هذه السمة التوقيع String action() لأن الطريقة يجب أن تعرض مفتاح تنقل. ولكن هنا، هي void action(). لم يتسبب ذلك في حدوث خطأ ويمكننا أن نفترض أنه في هذه الحالة لا يوجد تنقل. وهذا هو المطلوب. كان وضع actionListener بدلاً من action يتسبب في حدوث خلل،
- سيسترد الحقل idCreneau في السطر 2 معرف الفترة الزمنية للرابط الذي تم النقر عليه (السطر 33 من الصفحة).
3.6.6.3. حذف موعد
دعونا نلقي نظرة على الكود الذي يدير حذف موعد. وهذا يتوافق مع تسلسل المشاهدات التالي:
![]() |
الرمز المعني بهذه العملية هو التالي:
// فاصوليا التطبيق
@Inject
private Application application;
// نموذج
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private AgendaMedecinJour agendaMedecinJour;
private Long idCreneau;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
// إجراء على RV
public void action() {
// البحث عن الموعد في التقويم
int i = 0;
Boolean trouvé = false;
while (!trouvé && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
if (agendaMedecinJour.getCreneauxMedecinJour()[i].getCreneau().getId() == idCreneau) {
trouvé = true;
} else {
i++;
}
}
// هل تم العثور عليه؟
if (!trouvé) {
// هذا غريب - نعيد عرض form2
setForms(false, true, false, false);
return;
}
// تم العثور عليه
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// حسب الإجراء المطلوب
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// حجز
public void reserver() {
...
}
public void supprimer() {
try {
// حذف موعد
application.getMetier().supprimerRv(creneauChoisi.getRv());
// يتم تحديث الأجندة
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// عرض النموذج 2
setForms(false, true, false, false);
} catch (Throwable th) {
// عرض الأخطاء
prepareVueErreur(th);
}
}
- السطر 16: عند بدء تشغيل الطريقة action، تم إرسال معرف الفترة الزمنية المحددة إلى idCreneau (السطر 11)،
- الأسطر 18-26: نحاول استرداد الفترة الزمنية من id (السطر 21). يتم البحث عنه في الأجندة الحالية، agendaMedecinJour في السطر 10. عادةً ما يجب العثور عليه. إذا لم يكن الأمر كذلك، فلا يتم القيام بأي شيء (السطور 28-32)،
- السطر 34: إذا تم العثور على الفترة الزمنية المطلوبة، يتم استرداد مرجع لها وتخزينه في السطر 12،
- السطر 36: نتحقق مما إذا كان الموعد المحدد يحتوي على موعد. إذا كان الأمر كذلك، نقوم بحذفه (السطر 39)، وإلا نقوم بحجز موعد (السطر 37)،
- السطر 51: يتم حذف الموعد من الفترة الزمنية المختارة. الطبقة [métier] هي التي تقوم بهذه المهمة،
- السطر 53: نطلب من الطبقة [métier] جدول مواعيد الطبيب الجديد. سنرى بالطبع موعدًا أقل. ولكن نظرًا لأن التطبيق متعدد المستخدمين، يمكننا رؤية التعديلات التي أجراها مستخدمون آخرون،
- السطر 55: يتم إعادة عرض الصفحة [form2.xhtml]،
- السطر 58: نظرًا لأن الطبقة [métier] قد تم استدعاؤها، فقد تظهر استثناءات. في هذه الحالة، يتم تخزين مكدس الاستثناءات في قائمة الأخطاء في السطر 13 وعرضها باستخدام العرض [erreur.xhtml].
3.6.6.4. حجز موعد
تتم عملية حجز الموعد وفقًا للتسلسل التالي:
![]() |
النموذج المستخدم في هذه العملية هو التالي:
// القالب
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private String form3Titre;
private AgendaMedecinJour agendaMedecinJour;
private Medecin medecin;
private CreneauMedecinJour creneauChoisi;
private List<Erreur> erreurs;
// إجراء على RV
public void action() {
...
// تم العثور على
creneauChoisi = agendaMedecinJour.getCreneauxMedecinJour()[i];
// وفقًا للإجراء المطلوب
if (creneauChoisi.getRv() == null) {
reserver();
} else {
supprimer();
}
}
// الحجز
public void reserver() {
try {
// عنوان النموذج 3
form3Titre = Messages.getMessage(null, "form3.titre", new Object[]{medecin.getTitre(), medecin.getPrenom(), medecin.getNom(), new SimpleDateFormat("dd MMM yyyy").format(jour),
creneauChoisi.getCreneau().getHdebut(), creneauChoisi.getCreneau().getMdebut(), creneauChoisi.getCreneau().getHfin(), creneauChoisi.getCreneau().getMfin()}).getSummary();
// العميل المحدد في القائمة المنسدلة
idClient=null;
// يتم عرض النموذج 3
setForms(false, false, true, false);
} catch (Throwable th) {
// عرض الأخطاء
prepareVueErreur(th);
}
}
- السطر 14: إذا لم يكن هناك موعد في الفترة الزمنية المختارة، فهذا يعني أنه حجز،
- السطر 30: يتم إعداد عنوان الصفحة [form3.xhtml] بنفس التقنية المستخدمة لعنوان الصفحة [form2.xhtml]،
- السطر 34: في هذا النموذج، يوجد قائمة منسدلة يتم تغذية قيمتها بواسطة idClient. نضع قيمة هذا الحقل على null لعدم تحديد أي شخص،
- السطر 36: نعرض الصفحة [form3.xhtml]،
- السطر 39: أو صفحة الأخطاء في حالة حدوث استثناء.
الصفحة [form3.xhtml] هي التالية:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
<h2><h:outputText value="#{form.form3Titre}"/></h2>
<h:panelGrid columns="2">
<h:outputText value="#{msg['form3.client']}"/>
<h:selectOneMenu value="#{form.idClient}">
<f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.titre} #{client.prenom} #{client.nom}" itemValue="#{client.id}"/>
</h:selectOneMenu>
<h:panelGroup>
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
</h:panelGroup>
</h:panelGrid>
</body>
</html>
يتم تغذية هذه الصفحة بالنموذج التالي:
// فاصلة تطبيق
@Inject
private Application application;
// القالب
private Long idClient;
// قائمة العملاء
public List<Client> getClients() {
return application.getClients();
}
- السطر 6: يغذي رقم العميل السمة value في قائمة العملاء المنسدلة في السطر 12 من الصفحة. ويحدد العنصر المحدد في القائمة المنسدلة،
- السطور 9-11: تغذي الطريقة getClients محتوى القائمة المنسدلة (السطر 13). العنوان (itemLabel) لكل خيار هو [Titre Prénom Nom] للعميل، والقيمة المرتبطة (itemValue) هي معرف العميل. وبالتالي، فإن هذه القيمة هي التي سيتم إرسالها.
3.6.6.5. التحقق من صحة موعد
يتوافق التحقق من صحة موعد مع التسلسل التالي:
![]() |
ويعني النقر على الزر [Valider]:
<h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />
وبالتالي، فإن الطريقة [Form].validerRv هي التي ستدير هذا الحدث. ورمزها هو التالي:
// فاصلة تطبيق
@Inject
private Application application;
// نموذج
private Date jour = new Date();
private Boolean form1Rendered = true;
private Boolean form2Rendered = false;
private Boolean form3Rendered = false;
private Boolean erreurRendered = false;
private Long idCreneau;
private Long idClient;
private List<Erreur> erreurs;
// التحقق من صحة الموعد
public void validerRv() {
try {
// استرداد مثيل للفترة الزمنية المختارة
Creneau creneau = application.getMetier().getCreneauById(idCreneau);
// يتم إضافة الموعد
application.getMetier().ajouterRv(jour, creneau, application.gethClients().get(idClient));
// يتم تحديث الأجندة
agendaMedecinJour = application.getMetier().getAgendaMedecinJour(medecin, jour);
// يتم عرض form2
setForms(false, true, false, false);
} catch (Throwable th) {
// عرض الأخطاء
prepareVueErreur(th);
}
}
- السطر 12: قبل تنفيذ الطريقة validerRv، تلقى الحقل idClient معرف العميل الذي اختاره المستخدم،
- السطر 19: انطلاقًا من معرف الفترة الزمنية المخزنة في خطوة سابقة (حيث يكون نطاق الفول هو نطاق الجلسة)، يُطلب من الطبقة [métier] مرجعًا للفترة الزمنية نفسها،
- السطر 21: يُطلب من الطبقة [métier] إضافة موعد لليوم المحدد (اليوم)، والفترة الزمنية المحددة (الفترة)، والعميل المحدد (idClient)،
- السطر 23: يُطلب من الطبقة [métier] تحديث جدول مواعيد الطبيب. سنرى الموعد المضاف بالإضافة إلى جميع التعديلات التي قد يكون قام بها مستخدمون آخرون للتطبيق،
- السطر 25: يتم إعادة عرض جدول المواعيد [form2.xhtml]،
- السطر 28: يتم عرض صفحة الخطأ في حالة حدوث خطأ.
3.6.6.6. إلغاء موعد
وهذا يتوافق مع التسلسل التالي:
![]() |
الزر [Annuler] في الصفحة [form3.xhtml] هو التالي:
<h:commandButton value="#{msg['form3.annuler']}" actionListener="#{form.annulerRv}"/>
وبالتالي يتم استدعاء الطريقة [Form].annulerRv:
// إلغاء الموعد
public void annulerRv() {
// يتم عرض النموذج 2
setForms(false, true, false, false);
}
3.6.6.7. العودة إلى الصفحة الرئيسية
تبقى هناك إجراء واحد يجب الاطلاع عليه، وهو الإجراء التالي:
![]() |
رمز الزر [Accueil] في الصفحة [form2.xhtml] هو التالي:
<h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
طريقة [Form].accueil هي كما يلي:
public void accueil() {
// عرض الصفحة الرئيسية
setForms(true, false, false, false);
}
3.7. الخلاصة
لقد أنشأنا التطبيق التالي:
![]() |
لقد ركزنا على وظائف التطبيق أكثر من مظهره بالنسبة للمستخدم. سيتم تحسين المظهر باستخدام مكتبة المكونات PrimeFaces. لقد قمنا بإنشاء تطبيق بسيط ولكنه يمثل بنية Java EE ذات الطبقات باستخدام EJB. يمكن تحسين التطبيق بعدة طرق:
- يلزم إجراء مصادقة. لا يُسمح للجميع بإضافة/حذف المواعيد،
- وينبغي أن يكون بالإمكان تمرير التقويم للأمام وللخلف عند البحث عن يوم به فترات متاحة،
- يجب أن يكون من الممكن طلب قائمة بالأيام التي توجد فيها فترات متاحة لطبيب معين. في الواقع، إذا كان هذا الطبيب أخصائي عيون، فعادةً ما يتم حجز مواعيده قبل ستة أشهر،
- ...
3.8. الاختبارات باستخدام Eclipse
3.8.1. الطبقة [DAO]
![]() |
- في [1]، يتم استيراد المشروع EJB من الطبقة [DAO] وعميله،
- في [2]، يتم تحديد المشروع EJB من الطبقة [DAO] وتشغيله في [3]،
- في [4]، يتم تنفيذه على خادم،
![]() |
- في [5]، يُقترح خادم Glassfish فقط لأنه الوحيد الذي يحتوي على حاوية EJB،
- في [6]، تم نشر الوحدة النمطية EJB،
![]() |
- في [7]، يتم عرض السجلات:
هذه هي السجلات التي كانت لدينا مع Netbeans.
![]() |
- في [7A] [7B] يتم تنفيذ اختبار JUnit للعميل،
![]() |
- في [8]، نجح الاختبار،
- في [9]، سجلات وحدة التحكم.
![]() |
في [10]، يتم تفريغ التطبيق EJB.
3.8.2. الطبقة [métier]
![]() |
- في [1]، يتم استيراد المشاريع الأربعة لـ Maven من الطبقة [métier]،
- في [2]، يتم تحديد مشروع المؤسسة وتشغيله في [3]، على خادم Glassfish [4] [5]،
![]() |
- في [6]، تم نشر مشروع الشركة على Glassfish،
![]() |
- إلى [7]، وننظر إلى سجلات Glassfish،
السطر 3، نلاحظ الاسم المحمول لـ EJB [Metier] ونلصقه في وحدة التحكم الخاصة بهذا EJB:
public class ClientRdvMedecinsMetier {
// اسم الواجهة البعيدة لـ EJB [Metier]
private static String IDaoRemoteName = "java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
// تاريخ اليوم
private static Date jour = new Date();
![]() |
- في [8]، نقوم بتشغيل عميل وحدة التحكم،
- في [9]، سجلاته.
![]() |
- في [10]، يتم تفريغ تطبيق المؤسسة؛
3.8.3. الطبقة [web]
![]() |
- إلى [1]، يتم استيراد المشاريع الثلاثة لـ Maven من الطبقة [web]. المشروع الذي ينتهي بـ ear هو مشروع المؤسسة الذي يجب نشره على Glassfish،
- في [2]، يتم تنفيذه،
![]() |
- على خادم Glassfish [3]،
- في [4]، تم نشر تطبيق المؤسسة بنجاح،
![]() |
- في [5]، نطلب URL للتطبيق في متصفح Eclipse الداخلي.


























































































































