Skip to content

8. دراسة حالة

8.1. مقدمة

نقترح كتابة تطبيق ويب لجدولة المواعيد في عيادة طبية. تمت معالجة هذه المشكلة في الوثيقة "AngularJS / Spring 4 Tutorial" على الرابط [http://tahe.developpez.com/angularjs-spring4/]. كانت بنية هذا التطبيق كما يلي:

  • في [1]، يقوم خادم الويب بتسليم صفحات ثابتة إلى المتصفح. تحتوي هذه الصفحات على تطبيق AngularJS مبني على نمط MVC (Model–View–Controller). يشمل النموذج هنا كلاً من طرق العرض والمجال، الممثلين هنا بطبقة [Services
  • يتفاعل المستخدم مع العروض المقدمة له في المتصفح. تتطلب إجراءاته أحيانًا الاستعلام عن خادم Spring 4 [2]. سيقوم الخادم بمعالجة الطلب وإرجاع استجابة JSON (ترميز كائنات JavaScript) [3]. ستُستخدم هذه الاستجابة لتحديث العرض المقدم للمستخدم.

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

سيتصل المتصفح بتطبيق [Web 1] الذي تم تنفيذه باستخدام Spring MVC، والذي سيسترد بياناته من خدمة ويب [Web 2] التي تم تنفيذها أيضًا باستخدام Spring MVC.

8.2. ميزات التطبيق

ندعو القراء لاستكشاف ميزات التطبيق من خلال اختباره. نقوم بتحميل مشاريع Maven من مجلد [case-study] إلى STS:

أولاً، سنقوم بإنشاء قاعدة بيانات MySQL 5 [dbrdvmedecins] باستخدام أداة [Wamp Server] (انظر القسم 9.5):

  • في [1]، حدد أداة [phpMyAdmin] من WampServer؛
  • في [2]، حدد خيار [Import
  • في [3]، حدد الملف [database/dbrdvmedecins.sql
  • في [4]، قم بتشغيله؛
  • في [5]، يتم إنشاء قاعدة البيانات.

بعد ذلك، نحتاج إلى تشغيل الخادم المتصل بقاعدة البيانات. هذا هو مشروع [rdvmedecins-webjson-server]

سيكون الخادم متاحًا على عنوان URL [http://localhost:8080]. يمكن تغيير هذا العنوان في ملف [application.properties] الخاص بالمشروع:

  

server.port=8080

يتم تخزين بيانات اعتماد الوصول إلى قاعدة البيانات في فئة [DomainAndPersistenceConfig] التابعة لمشروع [rdvmedecins-metier-dao]:

  

    // the MySQL data source
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        return dataSource;
}

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

بعد ذلك، تمامًا كما في الخادم السابق، نبدأ تشغيل خادم [rdvmedecins-springthymeleaf-server]:

 

هذا الخادم متاح افتراضيًا على عنوان URL [http://localhost:8081]. مرة أخرى، يمكن تكوين هذا في ملف [application.properties] الخاص بالمشروع:


server.port=8081

بالإضافة إلى ذلك، يجب أن يعرف هذا الخادم عنوان URL للخادم المتصل بقاعدة البيانات. يوجد هذا التكوين في فئة [AppConfig] أعلاه:


    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // racine service web / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout en millisecondes
    private final int TIMEOUT = 5000;
    // CORS
private final boolean CORS_ALLOWED=true;

إذا تم تشغيل الخادم الأول على منفذ غير 8080، فيجب تعديل السطر 5.

ثم، باستخدام متصفح، اطلب عنوان URL [http://localhost:8081/boot.html]:

  • في [1]، صفحة تسجيل الدخول إلى التطبيق؛
  • في [2] و[3]، اسم المستخدم وكلمة المرور للمستخدم الذي يرغب في استخدام التطبيق. يوجد مستخدمان: admin/admin (اسم المستخدم/كلمة المرور) الذي يحمل الدور (ADMIN) و user/user الذي يحمل الدور (USER). لا يمتلك سوى الدور ADMIN صلاحية استخدام التطبيق. أما الدور USER فهو موجود فقط لتوضيح استجابة الخادم في حالة الاستخدام هذه؛
  • في [4]، الزر الذي يسمح لك بالاتصال بالخادم؛
  • في [5]، لغة التطبيق. هناك خياران: الفرنسية (الافتراضية) والإنجليزية؛
  • في [6]، عنوان URL للخادم [rdvmedecins-springthymeleaf-server
  • في [1]، تقوم بتسجيل الدخول؛
  • بمجرد تسجيل الدخول، يمكنك اختيار الطبيب الذي ترغب في زيارته [2] وتاريخ الموعد [3]. بمجرد اختيار الطبيب والتاريخ، يتم عرض التقويم تلقائيًا:
  • بمجرد عرض تقويم الطبيب، يمكنك حجز موعد [5]؛
  • في [6]، حدد المريض الذي سيحضر الموعد وقم بتأكيد اختيارك في [7]؛

بمجرد تأكيد الموعد، ستعود تلقائيًا إلى التقويم حيث يظهر الموعد الجديد الآن. يمكن حذف هذا الموعد لاحقًا [8].

تم وصف الميزات الرئيسية. إنها بسيطة. لنختتم بإعدادات اللغة:

Image

  • في [1]، يمكنك التبديل من الفرنسية إلى الإنجليزية؛
  • في [2]، تتحول الواجهة إلى اللغة الإنجليزية، بما في ذلك التقويم؛

8.3. قاعدة البيانات

قاعدة البيانات، المشار إليها فيما يلي بـ [dbrdvmedecins]، هي قاعدة بيانات MySQL5 تحتوي على الجداول التالية:

  

تتم إدارة المواعيد من خلال الجداول التالية:

  • [doctors]: تحتوي على قائمة الأطباء في العيادة؛
  • [clients]: تحتوي على قائمة مرضى العيادة؛
  • [slots]: تحتوي على الفترات الزمنية المتاحة لكل طبيب؛
  • [rv]: تحتوي على قائمة مواعيد الأطباء.

ترتبط الجداول [roles] و [users] و [users_roles] بالمصادقة. ولن نتناولها في الوقت الحالي. فيما يلي العلاقات بين الجداول التي تدير المواعيد:

 
  • تنتمي فترة زمنية إلى طبيب – ولدى الطبيب 0 أو أكثر من الفترات الزمنية؛
  • يجمع الموعد بين العميل والطبيب عبر الفترة الزمنية المخصصة للطبيب؛
  • لدى العميل 0 موعد أو أكثر؛
  • ترتبط الفترة الزمنية بـ 0 أو أكثر من المواعيد (في أيام مختلفة).

8.3.1. جدول [DOCTORS]

يحتوي على معلومات حول الأطباء الذين يديرهم تطبيق [RdvMedecins].

  • ID: الرقم الذي يحدد هوية الطبيب — المفتاح الأساسي للجدول
  • VERSION: رقم يحدد إصدار الصف في الجدول. يزداد هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تغيير على الصف.
  • LAST_NAME: لقب الطبيب
  • FIRST_NAME: الاسم الأول للطبيب
  • TITLE: لقبهم (السيدة، السيدة، السيد)

8.3.2. جدول [CLIENTS]

يتم تخزين عملاء الأطباء المختلفين في جدول [CLIENTS]:

  • ID: رقم التعريف الذي يحدد العميل - المفتاح الأساسي للجدول
  • VERSION: الرقم الذي يحدد إصدار الصف في الجدول. يزداد هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تغيير على الصف.
  • LAST NAME: اسم عائلة العميل
  • FIRST NAME: الاسم الأول للعميل
  • TITLE: لقبهم (السيدة، السيدة، السيد)

8.3.3. جدول [SLOTS]

يسرد المواعيد المتاحة:

  • ID: رقم تعريف الفترة الزمنية - المفتاح الأساسي للجدول (الصف 8)
  • VERSION: الرقم الذي يحدد إصدار الصف في الجدول. يزداد هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تغيير على الصف.
  • DOCTOR_ID: رقم التعريف الذي يحدد الطبيب الذي تنتمي إليه هذه الفترة الزمنية – مفتاح خارجي في عمود DOCTORS(ID).
  • START_TIME: وقت بدء الفترة الزمنية
  • MSTART: دقيقة بدء الفترة الزمنية
  • HFIN: وقت انتهاء الفترة الزمنية
  • MFIN: دقائق نهاية الفترة الزمنية

يشير الصف الثاني من جدول [SLOTS] (انظر [1] أعلاه)، على سبيل المثال، إلى أن الفترة رقم 2 تبدأ في الساعة 8:20 صباحًا وتنتهي في الساعة 8:40 صباحًا، وهي مخصصة للطبيبة رقم 1 (السيدة ماري بيليسييه).

8.3.4. جدول [RV]

يُدرج المواعيد المحجوزة لكل طبيب:

  • ID: معرف فريد للموعد – المفتاح الأساسي
  • DAY: يوم الموعد
  • SLOT_ID: فترة الموعد – مفتاح خارجي في حقل [ID] في جدول [SLOTS] – يحدد كل من فترة الموعد والطبيب المعني.
  • CLIENT_ID: معرف العميل الذي تم الحجز لصالحه – مفتاح خارجي في حقل [ID] بجدول [CLIENTS]

يحتوي هذا الجدول على قيد تفرد على قيم الأعمدة المرتبطة (DAY، SLOT_ID):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

إذا كان أحد الصفوف في الجدول [RV] يحتوي على القيمة (DAY1، SLOT_ID1) للأعمدة (DAY، SLOT_ID)، فلا يمكن أن تظهر هذه القيمة في أي مكان آخر. وإلا، فهذا يعني أنه تم حجز موعدين في نفس الوقت لنفس الطبيب. من منظور برمجة Java، يقوم برنامج تشغيل JDBC الخاص بقاعدة البيانات بإصدار استثناء SQLException عند حدوث ذلك.

الصف الذي يحمل الرقم التعريفي 3 (انظر [1] أعلاه) يعني أنه تم حجز موعد للفترة رقم 20 والعميل رقم 4 في 23/08/2006. يوضح لنا جدول [SLOTS] أن الفترة رقم 20 تتوافق مع الفترة الزمنية 4:20 مساءً – 4:40 مساءً وتخص الطبيبة رقم 1 (السيدة ماري بيليسييه). يخبرنا الجدول [CLIENTS] أن العميل رقم 4 هو السيدة بريجيت بيسترو.

8.3.5. إنشاء قاعدة البيانات

لإنشاء قاعدة البيانات [dbrdvmedecins]، يتم توفير البرنامج النصي [dbrdvmedecins.sql] مع الأمثلة الواردة في هذا المستند [1-3]:

نستخدم أداة [PhpMyAdmin] من WampServer:

  • في [1]، حدد أداة [phpMyAdmin] من WampServer؛
  • في [2]، حدد خيار [Import
  • في [3]، حدد الملف [database/dbrdvmedecins.sql
  • في [4]، قم بتشغيله؛
  • في [5]، يتم إنشاء قاعدة البيانات.

8.4. خدمة الويب / JSON

في البنية أعلاه، سنتناول الآن إنشاء خدمة الويب / JSON التي تم بناؤها باستخدام إطار عمل Spring MVC. سنكتبها في عدة خطوات:

  • أولاً، طبقات [الأعمال] و[DAO] (كائن الوصول إلى البيانات). سنستخدم Spring Data هنا؛
  • بعد ذلك، خدمة الويب JSON بدون مصادقة. سنستخدم Spring MVC هنا؛
  • ثم سنقوم بإضافة مكون المصادقة باستخدام Spring Security.

فيما يلي نسخة من الوثيقة [http://tahe.developpez.com/angularjs-spring4/] مع بعض التعديلات.

8.4.1. مقدمة إلى Spring Data

سنقوم بتنفيذ طبقة [DAO] للمشروع باستخدام Spring Data، وهو أحد مكونات نظام Spring.

يقدم موقع Spring الإلكتروني العديد من الدروس التعليمية للبدء في استخدام Spring [http://spring.io/guides]. سنستخدم إحداها لتقديم Spring Data. ولهذا الغرض، سنستخدم Spring Tool Suite (STS).

  • في [1]، نقوم باستيراد أحد البرامج التعليمية من [spring.io/guides
  • في [2]، نختار البرنامج التعليمي [الوصول إلى البيانات JPA]، الذي يوضح كيفية الوصول إلى قاعدة البيانات باستخدام Spring Data؛
  • في [3]، نختار مشروعًا تم تكوينه بواسطة Maven؛
  • في [4]، يتوفر البرنامج التعليمي في شكلين: [initial]، وهو نسخة فارغة تقوم بملئها باتباع البرنامج التعليمي، أو [complete]، وهي النسخة النهائية من البرنامج التعليمي. نختار الخيار الأخير؛
  • في [5]، يمكنك اختيار عرض البرنامج التعليمي في متصفح؛
  • في [6]، المشروع النهائي.

8.4.1.1. تكوين Maven للمشروع

يتم تكوين تبعيات Maven للمشروع في ملف [pom.xml]:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <!-- use UTF-8 for everything -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • الأسطر 5–9: تعريف مشروع Maven الأصلي. يحدد هذا المشروع معظم تبعيات المشروع. قد تكون هذه التبعيات كافية، وفي هذه الحالة لا تضاف أي تبعيات إضافية، أو قد لا تكون كافية، وفي هذه الحالة تضاف التبعيات المفقودة؛
  • الأسطر 12–15: تحدد تبعية لـ [spring-boot-starter-data-jpa]. تحتوي هذه الأداة على فئات Spring Data؛
  • الأسطر 16–19: تحدد تبعية لنظام إدارة قواعد البيانات H2، الذي يسمح لك بإنشاء وإدارة قواعد البيانات في الذاكرة.

دعونا نلقي نظرة على الفئات التي توفرها هذه التبعيات:

هناك العديد منها:

  • بعضها ينتمي إلى نظام Spring (تلك التي تبدأ بـ spring
  • والبعض الآخر جزء من منظومة Hibernate (Hibernate، JBoss)، ونحن نستخدم تطبيق JPA هنا؛
  • وبعضها الآخر عبارة عن مكتبات اختبار (JUnit، Hamcrest
  • وبعضها الآخر عبارة عن مكتبات تسجيل (log4j، logback، slf4j

سنحتفظ بها جميعًا. بالنسبة لتطبيق الإنتاج، يجب الاحتفاظ فقط بالمكتبات الضرورية.

في السطر 26 من ملف [pom.xml]، نجد السطر:


<start-class>hello.Application</start-class>

هذا السطر مرتبط بالأسطر التالية:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

الأسطر 6–9: يتيح لك [spring-boot-maven-plugin] إنشاء ملف JAR القابل للتنفيذ الخاص بالتطبيق. ثم تحدد السطر 26 من ملف [pom.xml] الفئة القابلة للتنفيذ لهذا الملف JAR.

8.4.1.2. طبقة [JPA]

يتم التعامل مع الوصول إلى قاعدة البيانات من خلال طبقة [JPA]، وهي واجهة برمجة تطبيقات Java Persistence:

  

التطبيق بسيط ويقوم بإدارة كيانات [Customer]. فئة [Customer] هي جزء من طبقة [JPA] وهي كما يلي:


package hello;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
@Entity
public class Customer {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;
 
    protected Customer() {
    }
 
    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
 
    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }
 
}

يحتوي العميل على معرف [id] واسم أول [firstName] واسم عائلة [lastName]. تمثل كل مثيل [Customer] صفًا في جدول قاعدة البيانات.

  • السطر 8: تعليق JPA يضمن أن استمرارية مثيلات [Customer] (إنشاء، قراءة، تحديث، حذف) ستدار بواسطة تطبيق JPA. استنادًا إلى تبعيات Maven، يمكننا أن نرى أن تطبيق JPA/Hibernate قيد الاستخدام؛
  • السطران 11-12: تعليقات توضيحية لـ JPA تربط حقل [id] بالمفتاح الأساسي لجدول [Customer]. يشير السطر 12 إلى أن تطبيق JPA سيستخدم طريقة إنشاء المفتاح الأساسي الخاصة بنظام إدارة قواعد البيانات المستخدم، وهو H2 في هذه الحالة؛

لا توجد تعليقات توضيحية أخرى لـ JPA. وبالتالي، سيتم استخدام القيم الافتراضية:

  • سيتم تسمية جدول [Customer] على اسم الفئة، أي [Customer
  • ستحمل أعمدة هذا الجدول أسماء حقول الفئة: [id, firstName, lastName]، مع ملاحظة أن حالة الأحرف لا تؤخذ في الاعتبار في أسماء أعمدة الجدول؛

لاحظ أن تطبيق JPA المستخدم لم يتم ذكره مطلقًا.

8.4.1.3. طبقة [DAO]

  

تنفذ فئة [CustomerRepository] طبقة [DAO]. وفيما يلي شفرة البرمجة الخاصة بها:


package hello;
 
import java.util.List;
 
import org.springframework.data.repository.CrudRepository;
 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
 
    List<Customer> findByLastName(String lastName);
}

وبالتالي، فهذه واجهة وليست فئة (السطر 7). وهي تمتد واجهة [CrudRepository]، وهي واجهة Spring Data (السطر 5). يتم تحديد معلمات هذه الواجهة بواسطة نوعين: الأول هو نوع العناصر المدارة، وهو هنا نوع [Customer]؛ والثاني هو نوع المفتاح الأساسي للعناصر المدارة، وهو هنا نوع [Long]. واجهة [CrudRepository] هي كما يلي:


package org.springframework.data.repository;
 
import java.io.Serializable;
 
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
 
    <S extends T> S save(S entity);
 
    <S extends T> Iterable<S> save(Iterable<S> entities);
 
    T findOne(ID id);
 
    boolean exists(ID id);
 
    Iterable<T> findAll();
 
    Iterable<T> findAll(Iterable<ID> ids);
 
    long count();
 
    void delete(ID id);
 
    void delete(T entity);
 
    void delete(Iterable<? extends T> entities);
 
    void deleteAll();
}

تحدد هذه الواجهة عمليات CRUD (إنشاء – قراءة – تحديث – حذف) التي يمكن تنفيذها على نوع JPA T:

  • السطر 8: تسمح لك طريقة save بحفظ كيان T في قاعدة البيانات. وهي تحفظ الكيان باستخدام المفتاح الأساسي المخصص له بواسطة نظام إدارة قواعد البيانات (DBMS). كما تسمح لك بتحديث كيان T المحدد بواسطة معرف المفتاح الأساسي الخاص به. يعتمد الاختيار بين هذين الإجراءين على قيمة معرف المفتاح الأساسي: إذا كانت قيمة معرف المفتاح الأساسي null، تتم عملية الحفظ؛ وإلا، تتم عملية التحديث؛
  • السطر 10: كما هو مذكور أعلاه، ولكن بالنسبة لقائمة الكيانات؛
  • السطر 12: تسترد طريقة findOne كيان T المحدد بواسطة معرف المفتاح الأساسي الخاص به؛
  • السطر 22: تسمح لك طريقة delete بحذف كيان T المحدد بواسطة معرف المفتاح الأساسي الخاص به؛
  • الأسطر 24-28: أشكال مختلفة من طريقة [delete
  • السطر 16: تسترد طريقة [findAll] جميع الكيانات T الدائمة؛
  • السطر 18: كما هو مذكور أعلاه، ولكن يقتصر على الكيانات التي تم توفير قائمة بمعرفاتها؛

لنعد إلى واجهة [CustomerRepository]:


package hello;
 
import java.util.List;
 
import org.springframework.data.repository.CrudRepository;
 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
 
    List<Customer> findByLastName(String lastName);
}
  • السطر 9 يسمح لك باسترداد [Customer] حسب [lastName

وهذا كل شيء بالنسبة لطبقة [DAO]. لا توجد فئة تنفيذ للواجهة السابقة. يتم إنشاؤها في وقت التشغيل بواسطة [Spring Data]. يتم تنفيذ أساليب واجهة [CrudRepository] تلقائيًا. أما بالنسبة للأساليب المضافة إلى واجهة [CustomerRepository]، فهذا يعتمد على الحالة. لنعد إلى تعريف [Customer]:


    private long id;
    private String firstName;
private String lastName;

يتم تنفيذ الطريقة الموجودة في السطر 9 تلقائيًا بواسطة [Spring Data] لأنها تشير إلى حقل [lastName] (السطر 3) في [Customer]. عندما تصادف طريقة [findBySomething] في الواجهة المراد تنفيذها، تقوم Spring Data بتنفيذها باستخدام استعلام JPQL (لغة استعلامات استمرارية Java) التالي:

select t from T t where t.something=:value

لذلك، يجب أن يحتوي النوع T على حقل باسم [something]. وبالتالي، فإن الطريقة

List<Customer> findByLastName(String lastName);

بكود مشابه لما يلي:

return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()

حيث يشير [em] إلى سياق الاستمرارية في JPA. ولا يكون ذلك ممكنًا إلا إذا كانت فئة [Customer] تحتوي على حقل باسم [lastName]، وهو ما يحدث بالفعل.

في الختام، في الحالات البسيطة، يسمح لنا Spring Data بتنفيذ طبقة [DAO] بواجهة بسيطة.

8.4.1.4. طبقة [console]

  

فئة [Application] هي كما يلي:


package hello;
 
import java.util.List;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
 
@Configuration
@EnableAutoConfiguration
public class Application {
 
    public static void main(String[] args) {
 
        ConfigurableApplicationContext context = SpringApplication.run(Application.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
 
        // save a couple of customers
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));
 
        // fetch all customers
        Iterable<Customer> customers = repository.findAll();
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : customers) {
            System.out.println(customer);
        }
        System.out.println();
 
        // fetch an individual customer by ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();
 
        // fetch customers by last name
        List<Customer> bauers = repository.findByLastName("Bauer");
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : bauers) {
            System.out.println(bauer);
        }
 
        context.close();
    }
 
}
  • السطر 10: يشير إلى أن الفئة تُستخدم لتكوين Spring. يمكن بالفعل تكوين الإصدارات الحديثة من Spring في Java بدلاً من XML. يمكن استخدام كلتا الطريقتين في وقت واحد. في كود الفئة المُعلَّمة بـ [Configuration]، نجد عادةً حبوب Spring، أي تعريفات الفئات التي سيتم إنشاء مثيلات لها. هنا، لم يتم تعريف أي حبوب. من المهم ملاحظة أنه عند العمل مع نظام إدارة قواعد البيانات (DBMS)، يجب تعريف حبوب Spring المختلفة:
    • [EntityManagerFactory] الذي يحدد تطبيق JPA المراد استخدامه،
    • [DataSource] الذي يحدد مصدر البيانات المراد استخدامه،
    • [TransactionManager] الذي يحدد مدير المعاملات المراد استخدامه؛

هنا، لم يتم تعريف أي من هذه الحبوب.

  • السطر 11: التعليق التوضيحي [EnableAutoConfiguration] هو تعليق توضيحي من مشروع [Spring Boot] (السطران 5-6). يوجه هذا التعليق Spring Boot عبر فئة [SpringApplication] (السطر 16) لتكوين التطبيق بناءً على المكتبات الموجودة في مسار الفئات الخاص به. نظرًا لوجود مكتبات Hibernate في مسار الفئات، سيتم تنفيذ حبة [entityManagerFactory] باستخدام Hibernate. نظرًا لوجود مكتبة H2 DBMS في مسار الفئات ( )، سيتم تنفيذ حبة [dataSource] باستخدام H2. في bean [dataSource]، يجب علينا أيضًا تعريف اسم المستخدم وكلمة المرور. هنا، سيستخدم Spring Boot المسؤول الافتراضي لـ H2، الذي لا يحتوي على كلمة مرور. ونظرًا لوجود مكتبة [spring-tx] في مسار الفئات، سيتم استخدام مدير المعاملات في Spring.

بالإضافة إلى ذلك، سيتم فحص الدليل الذي يحتوي على فئة [Application] بحثًا عن حبات يتم التعرف عليها ضمناً بواسطة Spring أو يتم تعريفها صراحةً بواسطة تعليقات Spring. وبالتالي، سيتم فحص فئتي [Customer] و [CustomerRepository]. نظرًا لأن الأولى تحتوي على تعليق [@Entity]، فسيتم تصنيفها ككيان يديره Hibernate. ونظرًا لأن الثانية تمتد واجهة [CrudRepository]، فسيتم تسجيلها كحبة Spring.

دعونا نفحص السطور 16-17 من الكود:


ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
  • السطر 16: يتم تنفيذ الطريقة الثابتة [run] الخاصة بفئة [SpringApplication] في مشروع Spring Boot. والمعلمة الخاصة بها هي الفئة التي تحتوي على تعليق [Configuration] أو [EnableAutoConfiguration]. وعندئذٍ سيتم تنفيذ كل ما تم شرحه سابقًا. والنتيجة هي سياق تطبيق Spring، أي مجموعة من الحبوب (beans) التي يديرها Spring؛
  • السطر 17: نطلب حبة تُنفذ واجهة [CustomerRepository] من سياق Spring هذا. هنا، نسترد الفئة التي أنشأتها Spring Data لتنفيذ هذه الواجهة.

تستخدم العمليات التالية ببساطة أساليب bean التي تنفذ واجهة [CustomerRepository]. لاحظ في السطر 50 أن السياق مغلق. يكون إخراج وحدة التحكم كما يلي:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v1.1.10.RELEASE)

2014-12-19 11:13:46.612  INFO 10932 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 10932 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\etude-de-cas\gs-accessing-data-jpa-complete)
2014-12-19 11:13:46.658  INFO 10932 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@279ad2e3: startup date [Fri Dec 19 11:13:46 CET 2014]; root of context hierarchy
2014-12-19 11:13:48.234  INFO 10932 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2014-12-19 11:13:48.258  INFO 10932 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2014-12-19 11:13:48.337  INFO 10932 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.7.Final}
2014-12-19 11:13:48.339  INFO 10932 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2014-12-19 11:13:48.341  INFO 10932 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2014-12-19 11:13:48.620  INFO 10932 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2014-12-19 11:13:48.689  INFO 10932 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2014-12-19 11:13:48.853  INFO 10932 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2014-12-19 11:13:49.143  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2014-12-19 11:13:49.151  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2014-12-19 11:13:49.692  INFO 10932 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-12-19 11:13:49.709  INFO 10932 --- [           main] hello.Application                        : Started Application in 3.461 seconds (JVM running for 4.435)
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2014-12-19 11:13:49.931  INFO 10932 --- [           main] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@279ad2e3: startup date [Fri Dec 19 11:13:46 CET 2014]; root of context hierarchy
2014-12-19 11:13:49.933  INFO 10932 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2014-12-19 11:13:49.934  INFO 10932 --- [           main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2014-12-19 11:13:49.935  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2014-12-19 11:13:49.938  INFO 10932 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
  • الأسطر 1-8: شعار مشروع Spring Boot؛
  • السطر 9: يتم تنفيذ فئة [hello.Application
  • السطر 10: [AnnotationConfigApplicationContext] هي فئة تنفذ واجهة [ApplicationContext] الخاصة بـ Spring. وهي عبارة عن حاوية bean؛
  • السطر 11: يتم تنفيذ bean [entityManagerFactory] باستخدام فئة [LocalContainerEntityManagerFactory]، وهي فئة Spring؛
  • السطر 15: يظهر [Hibernate]. هذا هو تطبيق JPA الذي تم اختياره؛
  • السطر 19: لهجة Hibernate هي متغير SQL الذي سيتم استخدامه مع نظام إدارة قواعد البيانات (DBMS). هنا، تشير لهجة [H2Dialect] إلى أن Hibernate سيعمل مع نظام إدارة قواعد البيانات H2؛
  • السطران 21-22: يتم إنشاء قاعدة البيانات. يتم إنشاء الجدول [CUSTOMER]. وهذا يعني أنه تم تكوين Hibernate لإنشاء جداول من تعريفات JPA، وفي هذه الحالة تعريف JPA لفئة [Customer
  • الأسطر 27–31: يتم إدراج العملاء الخمسة؛
  • الأسطر 33-35: نتيجة طريقة [findOne] للواجهة؛
  • الأسطر 37-40: نتائج طريقة [findByLastName
  • السطور 41 وما يليها: سجلات من إغلاق سياق Spring.

8.4.1.5. التكوين اليدوي لمشروع Spring Data

نقوم بنسخ المشروع السابق إلى مشروع [gs-accessing-data-jpa-2]:

  

في هذا المشروع الجديد، لن نعتمد على التكوين التلقائي الذي يوفره Spring Boot. سنقوم بتكوينه يدويًا. قد يكون هذا مفيدًا إذا كانت التكوينات الافتراضية لا تناسب احتياجاتنا.

أولاً، سنحدد التبعيات الضرورية في ملف [pom.xml]:


...
    <dependencies>
        <!-- Spring Core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring transactions -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring ORM -->        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.7.1.RELEASE</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>1.1.10.RELEASE</version>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.4.Final</version>
        </dependency>
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.178</version>
        </dependency>
        <!-- Commons DBCP -->
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
            <version>1.6</version>
        </dependency>
    </dependencies>
...
 
</project>
  • الأسطر 2–18: مكتبات Spring الأساسية؛
  • الأسطر 19–29: مكتبات Spring لإدارة معاملات قاعدة البيانات؛
  • الأسطر 30–35: مكتبة Spring للعمل مع ORM (مُخطط العلاقات بين الكائنات)؛
  • الأسطر 36–41: Spring Data المستخدمة للوصول إلى قاعدة البيانات؛
  • الأسطر 42–47: Spring Boot لتشغيل التطبيق؛
  • الأسطر 54–59: نظام إدارة قواعد البيانات H2؛
  • الأسطر 60–70: غالبًا ما تُستخدم قواعد البيانات مع مجموعات الاتصال، مما يتجنب فتح وإغلاق الاتصالات بشكل متكرر. هنا، يتم استخدام [commons-dbcp] في التنفيذ؛

لا نزال في [pom.xml]، نغير اسم الفئة القابلة للتنفيذ:


    <properties>
...
        <start-class>demo.console.Main</start-class>
</properties>

في المشروع الجديد، تظل كيان [Customer] وواجهة [CustomerRepository] دون تغيير. سنقوم بتعديل فئة [Application]، والتي سيتم تقسيمها إلى فئتين:

  • [Config]، التي ستكون فئة التكوين:
  • [Main]، التي ستكون فئة التنفيذ؛
  

الفئة القابلة للتنفيذ [Main] هي نفسها كما كانت من قبل، بدون تعليقات التكوين:


package demo.console;
 
import java.util.List;
 
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
 
public class Main {
 
    public static void main(String[] args) {
 
        ConfigurableApplicationContext context = SpringApplication.run(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
...
 
        context.close();
    }
 
}
  • السطر 12: لم تعد فئة [Main] تحتوي على أي تعليقات توضيحية للتكوين؛
  • السطر 16: يتم تشغيل التطبيق باستخدام Spring Boot. المعلمة [Config.class] هي فئة التكوين الجديدة للمشروع؛

فئة [Config] التي تهيئ المشروع هي كما يلي:


package demo.config;
 
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
 
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
    // h2 data source
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }
 
    // the provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("demo.entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • السطر 22: تجعل العلامة [@Configuration] فئة [Config] فئة تكوين Spring؛
  • السطر 21: تحدد العلامة [@EnableJpaRepositories] الدلائل التي توجد فيها واجهات Spring Data [CrudRepository]. ستصبح هذه الواجهات مكونات Spring وستكون متاحة في سياقها؛
  • السطر 20: تشير العلامة [@EnableTransactionManagement] إلى أن طرق واجهات [CrudRepository] يجب أن تُنفَّذ ضمن معاملة؛
  • السطر 19: تحدد العلامة [@EntityScan] الدلائل التي يجب البحث فيها عن كيانات JPA. وقد تم تعليقها هنا لأن هذه المعلومات تم توفيرها صراحةً في السطر 50. يجب أن تكون هذه العلامة موجودة في حالة استخدام وضع [@EnableAutoConfiguration] وعدم وجود كيانات JPA في نفس الدليل الذي توجد فيه فئة التكوين؛
  • السطر 18: تسمح لك علامة [@ComponentScan] بإدراج الدلائل التي يجب البحث فيها عن مكونات Spring. مكونات Spring هي فئات مزودة بعلامات Spring مثل @Service و@Component و@Controller وغيرها. هنا، لا توجد مكونات أخرى غير تلك المُعرَّفة داخل فئة [Config]، لذا تم تعليق هذه العلامة؛
  • الأسطر 25-33: تحدد مصدر البيانات، قاعدة بيانات H2. إن تعليق @Bean في السطر 25 هو الذي يجعل الكائن الذي تم إنشاؤه بواسطة هذه الطريقة مكونًا تديره Spring. يمكن أن يكون اسم الطريقة هنا أي شيء. ومع ذلك، يجب تسميته [dataSource] إذا كان EntityManagerFactory في السطر 47 غائبًا وتم تعريفه عبر التكوين التلقائي؛
  • السطر 29: سيتم تسمية قاعدة البيانات [demo] وسيتم إنشاؤها في مجلد المشروع؛
  • الأسطر 36–43: تعريف تطبيق JPA المستخدم، وهو في هذه الحالة تطبيق Hibernate. يمكن أن يكون اسم الطريقة هنا أي شيء؛
  • السطر 39: لا توجد سجلات SQL؛
  • السطر 30: سيتم إنشاء قاعدة البيانات إذا لم تكن موجودة؛
  • الأسطر 46-54: تحدد EntityManagerFactory التي ستدير استمرارية JPA. يجب تسمية الطريقة [entityManagerFactory
  • السطر 47: تتلقى الطريقة معلمتين من أنواع الحبتين المحددتين سابقًا. سيتم بعد ذلك إنشاء هاتين الحبتين وحقنهما بواسطة Spring كمعلمات للطريقة؛
  • السطر 49: يحدد تنفيذ JPA المراد استخدامه؛
  • السطر 50: يحدد الدلائل التي يمكن العثور فيها على كيانات JPA؛
  • السطر 51: يحدد مصدر البيانات المراد إدارته؛
  • الأسطر 57–62: مدير المعاملات. يجب تسمية الطريقة [transactionManager]. تتلقى الحبة من الأسطر 46–54 كمعلمة؛
  • السطر 60: يرتبط مدير المعاملات بـ EntityManagerFactory؛

يمكن تعريف الطرق السابقة بأي ترتيب.

يؤدي تشغيل المشروع إلى نفس النتائج. يظهر ملف جديد في مجلد المشروع، وهو ملف قاعدة بيانات H2:

  

أخيرًا، يمكننا الاستغناء عن Spring Boot. نقوم بإنشاء فئة قابلة للتنفيذ ثانية [Main2]:

  

تحتوي فئة [Main2] على الكود التالي:


package demo.console;
 
import java.util.List;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
 
public class Main2 {

    public static void main(String[] args) {
 
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
....
 
        context.close();
    }
 
}
  • السطر 15: يتم الآن استخدام فئة التكوين [Config] بواسطة فئة Spring [AnnotationConfigApplicationContext]. كما هو موضح في السطر 5، لم تعد هناك أي تبعيات على Spring Boot.

يؤدي التنفيذ إلى نفس النتائج كما في السابق.

8.4.1.6. إنشاء أرشيف قابل للتنفيذ

لإنشاء أرشيف قابل للتنفيذ للمشروع، اتبع الخطوات التالية:

  • في [1]: إنشاء تكوين وقت التشغيل؛
  • في [2]: من النوع [تطبيق Java]
  • في [3]: حدد المشروع المراد تشغيله (استخدم زر "تصفح"
  • في [4]: حدد الفئة المراد تشغيلها؛
  • في [5]: اسم تكوين التشغيل – يمكن أن يكون أي شيء؛
  • في [6]: تصدير المشروع؛
  • في [7]: كأرشيف JAR قابل للتنفيذ؛
  • في [8]: حدد مسار واسم الملف القابل للتنفيذ المراد إنشاؤه؛
  • في [9]: اسم تكوين التشغيل الذي تم إنشاؤه في [5]؛

بمجرد الانتهاء من ذلك، افتح وحدة تحكم في المجلد الذي يحتوي على الأرشيف القابل للتنفيذ:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-2.jar

يتم تشغيل الأرشيف على النحو التالي:


.....\dist>java -jar gs-accessing-data-jpa-2.jar

النتائج المعروضة في وحدة التحكم هي كما يلي:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
juin 12, 2014 9:48:38 AM org.hibernate.ejb.HibernatePersistence logDeprecation
WARN: HHH015016: Encountered a deprecated javax.persistence.spi.PersistenceProvider [org.hibernate.ejb.HibernatePersistence]; use [org.hibernate.jpa.HibernatePersistenceProvider] instead.
juin 12, 2014 9:48:38 AM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
        name: default
        ...]
juin 12, 2014 9:48:38 AM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.4.Final}
juin 12, 2014 9:48:38 AM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
juin 12, 2014 9:48:38 AM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
juin 12, 2014 9:48:39 AM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
juin 12, 2014 9:48:39 AM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
juin 12, 2014 9:48:39 AM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
juin 12, 2014 9:48:40 AM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

8.4.1.7. إنشاء مشروع Spring Data جديد

لإنشاء قالب مشروع Spring Data، اتبع الخطوات التالية:

  • في [1]، قم بإنشاء مشروع جديد؛
  • في [2]: حدد [مشروع Spring Starter
  • سيكون المشروع الذي تم إنشاؤه مشروع Maven. في [3]، حدد اسم مجموعة المشروع؛
  • في [4]، حدد اسم الأداة (ملف JAR في هذه الحالة) التي سيتم إنشاؤها عند بناء المشروع؛
  • في [5]: حدد حزمة الفئة القابلة للتنفيذ التي سيتم إنشاؤها في المشروع؛
  • في [6]: اسم المشروع في Eclipse – يمكن أن يكون أي شيء (لا يجب أن يكون هو نفسه [4])؛
  • في [7]: حدد أنك تقوم بإنشاء مشروع بطبقة [JPA]. سيتم بعد ذلك تضمين التبعيات المطلوبة لمثل هذا المشروع في ملف [pom.xml
  • في [8]: المشروع الذي تم إنشاؤه؛

يتضمن ملف [pom.xml] التبعيات المطلوبة لمشروع JPA:


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
  • الأسطر 9–12: التبعيات المطلوبة لـ JPA — ستشمل [Spring Data
  • الأسطر 13–17: التبعيات المطلوبة لاختبارات JUnit المدمجة مع Spring؛

الفئة القابلة للتنفيذ [Application] لا تقوم بأي شيء ولكنها مهيأة مسبقًا:


package istia.st;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

فئة الاختبار [ApplicationTests] لا تقوم بأي شيء ولكنها مهيأة مسبقًا:


package istia.st;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
 
    @Test
    public void contextLoads() {
    }
 
}
  • السطر 9: تسمح علامة [@SpringApplicationConfiguration] باستخدام ملف التكوين [Application]. وبالتالي ستستفيد فئة الاختبار من جميع الفاصوليا المحددة في هذا الملف؛
  • السطر 8: تتيح العلامة [@RunWith] تكامل Spring مع JUnit: سيتمكن تنفيذ الفئة كاختبار JUnit. [@RunWith] هي علامة JUnit (السطر 4)، في حين أن فئة [SpringJUnit4ClassRunner] هي فئة Spring (السطر 6)؛

الآن بعد أن أصبح لدينا هيكل تطبيق JPA، يمكننا إكماله لكتابة طبقة الثبات من جانب الخادم لتطبيق إدارة المواعيد الخاص بنا.

8.4.2. مشروع خادم Eclipse

  

المكونات الرئيسية للمشروع هي كما يلي:

  • [pom.xml]: ملف تكوين Maven الخاص بالمشروع؛
  • [rdvmedecins.entities]: كيانات JPA؛
  • [rdvmedecins.repositories]: واجهات Spring Data للوصول إلى كيانات JPA؛
  • [rdvmedecins.metier]: طبقة [الأعمال]؛
  • [rdvmedecins.domain]: الكيانات التي تتعامل معها طبقة [الأعمال]؛
  • [rdvmdecins.config]: فئات تكوين طبقة الاستمرارية؛
  • [rdvmedecins.boot]: تطبيق وحدة تحكم أساسي؛

8.4.3. تكوين Maven

ملف [pom.xml] الخاص بالمشروع هو كما يلي:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <modelVersion>4.0.0</modelVersion>
        <groupId>istia.st.spring4.rdvmedecins</groupId>
        <artifactId>rdvmedecins-metier-dao</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
        </parent>
        <dependencies>
                <!-- Spring Data JPA -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-data-jpa</artifactId>
                </dependency>
                <!-- Spring test -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <!-- Spring security -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-security</artifactId>
                </dependency>
                <!-- driver JDBC / MySQL -->
                <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                </dependency>
                <!-- Tomcat JDBC -->
                <dependency>
                        <groupId>org.apache.tomcat</groupId>
                        <artifactId>tomcat-jdbc</artifactId>
                </dependency>
                <!-- mapper jSON -->
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-databind</artifactId>
                </dependency>
                <!-- Googe Guava -->
                <dependency>
                        <groupId>com.google.guava</groupId>
                        <artifactId>guava</artifactId>
                        <version>16.0.1</version>
                </dependency>
        </dependencies>
        <properties>
                <!-- use UTF-8 for everything -->
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
                <start-class>rdvmedecins.boot.Boot</start-class>
                <java.version>1.8</java.version>
        </properties>
        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>
        <repositories>
                <repository>
                        <id>spring-milestones</id>
                        <name>Spring Milestones</name>
                        <url>http://repo.spring.io/libs-milestone</url>
                        <snapshots>
                                <enabled>false</enabled>
                        </snapshots>
                </repository>
                <repository>
                        <id>org.jboss.repository.releases</id>
                        <name>JBoss Maven Release Repository</name>
                        <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
                        <snapshots>
                                <enabled>false</enabled>
                        </snapshots>
                </repository>
        </repositories>
        <pluginRepositories>
                <pluginRepository>
                        <id>spring-milestones</id>
                        <name>Spring Milestones</name>
                        <url>http://repo.spring.io/libs-milestone</url>
                        <snapshots>
                                <enabled>false</enabled>
                        </snapshots>
                </pluginRepository>
        </pluginRepositories>
</project>
  • الأسطر 8–12: يعتمد المشروع على المشروع الأصلي [spring-boot-starter-parent]. بالنسبة للتبعيات الموجودة بالفعل في المشروع الأصلي، لم يتم تحديد أي إصدار. سيتم استخدام الإصدار المحدد في المشروع الأصلي. يتم إعلان التبعيات الأخرى كالمعتاد؛
  • الأسطر 15–18: لـ Spring Data؛
  • الأسطر 20–24: لاختبارات JUnit؛
  • الأسطر 26–29: لمكتبة Spring Security، التي تستخدم طبقة [DAO] الخاصة بها إحدى فئات تشفير كلمات المرور؛
  • الأسطر 31–34: برنامج تشغيل JDBC لنظام إدارة قواعد البيانات MySQL5؛
  • الأسطر 36–39: تجمع اتصالات JDBC في Tomcat. يقوم تجمع الاتصالات بتجميع الاتصالات المفتوحة إلى قاعدة البيانات. عندما يرغب الكود في فتح اتصال، فإنه يطلب واحدًا من التجمع. وعندما يغلق الكود الاتصال، لا يتم إغلاقه بل يُعاد إلى التجمع. يحدث كل هذا بشكل شفاف على مستوى الكود. يتم تحسين الأداء لأن فتح وإغلاق الاتصال بشكل متكرر يستغرق وقتًا. هنا، ينشئ تجمع الاتصالات عددًا معينًا من الاتصالات بقاعدة البيانات عند الإنشاء. بعد ذلك، لا يتم فتح أو إغلاق الاتصالات، ما لم يتبين أن عدد الاتصالات المخزنة في التجمع غير كافٍ. في هذه الحالة، يقوم التجمع تلقائيًا بإنشاء اتصالات جديدة؛
  • الأسطر 41-44: مكتبة Jackson لمعالجة JSON؛
  • الأسطر 46-50: مكتبة Google Collections؛

8.4.4. كيانات JPA

كيانات JPA هي الكائنات التي تغلف صفوف جداول قاعدة البيانات.

  

فئة [AbstractEntity] هي الفئة الأم للكيانات [Person، Slot، Appointment]. وتعريفها كما يلي:


package rdvmedecins.entities;
 
import java.io.Serializable;
 
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
 
@MappedSuperclass
public class AbstractEntity implements Serializable {
 
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;
    @Version
    protected Long version;
 
    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }
 
    // initialization
    public AbstractEntity build(Long id, Long version) {
        this.id = id;
        this.version = version;
        return this;
    }
 
        @Override
    public boolean equals(Object entity) {
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1) || entity==null) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return this.id.longValue() == other.id.longValue();
    }
 
 
    // getters and setters
    ..
}
  • السطر 11: تشير العلامة [@MappedSuperclass] إلى أن الفئة المُعلَّمة هي فئة أم لكيانات JPA [@Entity
  • الأسطر 15-17: تحدد المفتاح الأساسي [id] لكل كيان. إن التعليق التوضيحي [@Id] هو الذي يجعل الحقل [id] مفتاحًا أساسيًا. يشير التعليق التوضيحي [@GeneratedValue(strategy = GenerationType.IDENTITY)] إلى أن قيمة هذا المفتاح الأساسي يتم إنشاؤها بواسطة نظام إدارة قواعد البيانات (DBMS) وأن وضع الإنشاء [IDENTITY] يتم فرضه. بالنسبة لنظام إدارة قواعد البيانات MySQL، يعني هذا أن المفاتيح الأساسية سيتم إنشاؤها بواسطة نظام إدارة قواعد البيانات باستخدام السمة [AUTO_INCREMENT]
  • السطران 18-19: يحددان إصدار كل كيان. سيقوم تطبيق JPA بزيادة رقم الإصدار هذا في كل مرة يتم فيها تعديل الكيان. يُستخدم هذا الرقم لمنع التحديثات المتزامنة للكيان من قبل مستخدمين مختلفين: يقرأ مستخدمان، U1 و U2، الكيان E برقم إصدار يساوي V1. يقوم U1 بتعديل E ويحفظ هذا التغيير في قاعدة البيانات: ثم يتغير رقم الإصدار إلى V1+1. يقوم U2 بدوره بتعديل E ويحفظ هذا التغيير في قاعدة البيانات: سيتلقى استثناءً لأن إصداره (V1) يختلف عن الإصدار الموجود في قاعدة البيانات (V1+1
  • الأسطر 29-33: تقوم طريقة [build] بتهيئة الحقلين في [AbstractEntity]. تُرجع هذه الطريقة مرجعًا إلى مثيل [AbstractEntity] الذي تم تهيئته؛
  • الأسطر 36-44: يتم تجاوز طريقة [equals] للفئة: يُعتبر الكيانان متساويين إذا كان لهما نفس اسم الفئة ونفس معرف الهوية؛
  • الأسطر 21–26: عند تجاوز طريقة [equals] للفئة، يجب أيضًا تجاوز طريقة [hashCode] الخاصة بها (الأسطر 21–26). القاعدة هي أن الكيانين اللذين تعتبرهما طريقة [equals] متساويين يجب أن يكون لهما أيضًا نفس [hashCode]. هنا، [hashCode] للكيان يساوي مفتاحه الأساسي [id]. يُستخدم [hashCode] للفئة، على وجه الخصوص، في إدارة القواميس التي تكون قيمها مثيلات للفئة؛

الكيان [Person] هو الفئة الأم لكيانات [Doctor] و [Client]:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
 
@MappedSuperclass
public class Personne extends AbstractEntity {
    private static final long serialVersionUID = 1L;
    // attributes of a person
    @Column(length = 5)
    private String titre;
    @Column(length = 20)
    private String nom;
    @Column(length = 20)
    private String prenom;
 
    // default builder
    public Personne() {
    }
 
    // builder with parameters
    public Personne(String titre, String nom, String prenom) {
        this.titre = titre;
        this.nom = nom;
        this.prenom = prenom;
    }
 
    // toString
    public String toString() {
        return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
    }
 
    // getters and setters
    ...
}
  • السطر 6: تشير العلامة [@MappedSuperclass] إلى أن الفئة المُعلَّمة هي فئة أم لكيانات JPA [@Entity
  • الأسطر 10–15: الشخص لديه لقب (Ms.)، واسم أول (Jacqueline)، واسم عائلة (Tatou). لم يتم توفير أي معلومات حول أعمدة الجدول. وبالتالي، سيكون لها افتراضيًا نفس أسماء الحقول؛

الكيان [Medecin] هو كما يلي:


package rdvmedecins.entities;
 
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
 
    private static final long serialVersionUID = 1L;
 
    // default builder
    public Medecin() {
    }
 
    // builder with parameters
    public Medecin(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }
 
    public String toString() {
        return String.format("Medecin[%s]", super.toString());
    }
 
}
  • السطر 6: الفئة هي كيان JPA؛
  • السطر 7: مرتبطة بجدول [DOCTORS] في قاعدة البيانات؛
  • السطر 8: الكيان [Doctor] مشتق من الكيان [Person

يمكن تهيئة طبيب على النحو التالي:

Medecin m=new Medecin("Mr","Paul","Tatou");

وإذا أردنا، بالإضافة إلى ذلك، تعيين معرف وإصدار له، فيمكننا كتابة:

Medecin m=new Medecin("Mr","Paul","Tatou").build(10,1);

حيث طريقة [build] هي الطريقة المُعرَّفة في [AbstractEntity].

الكيان [Client] هو كما يلي:


package rdvmedecins.entities;
 
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "clients")
public class Client extends Personne {
 
    private static final long serialVersionUID = 1L;
 
    // default builder
    public Client() {
    }
 
    // builder with parameters
    public Client(String titre, String nom, String prenom) {
        super(titre, nom, prenom);
    }
 
    // identity
    public String toString() {
        return String.format("Client[%s]", super.toString());
    }
 
}
  • السطر 6: الفئة هي كيان JPA؛
  • السطر 7: مرتبطة بجدول [CLIENTS] في قاعدة البيانات؛
  • السطر 8: الكيان [Client] مشتق من الكيان [Person

الكيان [TimeSlot] هو كما يلي:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
 
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
 
    // default builder
    public Creneau() {
    }
 
    // builder with parameters
    public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
        this.medecin = medecin;
        this.hdebut = hdebut;
        this.mdebut = mdebut;
        this.hfin = hfin;
        this.mfin = mfin;
    }
 
    // toString
    public String toString() {
        return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
    }
 
    // foreign key
    public long getIdMedecin() {
        return idMedecin;
    }
 
    // setters - getters
    ...
}
  • السطر 10: الفئة هي كيان JPA؛
  • السطر 11: مرتبطة بجدول [CRENEAUX] في قاعدة البيانات؛
  • السطر 12: الكيان [Creneau] مشتق من الكيان [AbstractEntity] وبالتالي يرث الحقول [id] و [version
  • السطر 16: وقت بدء الفترة الزمنية (14)؛
  • السطر 17: دقائق بداية الفترة الزمنية (20)؛
  • السطر 18: ساعة انتهاء الفترة (14)؛
  • السطر 19: دقائق نهاية الفترة (40)؛
  • الأسطر 22-24: الطبيب الذي يمتلك الفترة الزمنية. يحتوي الجدول [CRENEAUX] على مفتاح خارجي في الجدول [MEDECINS]. يتم تمثيل هذه العلاقة بالأسطر 22-24؛
  • الصف 22: تشير التعليقة التوضيحية [@ManyToOne] إلى علاقة متعددة إلى واحد (الفترات الزمنية إلى الطبيب). تشير السمة [fetch=FetchType.LAZY] إلى أنه عند طلب كيان [Slot] من سياق الاستمرارية ويجب استرداده من قاعدة البيانات، لا يتم إرجاع كيان [Doctor] معه. ميزة هذا الوضع هي أن كيان [Doctor] لا يتم استرداده إلا إذا طلبه المطور. وهذا يوفر الذاكرة ويحسن الأداء؛
  • السطر 23: يحدد اسم عمود المفتاح الخارجي في جدول [CRENEAUX
  • السطران 27-28: المفتاح الخارجي في جدول [MEDECINS
  • السطر 27: تم استخدام عمود [ID_MEDECIN] بالفعل في السطر 23. وهذا يعني أنه يمكن تعديله بطريقتين مختلفتين، وهو ما لا يسمح به معيار JPA. ولذلك، نضيف السمتين [insertable = false, updatable = false]، لضمان أن يكون العمود للقراءة فقط؛

الكيان [Rv] هو كما يلي:


package rdvmedecins.entities;
 
import java.util.Date;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
 
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // characteristics of an Rv
    @Temporal(TemporalType.DATE)
    private Date jour;
 
    // an appointment is linked to a customer
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;
 
    // an appointment is linked to a time slot
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;
 
    // foreign keys
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;
 
    // default builder
    public Rv() {
    }
 
    // with parameters
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }
 
    // toString
    public String toString() {
        return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
    }
 
    // foreign keys
    public long getIdCreneau() {
        return idCreneau;
    }
 
    public long getIdClient() {
        return idClient;
    }
 
    // getters and setters
...
}
  • السطر 14: الفئة هي كيان JPA؛
  • السطر 15: مرتبطة بالجدول [RV] في قاعدة البيانات؛
  • السطر 16: الكيان [Rv] مشتق من الكيان [AbstractEntity] وبالتالي يرث الحقول [id] و [version
  • السطر 21: تاريخ الموعد؛
  • السطر 20: يحتوي نوع Java [Date] على التاريخ والوقت. هنا نحدد أنه يتم استخدام التاريخ فقط؛
  • الأسطر 24-26: العميل الذي تم تحديد هذا الموعد من أجله. يحتوي الجدول [RV] على مفتاح خارجي في الجدول [CLIENTS]. يتم تمثيل هذه العلاقة بالأسطر 24-26؛
  • الأسطر 29-31: الفترة الزمنية للموعد. يحتوي الجدول [RV] على مفتاح خارجي في الجدول [CRENEAUX]. يتم تمثيل هذه العلاقة بالأسطر 29-31؛
  • الصفوف 34-35: المفتاح الخارجي [idClient
  • الصفوف 36-37: المفتاح الخارجي [idCreneau

8.4.5. طبقة [DAO]

سنقوم بتنفيذ طبقة [DAO] باستخدام Spring Data:

  

يتم تنفيذ طبقة [DAO] باستخدام أربع واجهات Spring Data:

  • [ClientRepository]: توفر الوصول إلى كيانات [Client] JPA؛
  • [CreneauRepository]: توفر الوصول إلى كيانات JPA [Creneau
  • [MedecinRepository]: توفر الوصول إلى كيانات JPA [Medecin
  • [RvRepository]: توفر الوصول إلى كيانات JPA [Rv

واجهة [MedecinRepository] هي كما يلي:


package rdvmedecins.repositories;
 
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Medecin;
 
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
  • السطر 7: ترث واجهة [MedecinRepository] ببساطة الطرق من واجهة [CrudRepository] دون إضافة أي طرق أخرى؛

واجهة [ClientRepository] هي كما يلي:


package rdvmedecins.repositories;
 
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Client;
 
public interface ClientRepository extends CrudRepository<Client, Long> {
}
  • السطر 7: ترث واجهة [ClientRepository] ببساطة الطرق من واجهة [CrudRepository] دون إضافة أي طرق أخرى؛

واجهة [CreneauRepository] هي كما يلي:


package rdvmedecins.repositories;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Creneau;
 
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
    // liste des créneaux horaires d'un médecin
    @Query("select c from Creneau c where c.medecin.id=?1")
    Iterable<Creneau> getAllCreneaux(long idMedecin);
}
  • السطر 8: ترث واجهة [CreneauRepository] طرق واجهة [CrudRepository
  • السطران 10-11: تسترد طريقة [getAllCreneaux] الفترات الزمنية المتاحة للطبيب؛
  • السطر 11: المعلمة هي معرف الطبيب. والنتيجة هي قائمة بالمواعيد المتاحة في شكل كائن [Iterable<Creneau>]؛
  • السطر 10: تُستخدم العلامة [@Query] لتحديد استعلام JPQL (لغة استعلامات الاستمرارية في Java) الذي تنفذه الطريقة. وسيتم استبدال المعلمة [?1] بالمعلمة [idMedecin] الخاصة بالطريقة؛

واجهة [RvRepository] هي كما يلي:


package rdvmedecins.repositories;
 
import java.util.Date;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Rv;
 
public interface RvRepository extends CrudRepository<Rv, Long> {
 
    @Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
    Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
  • السطر 10: ترث واجهة [RvRepository] طرق واجهة [CrudRepository
  • السطران 12-13: تسترد طريقة [getRvMedecinJour] مواعيد الطبيب ليوم معين؛
  • السطر 13: المعلمات هي معرف الطبيب واليوم. والنتيجة هي قائمة بالمواعيد في شكل كائن [Iterable<Rv>]؛
  • السطر 12: تسمح لك العلامة [@Query] بتحديد استعلام JPQL الذي ينفذ الطريقة. سيتم استبدال المعلمة [?1] بمعلمة [idMedecin] الخاصة بالطريقة، وسيتم استبدال المعلمة [?2] بمعلمة [jour] الخاصة بالطريقة. استعلام JPQL التالي غير كافٍ:
select rv from Rv rv where rv.creneau.medecin.id=?1 and rv.jour=?2

لأن حقول فئة Rv، من الأنواع [Client] و [Creneau]، يتم استردادها في وضع [FetchType.LAZY]، مما يعني أنه يجب طلبها صراحةً للحصول عليها. ويتم ذلك في استعلام JPQL باستخدام صيغة [left join fetch entity]، التي تتطلب إجراء ربط مع الجدول المشار إليه بواسطة المفتاح الخارجي من أجل استرداد الكيان المشار إليه؛

8.4.6. طبقة [business]

  
  • [IMetier] هي واجهة طبقة [business]، و[Metier] هي تطبيقها؛
  • [Doctor'sDailySchedule] و [Doctor'sDailyTimeSlot] هما كيانان تجاريان؛

8.4.6.1. الكيانات

يربط كيان [CreneauMedecinJour] فترة زمنية بأي موعد محجوز ضمن تلك الفترة:


package rdvmedecins.domain;
 
import java.io.Serializable;
 
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
 
public class CreneauMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Creneau creneau;
    private Rv rv;
 
    // manufacturers
    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);
    }
 
    // getters and setters
...
}
  • السطر 12: الفترة الزمنية؛
  • السطر 13: الموعد، إن وجد – وإلا فسيكون فارغًا؛

الكيان [AgendaMedecinJour] هو جدول مواعيد الطبيب ليوم معين، أي قائمة مواعيده:


package rdvmedecins.domain;
 
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
 
import rdvmedecins.entities.Medecin;
 
public class AgendaMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Medecin medecin;
    private Date jour;
    private CreneauMedecinJour[] creneauxMedecinJour;
 
    // manufacturers
    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());
    }
 
    // getters and setters
...
}
  • السطر 13: الطبيب؛
  • السطر 14: اليوم في التقويم؛
  • السطر 15: أوقاتهم المتاحة، سواء كان هناك موعد أم لا؛

8.4.6.2. الخدمة

واجهة طبقة [الأعمال] هي كما يلي:


package rdvmedecins.metier;
 
import java.util.Date;
import java.util.List;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
 
public interface IMetier {
 
    // customer list
    public List<Client> getAllClients();
 
    // list of doctors
    public List<Medecin> getAllMedecins();
 
    // list of physician slots
    public List<Creneau> getAllCreneaux(long idMedecin);
 
    // list of doctor's appointments on a given day
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
 
    // find a customer identified by its id
    public Client getClientById(long id);
 
    // find a customer identified by its id
    public Medecin getMedecinById(long id);
 
    // find an Rv identified by its id
    public Rv getRvById(long id);
 
    // find a time slot identified by its id
    public Creneau getCreneauById(long id);
 
    // add a RV
    public Rv ajouterRv(Date jour, Creneau créneau, Client client);
 
    // delete a RV
    public void supprimerRv(Rv rv);
 
    // job
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
 
}

تشرح التعليقات دور كل طريقة.

تنفيذ واجهة [IMetier] هو فئة [Metier] التالية:


package rdvmedecins.metier;
 
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
 
import com.google.common.collect.Lists;
 
@Service("métier")
public class Metier implements IMetier {
 
    // repositories
    @Autowired
    private MedecinRepository medecinRepository;
    @Autowired
    private ClientRepository clientRepository;
    @Autowired
    private CreneauRepository creneauRepository;
    @Autowired
    private RvRepository rvRepository;
 
    // interface implementation
    @Override
    public List<Client> getAllClients() {
        return Lists.newArrayList(clientRepository.findAll());
    }
 
    @Override
    public List<Medecin> getAllMedecins() {
        return Lists.newArrayList(medecinRepository.findAll());
    }
 
    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
    }
 
    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
    }
 
    @Override
    public Client getClientById(long id) {
        return clientRepository.findOne(id);
    }
 
    @Override
    public Medecin getMedecinById(long id) {
        return medecinRepository.findOne(id);
    }
 
    @Override
    public Rv getRvById(long id) {
        return rvRepository.findOne(id);
    }
 
    @Override
    public Creneau getCreneauById(long id) {
        return creneauRepository.findOne(id);
    }
 
    @Override
    public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
        return rvRepository.save(new Rv(jour, client, créneau));
    }
 
    @Override
    public void supprimerRv(Rv rv) {
        rvRepository.delete(rv.getId());
    }
 
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
    ...
    }
 
}
  • السطر 24: التعليق التوضيحي [@Service] هو تعليق توضيحي لـ Spring يجعل الفئة المُعلَّمة مكونًا مُدارًا بواسطة Spring. يمكنك تسمية المكون أو عدم تسميته. هذا المكون يُسمى [business
  • السطر 25: تنفذ فئة [Metier] واجهة [IMetier
  • السطر 28: التعليق التوضيحي [@Autowired] هو تعليق توضيحي لـ Spring. سيتم تهيئة (حقن) قيمة الحقل المُعلَّم بهذه الطريقة بواسطة Spring مع الإشارة إلى مكون Spring من النوع أو الاسم المحدد. هنا، لا يحدد التعليق التوضيحي [@Autowired] اسمًا. لذلك، سيتم إجراء الحقن القائم على النوع؛
  • السطر 29: سيتم تهيئة الحقل [medecinRepository] بالإشارة إلى مكون Spring من النوع [MedecinRepository]. وستكون هذه هي الإشارة إلى الفئة التي أنشأتها Spring Data لتنفيذ واجهة [MedecinRepository] التي عرضناها سابقًا؛
  • الأسطر 30-35: تتكرر هذه العملية للواجهات الثلاث الأخرى التي تمت مناقشتها؛
  • الأسطر 39-41: تنفيذ طريقة [getAllClients
  • السطر 40: نستخدم طريقة [findAll] الخاصة بواجهة [ClientRepository]. تُرجع هذه الطريقة نوع [Iterable<Client>]، الذي نحوله إلى [List<Client>] باستخدام الطريقة الثابتة [Lists.newArrayList]. تم تعريف فئة [Lists] في مكتبة Google Guava. في [pom.xml]، تم استيراد هذه التبعية:

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
  • الأسطر 38–86: يتم تنفيذ أساليب واجهة [IMetier] باستخدام فئات من طبقة [DAO

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


public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        // list of doctor's time slots
        List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
        // list of bookings for the same doctor on the same day
        List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
        // a dictionary is created from the Rvs taken
        Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
        for (Rv resa : reservations) {
            hReservations.put(resa.getCreneau().getId(), resa);
        }
        // create the agenda for the requested day
        AgendaMedecinJour agenda = new AgendaMedecinJour();
        // the doctor
        agenda.setMedecin(getMedecinById(idMedecin));
        // the day
        agenda.setJour(jour);
        // reservation slots
        CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
        agenda.setCreneauxMedecinJour(creneauxMedecinJour);
        // filling reservation slots
        for (int i = 0; i < creneauxHoraires.size(); i++) {
            // line i agenda
            creneauxMedecinJour[i] = new CreneauMedecinJour();
            // time slot
            Creneau créneau = creneauxHoraires.get(i);
            long idCreneau = créneau.getId();
            creneauxMedecinJour[i].setCreneau(créneau);
            // is the slot free or reserved?
            if (hReservations.containsKey(idCreneau)) {
                // the slot is occupied - we note the resa
                Rv resa = hReservations.get(idCreneau);
                creneauxMedecinJour[i].setRv(resa);
            }
        }
        // we return the result
        return agenda;
    }

نشجع القراء على قراءة التعليقات. الخوارزمية هي كما يلي:

  • استرجاع جميع المواعيد المتاحة للطبيب المحدد؛
  • استرجاع جميع مواعيده في اليوم المحدد؛
  • باستخدام هاتين المعلومتين، يمكننا تحديد ما إذا كانت الفترة الزمنية متاحة أم محجوزة؛

8.4.7. تكوين مشروع Spring

  

تقوم فئة [DomainAndPersistenceConfig] بتكوين المشروع بأكمله:


package rdvmedecins.config;
 
import javax.persistence.EntityManagerFactory;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
 
@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {
 
    // JPA entity packages
    public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
 
    // the MySQL data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration JDBC
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
 
    // provider JPA is Hibernate
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }
 
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(ENTITIES_PACKAGES);
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • السطر 17: الفئة هي فئة تكوين Spring؛
  • السطر 18: الحزم التي تحتوي على واجهات Spring Data [CrudRepository]. سيتم إضافة هذه إلى سياق Spring؛
  • السطر 19: يضيف جميع الفئات في حزمة [rdvmedecins] وفئاتها الفرعية التي تحتوي على تعليق Spring إلى سياق Spring. في حزمة [rdvmedecins.metier]، سيتم العثور على فئة [Metier] مع تعليقها [@Service] وإضافتها إلى سياق Spring؛
  • الأسطر 26-39: تكوين تجمع اتصالات Tomcat JDBC (السطر 5)؛
  • السطر 36: سيحتوي تجمع الاتصالات على 5 اتصالات مفتوحة بشكل افتراضي. يظهر هذا السطر لأغراض توضيحية. في حالتنا، سيكون اتصال واحد كافياً. إذا كانت طبقة [DAO] ستُستخدم من قبل خيوط متعددة، فسيكون هذا السطر ضرورياً. سيكون هذا هو الحال لاحقاً، عندما تعمل طبقة [DAO] كأساس لتطبيق ويب يدعم بطبيعته خدمة عدة مستخدمين في وقت واحد؛
  • الأسطر 42-49: تطبيق JPA المستخدم هو تطبيق Hibernate؛
  • السطر 45: لا توجد سجلات SQL؛
  • السطر 46: لا توجد إعادة إنشاء للجداول؛
  • السطر 47: نظام إدارة قواعد البيانات المستخدم هو MySQL؛
  • الأسطر 53-61: تحدد EntityManagerFactory لطبقة JPA. من هذا الكائن، نحصل على كائن [EntityManager]، الذي يُستخدم لتنفيذ عمليات JPA؛
  • السطر 57: يحدد الحزمة (الحزم) التي توجد فيها كيانات JPA؛
  • السطر 58: يحدد مصدر البيانات الذي سيتم توصيله بطبقة JPA؛
  • الأسطر 64-69: مدير المعاملات المرتبط بـ EntityManagerFactory السابق. بشكل افتراضي، تعمل طرق واجهات [CrudRepository] في Spring Data ضمن معاملة. تبدأ المعاملة قبل الدخول إلى الطريقة وتكتمل (عبر التثبيت أو التراجع) بعد الخروج منها؛

8.4.8. اختبارات لطبقة [الأعمال]

  

فئة [rdvmedecins.tests.Metier] هي فئة اختبار Spring/JUnit 4:


package rdvmedecins.tests;
 
import java.text.ParseException;
import java.util.Date;
import java.util.List;
 
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
 
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
 
    @Autowired
    private IMetier métier;
 
    @Test
    public void test1(){
        // customer display
        List<Client> clients = métier.getAllClients();
        display("Liste des clients :", clients);
        // physician display
        List<Medecin> medecins = métier.getAllMedecins();
        display("Liste des médecins :", medecins);
        // display doctor's slots
        Medecin médecin = medecins.get(0);
        List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
        display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
        // list of doctor's appointments on a given day
        Date jour = new Date();
        display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // add a RV to the list
        Rv rv = null;
        Creneau créneau = 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, créneau,
            client));
        rv = métier.ajouterRv(jour, créneau, client);
        // check
        Rv rv2 = métier.getRvById(rv.getId());
        Assert.assertEquals(rv, rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // add a RV in the same slot on the same day
        // must trigger an exception
        System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
            client));
        Boolean erreur = false;
        try {
            rv = métier.ajouterRv(jour, créneau, client);
            System.out.println("Rv ajouté");
        } catch (Exception ex) {
            Throwable th = ex;
            while (th != null) {
                System.out.println(ex.getMessage());
                th = th.getCause();
            }
            // we note the error
            erreur = true;
        }
        // check for errors
        Assert.assertTrue(erreur);
        // RV list
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
        // calendar display
        AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
        System.out.println(agenda);
        Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
        // delete a RV
        System.out.println("Suppression du Rv ajouté");
        métier.supprimerRv(rv);
        // check
        rv2 = métier.getRvById(rv.getId());
        Assert.assertNull(rv2);
        display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
    }
 
    // utility method - displays items in a collection
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
 
}
  • السطر 22: تسمح علامة [@SpringApplicationConfiguration] باستخدام ملف التكوين [DomainAndPersistenceConfig] الذي تمت مناقشته سابقًا. وبالتالي، تستفيد فئة الاختبار من جميع الفاصوليا المحددة في هذا الملف؛
  • السطر 23: تتيح العلامة [@RunWith] تكامل Spring مع JUnit: يمكن تنفيذ الفئة كاختبار JUnit. [@RunWith] هي علامة JUnit (السطر 9)، في حين أن فئة [SpringJUnit4ClassRunner] هي فئة Spring (السطر 12)؛
  • السطران 26-27: حقن مرجع إلى طبقة [business] في فئة الاختبار؛
  • العديد من الاختبارات هي مجرد اختبارات بصرية:
    • السطران 32-33: قائمة العملاء؛
    • السطران 35-36: قائمة الأطباء؛
    • السطران 39-40: قائمة بمواعيد الطبيب؛
    • السطر 43: قائمة مواعيد الطبيب؛
  • السطر 50: إضافة موعد جديد. تُرجع طريقة [addAppt] الموعد مع معلومات إضافية، وهي معرف المفتاح الأساسي الخاص به؛
  • السطر 53: يُستخدم هذا المفتاح الأساسي للبحث عن الموعد في قاعدة البيانات؛
  • السطر 54: نتحقق من أن الموعد الذي يتم البحث عنه والموعد الذي تم العثور عليه متطابقان. تذكر أن طريقة [equals] للكيان [Rv] قد أعيد تعريفها: يكون الموعدان متطابقين إذا كان لهما نفس المعرف. هنا، يوضح لنا هذا أن الموعد المضاف قد تم إدراجه بالفعل في قاعدة البيانات؛
  • الأسطر 61–73: نحاول إضافة الموعد نفسه للمرة الثانية. يجب أن يرفض نظام إدارة قواعد البيانات (DBMS) ذلك بسبب وجود قيد التفرد:

CREATE TABLE IF NOT EXISTS `rv` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `JOUR` date NOT NULL,
  `ID_CLIENT` bigint(20) NOT NULL,
  `ID_CRENEAU` bigint(20) NOT NULL,
  `VERSION` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`ID`),
  UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
  KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
  KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;

يحدد السطر 8 أعلاه أن المجموعة [DAY, SLOT_ID] يجب أن تكون فريدة، مما يمنع جدولة موعدين في نفس الفترة الزمنية في نفس اليوم.

  • السطر 73: نتحقق من حدوث استثناء بالفعل؛
  • السطر 77: نسترد تقويم الطبيب الذي أضفنا له موعدًا للتو؛
  • السطر 79: نتحقق من أن الموعد المضاف موجود بالفعل في جدوله؛
  • السطر 82: حذف الموعد المضاف؛
  • السطر 84: استرداد الموعد المحذوف من قاعدة البيانات؛
  • السطر 85: نتحقق من أننا استرجعنا مؤشرًا فارغًا، مما يشير إلى أن الموعد الذي بحثنا عنه غير موجود؛

تم تنفيذ الاختبار بنجاح:

 

8.4.9. برنامج وحدة التحكم

  

برنامج وحدة التحكم بسيط. وهو يوضح كيفية استرداد مفتاح خارجي:


package rdvmedecins.boot;
 
import java.text.SimpleDateFormat;
import java.util.Date;
 
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
 
public class Boot {
    // the boot
    public static void main(String[] args) {
        // prepare the configuration
        SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
        app.setLogStartupInfo(false);
        // launch it
        ConfigurableApplicationContext context = app.run(args);
        // business
        IMetier métier = context.getBean(IMetier.class);
        try {
            // add a RV to the list
            Date jour = new Date();
            System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
            Client client = (Client) new Client().build(1L, 1L);
            Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
            Rv rv = métier.ajouterRv(jour, créneau, client);
            System.out.println(String.format("Rv ajouté = %s", rv));
            // check
            créneau = métier.getCreneauById(1L);
            long idMedecin = créneau.getIdMedecin();
            display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
        } catch (Exception ex) {
            System.out.println("Exception : " + ex.getCause());
        }
        // closing the Spring context
        context.close();
    }
 
    // utility method - displays items in a collection
    private static <T> void display(String message, Iterable<T> elements) {
        System.out.println(message);
        for (T element : elements) {
            System.out.println(element);
        }
    }
 
}

يقوم البرنامج بإضافة موعد ثم يتحقق من أنه قد تمت إضافته.

  • السطر 19: ستستخدم فئة [SpringApplication] فئة التكوين [DomainAndPersistenceConfig
  • السطر 20: إخفاء سجلات بدء تشغيل التطبيق؛
  • السطر 22: يتم تنفيذ فئة [SpringApplication]. وهي تُرجع سياق Spring، أي قائمة الفاصوليا المسجلة؛
  • السطر 24: يتم استرداد مرجع إلى الحبة التي تنفذ واجهة [IMetier]. وهذا بالتالي مرجع إلى طبقة [business
  • الأسطر 27–31: إضافة موعد جديد لليوم، للعميل رقم 1 في الفتحة رقم 1. تم إنشاء العميل والفتحة من الصفر لإثبات أنه يتم استخدام المعرفات فقط. قمنا بتهيئة الإصدار هنا، ولكن كان بإمكاننا استخدام أي قيمة. لا يتم استخدامه هنا؛
  • السطر 34: نريد معرفة الطبيب الذي لديه الفتحة رقم 1. للقيام بذلك، نحتاج إلى الاستعلام عن الفتحة رقم 1 في قاعدة البيانات. نظرًا لأننا في وضع [FetchType.LAZY]، لا يتم إرجاع الطبيب مع الفتحة. ومع ذلك، حرصنا على تضمين حقل [idMedecin] في كيان [Creneau] لاسترداد المفتاح الأساسي للطبيب؛
  • السطر 35: نسترد المفتاح الأساسي للطبيب؛
  • السطر 36: نعرض قائمة مواعيد الطبيب؛

إخراج وحدة التحكم كما يلي:

1
2
3
4
Ajout d'un Rv le [10/06/2014] dans le créneau 1 pour le client 1
Rv ajouté = Rv[113, Tue Jun 10 16:51:01 CEST 2014, 1, 1]
Liste des rendez-vous
Rv[113, 2014-06-10, 1, 1]

8.4.10. إدارة السجلات

يتم تكوين سجلات وحدة التحكم عبر ملفين: [application.properties] و [logback.xml] [1]:

يستخدم إطار عمل Spring Boot ملف [application.properties]. ويتيح لك هذا الملف تعريف مجموعة واسعة من الإعدادات لتجاوز القيم الافتراضية التي يستخدمها Spring Boot (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). وفيما يلي محتواه:


logging.level.org.hibernate=OFF
spring.main.show-banner=false
  • السطر 1: يتحكم في مستوى تسجيل Hibernate — هنا، لا توجد سجلات
  • السطر 2: يتحكم في عرض شعار Spring Boot — هنا، لا يوجد شعار

ملف [logback.xml] هو ملف التكوين لإطار عمل التسجيل [logback] [2]:


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- log level control -->
        <root level="info"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>
  • يتم التحكم في مستوى السجل العام من خلال السطر 9 — هنا، سجلات مستوى [info

ينتج عن ذلك النتيجة التالية:

1
2
3
4
5
6
7
14:20:35.634 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@345965f2: startup date [Wed Oct 14 14:20:35 CEST 2015]; root of context hierarchy
14:20:36.118 [main] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
Ajout d'un Rv le [14/10/2015] dans le créneau 1 pour le client 1
Rv ajouté = Rv[191, Wed Oct 14 14:20:38 CEST 2015, 1, 1]
Liste des rendez-vous
Rv[191, 2015-10-14, 1, 1]
14:20:38.211 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@345965f2: startup date [Wed Oct 14 14:20:35 CEST 2015]; root of context hierarchy

إذا قمنا بتعيين مستوى تسجيل Hibernate إلى [info] (دون تغيير أي شيء آخر):


logging.level.org.hibernate=INFO
spring.main.show-banner=false

ينتج عن هذا النتيجة التالية:

10:33:12.198 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5a4aa2f2: startup date [Wed Oct 14 10:33:12 CEST 2015]; root of context hierarchy
10:33:12.681 [main] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
10:33:12.702 [main] INFO  o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
10:33:12.773 [main] INFO  org.hibernate.Version - HHH000412: Hibernate Core {4.3.11.Final}
10:33:12.775 [main] INFO  org.hibernate.cfg.Environment - HHH000206: hibernate.properties not found
10:33:12.776 [main] INFO  org.hibernate.cfg.Environment - HHH000021: Bytecode provider name : javassist
10:33:13.011 [main] INFO  o.h.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
10:33:13.434 [main] INFO  org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
10:33:13.621 [main] INFO  o.h.h.i.a.ASTQueryTranslatorFactory - HHH000397: Using ASTQueryTranslatorFactory
Ajout d'un Rv le [14/10/2015] dans le créneau 1 pour le client 1
Rv ajouté = Rv[181, Wed Oct 14 10:33:14 CEST 2015, 1, 1]
Liste des rendez-vous
Rv[181, 2015-10-14, 1, 1]
10:33:14.782 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5a4aa2f2: startup date [Wed Oct 14 10:33:12 CEST 2015]; root of context hierarchy

إذا قمنا بتعيين مستوى السجل إلى [debug] (دون تغيير أي شيء آخر):


logging.level.org.hibernate=DEBUG
spring.main.show-banner=false

ينتج عن هذا النتيجة التالية:


10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'clientRepository' to allow for resolving potential circular references
10:35:13.522 [main] DEBUG o.s.b.f.annotation.InjectionMetadata - Processing injected element of bean 'clientRepository': PersistenceElement for public void org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.setEntityManager(javax.persistence.EntityManager)
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'entityManagerFactory'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'jpaMappingContext'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name 'clientRepository'
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$ThreadBoundTargetSource@723ed581
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [org.springframework.data.jpa.repository.support.SimpleJpaRepository@796065aa]
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'clientRepository'
10:35:13.522 [main] DEBUG o.s.b.f.a.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'métier' to bean named 'clientRepository'
...

8.4.11. طبقة [الويب / JSON]

  

سنقوم ببناء طبقة [الويب / JSON] على عدة خطوات:

  • الخطوة 1: طبقة ويب تشغيلية بدون مصادقة؛
  • الخطوة 2: تنفيذ المصادقة باستخدام Spring Security؛
  • الخطوة 3: تطبيق CORS [مشاركة الموارد عبر الأصول (CORS) هي آلية تسمح بطلب العديد من الموارد (مثل الخطوط وJavaScript وغيرها) الموجودة على صفحة ويب من نطاق آخر خارج النطاق الذي نشأت منه المورد. (ويكيبيديا)]. سيكون عميل خدمة الويب لدينا هو عميل ويب Angular الذي لا ينتمي بالضرورة إلى نفس المجال الذي تنتمي إليه خدمة الويب الخاصة بنا. بشكل افتراضي، لا يمكنه الوصول إليها ما لم تصرح له خدمة الويب بذلك. سنرى كيف؛

8.4.11.1. تكوين Maven

ملف [pom.xml] الخاص بالمشروع هو كما يلي:


<?xml version="1.0" encoding="UTF-8"?>
<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.spring4.mvc</groupId>
        <artifactId>rdvmedecins-webjson-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
 
        <name>rdvmedecins-webjson-server</name>
        <description>Gestion de RV Médecins</description>
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
        </parent>
        <dependencies>
                <!-- spring mvc web layer -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <!-- test layer -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <!-- layer DAO -->
                <dependency>
                        <groupId>istia.st.spring4.rdvmedecins</groupId>
                        <artifactId>rdvmedecins-metier-dao</artifactId>
                        <version>0.0.1-SNAPSHOT</version>
                </dependency>
        </dependencies>
...
</project>
  • الأسطر 12–15: مشروع Maven الأصلي؛
  • الأسطر 19–22: التبعيات لمشروع Spring MVC؛
  • الأسطر 24–28: التبعيات لاختبارات JUnit/Spring؛
  • الأسطر 30–34: التبعيات على طبقات المشروع [منطق الأعمال، DAO، JPA

8.4.11.2. واجهة خدمة الويب

  • في [1] أعلاه، لا يمكن للمتصفح طلب سوى عدد محدود من عناوين URL ذات صيغة محددة؛
  • في [4]، يتلقى استجابة JSON؛

ستكون جميع الاستجابات الواردة من خدمة الويب الخاصة بنا بنفس التنسيق، بما يتوافق مع تمثيل JSON لكائن من النوع [Response] على النحو التالي:


package rdvmedecins.web.models;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}
  • السطر 7: رمز خطأ الاستجابة 0: OK، أي شيء آخر: KO؛
  • السطر 11: قائمة برسائل الخطأ، في حالة وجود خطأ؛
  • السطر 13: نص الاستجابة؛

نقدم الآن لقطات الشاشة التي توضح واجهة خدمة الويب / JSON:

قائمة بجميع المرضى في العيادة الطبية [/getAllClients]

قائمة بجميع الأطباء في العيادة [/getAllMedecins]

قائمة المواعيد المتاحة للطبيب [/getAllCreneaux/{idMedecin}]

قائمة مواعيد الطبيب [/getRvMedecinJour/{idMedecin}/{yyyy-mm-dd}

جدول أعمال الطبيب اليومي [/getAgendaMedecinJour/{idMedecin}/{yyyy-mm-dd}]

لإضافة أو حذف موعد، نستخدم ملحق Chrome [Advanced Rest Client] لأن هذه العمليات تتم باستخدام طلب POST.

إضافة موعد [/addAppointment]

  • في [0]، عنوان URL لخدمة الويب؛
  • في [1]، يتم استخدام طريقة POST؛
  • في [2]، نص JSON للمعلومات المرسلة إلى خدمة الويب بالتنسيق {day, clientId, slotId
  • في [3]، يحدد العميل لخدمة الويب أنه يرسل المعلومات بتنسيق JSON؛

ثم يكون الرد كما يلي:

  • في [4]: يرسل العميل الرأس مشيرًا إلى أن البيانات التي يرسلها بتنسيق JSON؛
  • في [5]: ترد خدمة الويب بأنها ترسل JSON أيضًا؛
  • في [6]: استجابة خدمة الويب بتنسيق JSON. يحتوي حقل [body] على تمثيل JSON للموعد المضاف؛

يمكن التحقق من وجود الموعد الجديد:

لاحظ رقم المعاينة [50]. سنقوم بحذف هذه المعاينة.

حذف موعد [/deleteApp]

  • في [1]، عنوان URL لخدمة الويب؛
  • في [2]، يتم استخدام طريقة POST؛
  • في [3]، نص JSON للمعلومات المرسلة إلى خدمة الويب في النموذج {idRv
  • في [4]، يحدد العميل لخدمة الويب أنه يرسل بيانات JSON؛

ثم يكون الرد كما يلي:

  • في [5]: يتم تعيين حقل [status] إلى 0، مما يشير إلى نجاح العملية؛

يمكن التحقق من حذف الموعد:

في الأعلى، لم يعد موعد المريضة [السيدة جيرمين] موجودًا.

تسمح الخدمة الإلكترونية أيضًا بالبحث عن الكيانات باستخدام معرّفها:

تتم معالجة جميع عناوين URL هذه بواسطة وحدة التحكم [RdvMedecinsController]، والتي سنقدمها بعد قليل.

8.4.11.3. تكوين خدمة الويب

  

فئة التكوين [AppConfig] هي كما يلي:


package rdvmedecins.web.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
 
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
 
}
  • السطر 12: تقوم فئة [AppConfig] بتكوين التطبيق بأكمله؛
  • السطر 9: فئة [AppConfig] هي فئة تكوين Spring؛
  • السطر 10: نحدد أنه يجب البحث عن مكونات Spring في الحزمة [rdvmedecins.web] وحزمها الفرعية. هكذا سيتم اكتشاف المكونات التالية:
    • [@RestController RdvMedecinsController] في حزمة [rdvmedecins.web.controllers
    • [@Component ApplicationModel] في حزمة [rdvmedecins.web.models
  • السطر 11: نستورد فئة [DomainAndPersistenceConfig]، التي تهيئ مشروع [rdvmedecins-metier-dao] لتوفير الوصول إلى حبوب ذلك المشروع؛
  • السطر 11: تقوم فئة [SecurityConfig] بتكوين أمان تطبيق الويب. سنتجاهلها في الوقت الحالي؛
  • السطر 11: تقوم فئة [WebConfig] بتكوين طبقة [web / JSON

فئة [WebConfig] هي كما يلي:


package rdvmedecins.web.config;
 
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
@Configuration
@EnableWebMvc
public class WebConfig {
 
    // dispatcherservlet configuration for CORS headers
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
 
    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }
 
    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8080);
    }
 
    // mappers jSON
    @Bean
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
    }
 
    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(
                new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
        return jsonMapperLongRv;
    }
 
    @Bean
    public ObjectMapper jsonMapperShortRv() {
        ObjectMapper jsonMapperShortRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
        jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
        return jsonMapperShortRv;
    }
 
}
  • الأسطر 20–25: تعريف حبة [dispatcherServlet]. فئة [DispatcherServlet] هي السيرفلت الخاص بإطار عمل Spring MVC. وهي تعمل كـ [FrontController]: فهي تعترض الطلبات المرسلة إلى موقع Spring MVC وتوجهها إلى أحد وحدات التحكم في الموقع؛
  • السطر 22: إنشاء مثيل للفئة؛
  • السطر 23: يمكن تجاهل هذا السطر في الوقت الحالي؛
  • الأسطر 27–30: تتولى خدمة [dispatcherServlet] معالجة جميع عناوين URL؛
  • الأسطر 27-30: تنشيط خادم Tomcat المضمن في تبعيات المشروع. سيتم تشغيله على المنفذ 8080؛
  • الأسطر 38-67: أربعة مخططات JSON تم تكوينها باستخدام مرشحات JSON مختلفة؛
  • الأسطر 38-41: مخطط JSON بدون مرشحات؛
  • الأسطر 43-49: يقوم مخطط JSON [jsonMapperShortCreneau] بتسلسل/إلغاء تسلسل كائن [Creneau] مع تجاهل الحقل [Creneau.medecin
  • الأسطر 51–59: يقوم مخطط JSON [jsonMapperLongRv] بتسلسل/إلغاء تسلسل كائن [Rv] مع تجاهل الحقل [Rv.creneau.medecin
  • الأسطر 61-67: يقوم مخطط JSON [jsonMapperShortRv] بتسلسل/إلغاء تسلسل كائن [Rv] مع تجاهل الحقلين [Rv.creneau] و [Rv.client

8.4.11.4. فئة [ApplicationModel]

  

ستخدم فئة [ApplicationModel] لغرضين:

  • كذاكرة مؤقتة لتخزين قوائم الأطباء والمرضى (العملاء)؛
  • كواجهة واحدة لوحدات التحكم؛

package rdvmedecins.web.models;
 
import java.util.Date;
import java.util.List;
 
import javax.annotation.PostConstruct;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;
 
@Component
public class ApplicationModel implements IMetier {
 
    // the [business] layer
    @Autowired
    private IMetier métier;
 
    // data from the [business] layer
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    // configuration data
    private boolean CORSneeded = false;
    private boolean secured = false;
 
    @PostConstruct
    public void init() {
        // we get the doctors and the customers
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
    }
 
    // getter
    public List<String> getMessages() {
        return messages;
    }
 
    // ------------------------- [business] layer interface
    @Override
    public List<Client> getAllClients() {
        return clients;
    }
 
    @Override
    public List<Medecin> getAllMedecins() {
        return médecins;
    }
 
    @Override
    public List<Creneau> getAllCreneaux(long idMedecin) {
        return métier.getAllCreneaux(idMedecin);
    }
 
    @Override
    public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
        return métier.getRvMedecinJour(idMedecin, jour);
    }
 
    @Override
    public Client getClientById(long id) {
        return métier.getClientById(id);
    }
 
    @Override
    public Medecin getMedecinById(long id) {
        return métier.getMedecinById(id);
    }
 
    @Override
    public Rv getRvById(long id) {
        return métier.getRvById(id);
    }
 
    @Override
    public Creneau getCreneauById(long id) {
        return métier.getCreneauById(id);
    }
 
    @Override
    public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
        return métier.ajouterRv(jour, creneau, client);
    }
 
    @Override
    public void supprimerRv(long idRv) {
        métier.supprimerRv(idRv);
    }
 
    @Override
    public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
        return métier.getAgendaMedecinJour(idMedecin, jour);
    }
 
     // getters and setters
public boolean isCORSneeded() {
        return CORSneeded;
    }
 
    public boolean isSecured() {
        return secured;
    }
 
}
  • السطر 19: تجعل العلامة [@Component] فئة [ApplicationModel] مكونًا في Spring. مثل جميع مكونات Spring التي رأيناها حتى الآن (باستثناء @Controller)، سيتم إنشاء مثيل واحد فقط من هذا النوع (singleton
  • السطر 20: تنفذ فئة [ApplicationModel] واجهة [IMetier
  • السطران 23-24: يتم إدخال مرجع إلى طبقة [business] بواسطة Spring؛
  • السطر 34: تضمن علامة [@PostConstruct] أن يتم تنفيذ طريقة [init] فور إنشاء مثيل لفئة [ApplicationModel
  • السطران 38-39: استرداد قوائم الأطباء والعملاء من طبقة [business
  • السطر 41: في حالة حدوث استثناء، يتم تخزين الرسائل من مكدس الاستثناءات في الحقل الموجود في السطر 17؛

تتطور بنية طبقة الويب على النحو التالي:

  • في [2b]، تتواصل أساليب وحدة (وحدات) التحكم مع العنصر الفريد [ApplicationModel

توفر هذه الاستراتيجية مرونة في إدارة ذاكرة التخزين المؤقت. حاليًا، لا يتم تخزين فترات مواعيد الأطباء في ذاكرة التخزين المؤقت. لتخزينها، ما عليك سوى تعديل فئة [ApplicationModel]. لا يؤثر هذا على وحدة التحكم، التي ستستمر في استخدام طريقة [List<Creneau> getAllCreneaux(long idMedecin)] كما كانت تفعل من قبل. ما سيتغير هو تنفيذ هذه الطريقة في [ApplicationModel].

8.4.11.5. الفئة الثابتة

تحتوي فئة [Static] على مجموعة من طرق المساعدة الثابتة التي لا تحتوي على جوانب "أعمال" أو "ويب":

  

والكود الخاص به هو كما يلي:


package rdvmedecins.web.helpers;
 
import java.util.ArrayList;
import java.util.List;
 
public class Static {
 
    public Static() {
    }
 
    // list of exception error messages
    public static List<String> getErreursForException(Exception exception) {
        // retrieve the list of exception error messages
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            erreurs.add(cause.getMessage());
            cause = cause.getCause();
        }
        return erreurs;
    }
}
  • السطر 12: طريقة [Static.getErrorsForException] التي تم استخدامها (السطر 8 أدناه) في طريقة [init] لفئة [ApplicationModel]:

    @PostConstruct
    public void init() {
        // we get the doctors and the customers
        try {
            médecins = métier.getAllMedecins();
            clients = métier.getAllClients();
        } catch (Exception ex) {
            messages = Static.getErreursForException(ex);
        }
}

تقوم هذه الطريقة بإنشاء كائن [List<String>] يحتوي على رسائل الخطأ [exception.getMessage()] الخاصة باستثناء [exception] ورسائل الخطأ الخاصة باستثناءه الداخلي [exception.getCause()].

8.4.11.6. هيكل وحدة التحكم [RdvMedecinsController]

  

سنشرح الآن بالتفصيل كيفية التعامل مع عناوين URL لخدمة الويب. تشارك ثلاث فئات رئيسية في هذه العملية:

  • وحدة التحكم [RdvMedecinsController
  • فئة طرق المساعدة [Static
  • فئة ذاكرة التخزين المؤقت [ApplicationModel
  

وحدة التحكم [RdvMedecinsController] هي كما يلي:


package rdvmedecins.web.controllers;
 
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;
 
@Controller
public class RdvMedecinsController {
 
    @Autowired
    private ApplicationModel application;
 
    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;
 
    // message list
    private List<String> messages;
 
    // mappers jSON
    @Autowired
    private ObjectMapper jsonMapper;
 
    @Autowired
    private ObjectMapper jsonMapperShortCreneau;
 
    @Autowired
    private ObjectMapper jsonMapperLongRv;
 
    @Autowired
    private ObjectMapper jsonMapperShortRv;
 
    @PostConstruct
    public void init() {
        // application error messages
        messages = application.getMessages();
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {...}
 
    // customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllClients() throws JsonProcessingException {...}
 
    // list of physician slots
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
 
    // list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
                    throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}
 
    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
                    throws JsonProcessingException {...}
 
    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {...}
}
  • السطر 35: تجعل العلامة [@Controller] فئة [RdvMedecinsController] وحدة تحكم Spring، أي C في MVC؛
  • السطران 38–39: سيتم حقن كائن من النوع [ApplicationModel] هنا بواسطة Spring. وقد قدمنا هذا الكائن سابقًا؛
  • السطران 41-42: سيتم حقن كائن من النوع [RdvMedecinsCorsController] هنا بواسطة Spring. سنقدم هذا الكائن لاحقًا؛
  • الأسطر 48–58: مخططات JSON المحددة في فئة التكوين [WebConfig
  • السطر 60: تشير العلامة [@PostConstruct] إلى طريقة يجب تنفيذها فور إنشاء مثيل للفئة. عند تشغيل هذه الطريقة، تكون الكائنات التي تم إدخالها بواسطة Spring متاحة؛
  • السطر 63: نسترد أي رسائل خطأ من الكائن [ApplicationModel]. تم إنشاء مثيل لهذا الكائن عند بدء تشغيل التطبيق ومحاولته تخزين الأطباء والعملاء مؤقتًا. إذا فشل ذلك، فإن [messages!=null]. سيسمح هذا لطرق وحدة التحكم بتحديد ما إذا كان التطبيق قد تم تهيئته بشكل صحيح؛
  • الأسطر 67–118: عناوين URL التي تعرضها خدمة [web/jSON]. ترجع جميع الطرق سلسلة JSON من النوع [Response<T>] التالي:
 

package rdvmedecins.web.models;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}
  • السطر 9: رمز خطأ: 0 يعني عدم وجود خطأ؛
  • السطر 11: إذا كان [status!=0]، فإن [messages] هي قائمة برسائل الخطأ؛
  • السطر 13: كائن T مغلف في الاستجابة. يكون T فارغًا في حالة وجود خطأ؛

يتم تسلسل هذا الكائن إلى JSON قبل إرساله إلى متصفح العميل؛

  • السطر 67: عنوان URL المعروض هو [/getAllDoctors]. يجب على العميل استخدام طريقة [GET] لتقديم طلبه (method = RequestMethod.GET). إذا تم طلب عنوان URL هذا عبر POST، فسيتم رفضه وسيقوم Spring MVC بإرسال رمز خطأ HTTP إلى عميل الويب. تعيد الطريقة نفسها الاستجابة إلى العميل (السطر 68). ستكون هذه سلسلة (السطر 67). سيتم إرسال رأس HTTP [Content-type: application/json; charset=UTF-8] إلى العميل للإشارة إلى أنه سيتلقى سلسلة JSON (السطر 67)؛
  • السطر 77: تم تكوين عنوان URL باستخدام {idMedecin}. يتم استرداد هذه المعلمة باستخدام التعليق التوضيحي [@PathVariable] في السطر 79؛
  • السطر 79: تحصل المعلمة [long idMedecin] على قيمتها من المعلمة {idMedecin} في عنوان URL [@PathVariable("idMedecin")]. يمكن أن يكون للمعلمة في عنوان URL وتلك الموجودة في الطريقة اسم ان مختلفان. لاحظ أن [@PathVariable("idMedecin")] من النوع String (عنوان URL بأكمله هو String)، في حين أن المعلمة [long idMedecin] من النوع [long]. يتم إجراء تحويل النوع تلقائيًا. يتم إرجاع رمز خطأ HTTP إذا فشل تحويل النوع هذا؛
  • السطر 105: تشير العلامة [@RequestBody] إلى نص الطلب. في طلب GET، لا يوجد نص أبدًا تقريبًا (ولكن من الممكن تضمينه). في طلب POST، عادةً ما يكون هناك نص (ولكن من الممكن حذفه). بالنسبة لعنوان URL [ajouterRv]، يرسل عميل الويب سلسلة JSON التالية في طلب POST الخاص به:
{"jour":"2014-06-12", "idClient":3, "idCreneau":7}

إن بناء الجملة [@RequestBody PostAjouterRv post] (السطر 105)، مقترناً بحقيقة أن الطريقة تتوقع JSON [consumes = "application/json; charset=UTF-8"] (السطر 103)، يعني أن سلسلة JSON المرسلة من قبل عميل الويب سيتم تحويلها إلى كائن من النوع [PostAjouterRv]. وهذا كما يلي:


package rdvmedecins.web.models;
 
public class PostAjouterRv {
 
    // pOST DATA
    private String jour;
    private long idClient;
    private long idCreneau;
 
    // getters and setters
    ...
}

هنا أيضًا، ستحدث تحويلات الأنواع الضرورية تلقائيًا؛

  • تحتوي الأسطر 107–109 على آلية مماثلة لعنوان URL [/supprimerRv]. سلسلة JSON المرسلة هي كما يلي:
{"idRv":116}

ونوع [PostSupprimerRv] هو كما يلي:


package rdvmedecins.web.models;
 
public class PostSupprimerRv {
 
    // pOST DATA
    private long idRv;
 
    // getters and setters
    ...
}

8.4.11.7. عنوان URL [/getAllDoctors]

يتم التعامل مع عنوان URL [/getAllMedecins] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:


// list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins() throws JsonProcessingException {
        // the answer
        Response<List<Medecin>> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // list of doctors
            try {
                response = new Response<>(0, null, application.getAllMedecins());
            } catch (RuntimeException e) {
                response = new Response<>(1, Static.getErreursForException(e), null);
            }
        }
        // answer
        return jsonMapper.writeValueAsString(response);
    }
  • السطران 9-10: نتحقق مما إذا كان التطبيق قد تم تهيئته بشكل صحيح (messages==null). إذا لم يكن الأمر كذلك، فإننا نُرجع استجابة بقيمة status=-1 و body=messages؛
  • السطر 13: خلاف ذلك، نطلب قائمة الأطباء من فئة [ApplicationModel
  • السطر 19: نرسل سلسلة JSON للرد باستخدام أداة تعيين JSON [jsonMapper] لأن فئة [Medecin] لا تحتوي على مرشح JSON. قد يكون الرد خاليًا من الأخطاء (السطر 14) أو يحتوي على خطأ (السطر 16). لا ترمي الطريقة [application.getAllMedecins()] استثناءً لأنها تعيد ببساطة قائمة مخزنة مؤقتًا. ومع ذلك، سنحتفظ بمعالجة الاستثناء هذه في حالة عدم وجود الأطباء في ذاكرة التخزين المؤقت بعد الآن؛

لم نوضح بعد الحالة التي يتم فيها تهيئة التطبيق بشكل غير صحيح. دعونا نوقف نظام إدارة قواعد البيانات MySQL5، ونبدأ خدمة الويب، ثم نطلب عنوان URL [/getAllMedecins]:

Image

نحصل بالفعل على خطأ. في الظروف العادية، نحصل على العرض التالي:

8.4.11.8. عنوان URL [/getAllClients]

يتم التعامل مع عنوان URL [/getAllClients] بواسطة الطريقة التالية في [RdvMedecinsController]:


// customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllClients() throws JsonProcessingException {
        // the answer
        Response<List<Client>> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        }
        // customer list
        try {
            response = new Response<>(0, null, application.getAllClients());
        } catch (RuntimeException e) {
            response = new Response<>(1, Static.getErreursForException(e), null);
        }
        // answer
        return jsonMapper.writeValueAsString(response);
    }

وهي تشبه طريقة [getAllMedecins] التي تناولناها سابقًا. وفيما يلي النتائج التي تم الحصول عليها:

8.4.11.9. عنوان URL [/getAllSlots/{doctorId}]

يتم التعامل مع عنوان URL [/getAllSlots/{doctorId}] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:


// list of physician slots
    @RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
        // the answer
        Response<List<Creneau>> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        }
        // we get the doctor back
        Response<Medecin> responseMedecin = getMedecin(idMedecin);
        if (responseMedecin.getStatus() != 0) {
            response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
        } else {
            Medecin médecin = responseMedecin.getBody();
            // doctor's slots
            try {
                response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperShortCreneau.writeValueAsString(response);
    }
  • السطر 12: يتم طلب الطبيب المحدد بواسطة المعلمة [id] من طريقة محلية:

private Response<Medecin> getMedecin(long id) {
        // we get the doctor back
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (RuntimeException e1) {
            return new Response<Medecin>(1, Static.getErreursForException(e1), null);
        }
        // existing doctor?
        if (médecin == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
            return new Response<Medecin>(2, messages, null);
        }
        // ok
        return new Response<Medecin>(0, null, médecin);
    }

تُرجع هذه الطريقة قيمة حالة في النطاق [0,1,2]. لنعد إلى كود طريقة [getAllCreneaux]:

  • السطران 13-14: إذا كانت الحالة!=0، فإننا نبني استجابة تحتوي على خطأ؛
  • السطر 16: نسترد الطبيب؛
  • السطر 19: نسترد فترات زمنية لهذا الطبيب؛
  • السطر 25: نرسل كائن [List<Creneau>] كاستجابة. دعونا نستذكر تعريف فئة [Creneau]:

@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
    private long idMedecin;
...
}
  • السطر 13: يتم استرداد الطبيب في وضع [FetchType.LAZY

تذكر استعلام JPQL الذي ينفذ طريقة [getAllCreneaux] في طبقة [DAO]:


@Query("select c from Creneau c where c.medecin.id=?1")

تفرض صيغة [c.medecin.id] إجراء ربط بين جدولي [CRENEAUX] و[MEDECINS]. ونتيجة لذلك، يعرض الاستعلام جميع فترات عمل الطبيب، مع تضمين اسم الطبيب في كل منها. وعندما نقوم بتسلسل هذه الفترات إلى صيغة JSON، تظهر سلسلة JSON الخاصة بالطبيب في كل منها. وهذا أمر غير ضروري. للتحكم في عملية التسلسل، نحتاج إلى أمرين:

  1. الوصول إلى الكائن الذي يتم تسلسله؛
  2. تكوين الكائن المراد تسلسله؛

يتم التعامل مع النقطة 1 عن طريق إدخال محول JSON المناسب للكائن في وحدة التحكم:


@Autowired
private ObjectMapper jsonMapperShortCreneau;

يتم تحقيق النقطة 2 عن طريق إضافة تعليق توضيحي إلى فئة [Creneau] المحددة في مشروع [rdvmedecins-metier-dao]:

  

@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
  • السطر 3: تعليق توضيحي من مكتبة Jackson JSON. يقوم بإنشاء مرشح يسمى [creneauFilter]. باستخدام هذا المرشح، سنتمكن من تحديد الحقول التي يجب أو لا يجب تسلسلها برمجياً؛

يتم تسلسل كائن [Creneau] في السطر التالي من طريقة [getAllCreneaux]:


        // réponse
        return jsonMapperShortCreneau.writeValueAsString(response);

تم تعريف مخطط JSON [jsonMapperShortCreneau] في فئة [WebConfig] على النحو التالي:


    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
}
  • السطر 5: يتم ربط المرشح المسمى [creneauFilter] بمرشح [creneauFilter] من السطر 4. يقوم هذا المرشح بتسلسل كائن [Creneau] بدون حقل [medecin] الخاص به؛

النتيجة التي ترجعها طريقة [getAllCreneaux] هي سلسلة JSON من النوع [Response<List<Creneau>].

النتائج التي تم الحصول عليها هي كما يلي:

أو هذه إذا لم تكن الفتحة موجودة:

من هذا المثال، يمكننا استنتاج القاعدة التالية:

  • تُرجع طرق خادم الويب/JSON كائنًا من النوع [Response<T>] يتم تسلسله إلى JSON؛
  • إذا كان النوع T يحتوي على مرشح JSON واحد أو أكثر، فسيتم استخدام أداة تعيين تحتوي على تلك المرشحات نفسها لتسلسله؛

8.4.11.10. عنوان URL [/getRvMedecinJour/{idMedecin}/{jour}]

يتم التعامل مع عنوان URL [/getRvMedecinJour/{idMedecin}/{jour}] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:


// list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
                    throws JsonProcessingException {
        // the answer
        Response<List<Rv>> response=null;
        boolean erreur = false;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // check the date
        Date jourAgenda = null;
        if (!erreur) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<List<Rv>>(3, messages, null);
                erreur = true;
            }
        }
        Response<Medecin> responseMedecin = null;
        if (!erreur) {
            // we get the doctor back
            responseMedecin = getMedecin(idMedecin);
            if (responseMedecin.getStatus() != 0) {
                response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            Medecin médecin = responseMedecin.getBody();
            // list of appointments
            try {
                response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
            } catch (RuntimeException e1) {
                response = new Response<>(4, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • يجب أن نُرجع سلسلة JSON من النوع [Response<List<Rv>>]. تحتوي فئة [Rv] على حقل [Rv.creneau]. إذا تم تسلسل هذا الحقل، فسوف نواجه مرشح JSON [creneauFilter
  • السطر 47: يتم تسلسل الكائن من النوع [Response<List<Rv>>] من السطر 7 إلى JSON؛

دعونا ندرس الحالة التي تم فيها الحصول على قائمة المواعيد في السطر 42. يتم تعريف فئة [Rv] في مشروع [rdvmedecins-metier-dao] على النحو التالي:


@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // characteristics of an Rv
    @Temporal(TemporalType.DATE)
    private Date jour;
 
    // an appointment is linked to a customer
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_client")
    private Client client;
 
    // an appointment is linked to a time slot
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_creneau")
    private Creneau creneau;
 
    // foreign keys
    @Column(name = "id_client", insertable = false, updatable = false)
    private long idClient;
    @Column(name = "id_creneau", insertable = false, updatable = false)
    private long idCreneau;
 
...
 
}
  • السطر 11: يتم استرداد العميل باستخدام الوضع [FetchType.LAZY
  • السطر 18: يتم استرداد الفتحة باستخدام الوضع [FetchType.LAZY

دعونا نستذكر استعلام JPQL الذي يسترد المواعيد:


@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")

يتم تنفيذ عمليات الربط بشكل صريح لاسترداد الحقول [client] و [slot]. علاوة على ذلك، وبسبب عملية الربط [cr.doctor.id=?1]، سنحصل أيضًا على الطبيب. وبالتالي، سيظهر الطبيب في سلسلة JSON لكل موعد. ومع ذلك، فإن هذه المعلومات المكررة غير ضرورية. لقد رأينا كيفية حل هذه المشكلة باستخدام مرشح JSON على كائن [Creneau]. وبسبب أوضاع [FetchType.LAZY] لحقول [client] و [slot] في فئة [Rv]، سنكتشف قريبًا الحاجة إلى تطبيق مرشح JSON على فئة [RV] في مشروع [rdvmedecins-metier-dao]:


@Entity
@Table(name = "rv")
@JsonFilter("rvFilter")
public class Rv extends AbstractEntity {
...

سنقوم بالتحكم في تسلسل كائن [Rv] باستخدام مرشح [rvFilter]. على ما يبدو، في هذه الحالة، لا نحتاج إلى التصفية لأننا نحتاج إلى جميع حقول كائن [Rv]. ومع ذلك، نظرًا لأننا حددنا أن الفئة تحتوي على مرشح JSON، يجب علينا تعريفه لأي تسلسل لكائن من النوع [Rv]؛ وإلا، فسوف نحصل على استثناء. للقيام بذلك، نستخدم مخطط JSON التالي المحدد في فئة [rdvMedecinsController]:


    @Autowired
    private ObjectMapper jsonMapperLongRv;

يتم تعريف هذا المُعِد كما يلي في فئة التكوين [WebConfig]:


    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
        return jsonMapperLongRv;
}
  • السطر 4: نحدد أن جميع حقول كائن [Rv] يجب تسلسلها؛
  • السطر 5: نحدد أنه في كائن [Creneau]، يجب ألا يتم تسلسل الحقل [medecin
  • السطر 6: نضيف المرشحين [rvFilter] و [creneauFilter] إلى مرشحات JSON للكائن [jsonMapperLongRv

النتائج التي تم الحصول عليها هي كما يلي:

أو هذه النتائج في يوم بدون مواعيد:

أو هذه التي تحتوي على يوم غير صحيح:

أو هذه التي تحتوي على اسم طبيب غير صحيح:

8.4.11.11. عنوان URL [/getAgendaMedecinJour/{idMedecin}/{jour}]

يتم التعامل مع عنوان URL [/getAgendaMedecinJour/{idMedecin}/{jour}] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
                    throws JsonProcessingException {
        // the answer
        Response<AgendaMedecinJour> response = null;
        boolean erreur = false;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // check the date
        Date jourAgenda = null;
        if (!erreur) {
            // check the date
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                erreur = true;
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<>(3, messages, null);
            }
        }
        // we get the doctor back
        Medecin médecin = null;
        if (!erreur) {
            // we get the doctor back
            Response<Medecin> responseMedecin = getMedecin(idMedecin);
            if (responseMedecin.getStatus() != 0) {
                response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
            } else {
                médecin = responseMedecin.getBody();
            }
        }
        // get your diary back
        if (!erreur) {
            try {
                response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
            } catch (RuntimeException e1) {
                erreur = true;
                response = new Response<>(4, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • السطران 6 و49: نُرجع سلسلة JSON من النوع [AgendaMedecinJour] مغلفة في كائن [Response

نوع [AgendaMedecinJour] هو كما يلي:


public class AgendaMedecinJour implements Serializable {
    // fields
    private Medecin medecin;
    private Date jour;
   private CreneauMedecinJour[] creneauxMedecinJour;

يكون النوع [CreneauMedecinJour] على النحو التالي:


public class CreneauMedecinJour implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // fields
    private Creneau creneau;
   private Rv rv;

تحتوي الحقول [creneau] و [rv] على مرشحات JSON تحتاج إلى التهيئة. وهذا ما تقوم به السطر 49 من طريقة [getAgendaMedecinJour]، باستخدام أداة تعيين JSON [jsonMapperLongRv] التي صادفناها سابقًا:


    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(
                new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
        return jsonMapperLongRv;
}

النتائج التي تم الحصول عليها هي كما يلي:

فيما سبق، نرى أن الدكتور بيليسييه لديه موعد مع السيدة بريجيت بيسترو في 28/01/2015 الساعة 8:20 صباحاً؛

أو هذه إذا كان التاريخ غير صحيح:

أو هذه إذا كان رقم تعريف الطبيب غير صالح:

8.4.11.12. عنوان URL [/getMedecinById/{id}]

يتم التعامل مع عنوان URL [/getMedecinById/{id}] بواسطة الطريقة التالية في [RdvMedecinsController]:


    @RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Medecin> response;
        // application status
        if (messages != null) {
            response = new Response<Medecin>(-1, messages, null);
        } else {
            response = getMedecin(id);
        }
        // answer
        return jsonMapper.writeValueAsString(response);
}
  • السطران 5 و 13: تُرجع الطريقة سلسلة JSON من النوع [Doctor]. لا يحتوي هذا النوع على تعليق توضيحي لمرشح JSON. لذلك، في السطر 14، يتم استخدام مُعِد خرائط JSON بدون مرشحات؛

السطر 10: طريقة [getMedecin] هي كما يلي:


    private Response<Medecin> getMedecin(long id) {
        // we get the doctor back
        Medecin médecin = null;
        try {
            médecin = application.getMedecinById(id);
        } catch (RuntimeException e1) {
            return new Response<Medecin>(1, Static.getErreursForException(e1), null);
        }
        // existing doctor?
        if (médecin == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
            return new Response<Medecin>(2, messages, null);
        }
        // ok
        return new Response<Medecin>(0, null, médecin);
}

النتائج هي كما يلي:

أو هذه إذا كان رقم تعريف الطبيب غير صحيح:

8.4.11.13. عنوان URL [/getClientById/{id}]

يتم التعامل مع عنوان URL [/getClientById/{id}] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:


    @RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Client> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            response = getClient(id);
        }
        // answer
        return jsonMapper.writeValueAsString(response);
}
  • السطران 5 و 13: تُرجع الطريقة سلسلة JSON من النوع [Client]. لا يحتوي هذا النوع على أي تعليقات توضيحية لمرشحات JSON. لذلك، في السطر 13، يتم استخدام مُعِد خرائط JSON بدون مرشحات؛

السطر 11: طريقة [getClient] هي كما يلي:


    private Response<Client> getClient(long id) {
        // we get the customer back
        Client client = null;
        try {
            client = application.getClientById(id);
        } catch (RuntimeException e1) {
            return new Response<Client>(1, Static.getErreursForException(e1), null);
        }
        // existing customer?
        if (client == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le client d'id [%s] n'existe pas", id));
            return new Response<Client>(2, messages, null);
        }
        // ok
        return new Response<Client>(0, null, client);
}

النتائج هي كما يلي:

أو هذه إذا كان رقم تعريف العميل غير صحيح:

8.4.11.14. عنوان URL [/getCreneauById/{id}]

يتم التعامل مع عنوان URL [/getCreneauById/{id}] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:


    @RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Creneau> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // we give back the slot
            response = getCreneau(id);
        }
        // answer
        return jsonMapperShortCreneau.writeValueAsString(response);
}
  • السطران 5 و 14: تُرجع الطريقة سلسلة JSON من النوع [Response<Creneau>]؛

السطر 8: طريقة [getCreneau] هي كما يلي:


    private Response<Creneau> getCreneau(long id) {
        // we get the slot back
        Creneau créneau = null;
        try {
            créneau = application.getCreneauById(id);
        } catch (RuntimeException e1) {
            return new Response<Creneau>(1, Static.getErreursForException(e1), null);
        }
        // existing niche?
        if (créneau == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le créneau d'id [%s] n'existe pas", id));
            return new Response<Creneau>(2, messages, null);
        }
        // ok
        return new Response<Creneau>(0, null, créneau);
    }

دعونا نراجع كود كيان [Creneau]:


@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
    // characteristics of a RV slot
    private int hdebut;
    private int mdebut;
    private int hfin;
    private int mfin;
 
    // a slot is linked to a doctor
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_medecin")
    private Medecin medecin;
 
    // foreign key
    @Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
  • الأسطر 14-16: نظرًا لأن حقل [doctor] في وضع [fetch = FetchType.LAZY]، فإنه لا يتم استرداده عند استرداد فتحة عبر [id] الخاص بها. لذلك من الضروري استبعاده من التسلسل. بدون هذا الاستبعاد، تحدث استثناء. ويرجع ذلك إلى حقيقة أن كائن التسلسل [mapper] سوف يستدعي طريقة [getMedecin] لاسترداد حقل [medecin]. ومع ذلك، مع تطبيق JPA/Hibernate، يعرض وضع [fetch = FetchType.LAZY] لحقل [medecin] كائن [Creneau] الذي تمت برمجة طريقة [getMedecin] الخاصة به لجلب الطبيب من سياق JPA. ويُسمى هذا كائن [proxy]. والآن، دعونا نستذكر بنية تطبيق الويب:

يوجد وحدة التحكم في كتلة [Controllers / Actions]. وبمجرد الدخول إلى هذه الكتلة، لا ينطبق مفهوم سياق JPA بعد ذلك. يتم إنشاء سياق JPA أثناء العمليات في طبقة [DAO] ولا يستمر بعد ذلك. لذلك، عندما يحاول وحدة التحكم الوصول إلى سياق JPA، تحدث استثناء يشير إلى أنه مغلق. لتجنب هذا الاستثناء، يجب منع تسلسل حقل [medecin] لفئة [Rv]. وهذا ما يفعله مخطط JSON [jsonMapperShortCreneau]:


    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
}

النتائج التي تم الحصول عليها هي كما يلي:

أو هذه النتائج إذا كان رقم الفتحة غير صحيح:

8.4.11.15. عنوان URL [/getRvById/{id}]

يتم التعامل مع عنوان URL [/getRvById/{id}] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:


    @RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
        // the answer
        Response<Rv> response;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
        } else {
            // we recover rv
            response = getRv(id);
        }
        // answer
        return jsonMapperShortRv.writeValueAsString(response);
}
  • السطران 5 و 14: تُرجع الطريقة سلسلة JSON من النوع [Response<Rv>]؛

السطر 11: طريقة [getRv] هي كما يلي:


    private Response<Rv> getRv(long id) {
        // we recover the Rv
        Rv rv = null;
        try {
            rv = application.getRvById(id);
        } catch (RuntimeException e1) {
            return new Response<Rv>(1, Static.getErreursForException(e1), null);
        }
        // Existing Rv?
        if (rv == null) {
            List<String> messages = new ArrayList<String>();
            messages.add(String.format("Le rendez-vous d'id [%s] n'existe pas", id));
            return new Response<Rv>(2, messages, null);
        }
        // ok
        return new Response<Rv>(0, null, rv);
}

تحتوي فئة [Rv] على حقلين مزودين بعلامة [fetch = FetchType.LAZY]: الحقلان [creneau] و [client]. وبالتالي، لا يتم استرداد هذين الحقلين عند استرداد [Rv] عبر مفتاحه الأساسي. وللأسباب نفسها المذكورة سابقًا، يجب استبعادهما من التسلسل. وهذا ما يقوم به المُعِد [jsonMapperShortRv] التالي، المُعرَّف في فئة [WebConfig]:


    @Bean
    public ObjectMapper jsonMapperShortRv() {
        ObjectMapper jsonMapperShortRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
        jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
        return jsonMapperShortRv;
}

النتائج التي تم الحصول عليها هي كما يلي:

أو هذه النتائج إذا كان رقم الموعد غير صحيح:

8.4.11.16. عنوان URL [/ajouterRv]

يتم التعامل مع عنوان URL [/addAppt] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:


@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
        // the answer
        Response<Rv> response = null;
        boolean erreur = false;
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // retrieve posted values
        String jour;
        long idCreneau = -1;
        long idClient = -1;
        Date jourAgenda = null;
        if (!erreur) {
            // retrieve posted values
            jour = post.getJour();
            idCreneau = post.getIdCreneau();
            idClient = post.getIdClient();
            // check the date
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            sdf.setLenient(false);
            try {
                jourAgenda = sdf.parse(jour);
            } catch (ParseException e) {
                List<String> messages = new ArrayList<String>();
                messages.add(String.format("La date [%s] est invalide", jour));
                response = new Response<>(6, messages, null);
                erreur = true;
            }
        }
        // we get the slot back
        Response<Creneau> responseCréneau = null;
        if (!erreur) {
            // we get the slot back
            responseCréneau = getCreneau(idCreneau);
            if (responseCréneau.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
            }
        }
        // we get the customer back
        Response<Client> responseClient = null;
        Creneau créneau = null;
        if (!erreur) {
            créneau = (Creneau) responseCréneau.getBody();
            // we get the customer back
            responseClient = getClient(idClient);
            if (responseClient.getStatus() != 0) {
                erreur = true;
                response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
            }
        }
        if (!erreur) {
            Client client = responseClient.getBody();
            // we add the Rv
            try {
                response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
            } catch (RuntimeException e1) {
                erreur = true;
                response = new Response<>(5, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapperLongRv.writeValueAsString(response);
    }
  • السطران 5 و67: يجب أن تُرجع الطريقة سلسلة JSON من النوع [Response<Rv>]؛
  • السطر 3: تسترد التعليقات التوضيحية [@RequestBody PostAjouterRv post] نص POST وتضعه في المعلمة [PostAjouterRv post]. هذا النص هو JSON [consumes = "application/json; charset=UTF-8"] والذي سيتم فك تسلسله تلقائيًا إلى النوع [PostAjouterRv] التالي:

public class PostAjouterRv {
 
    // pOST DATA
    private String jour;
    private long idClient;
    private long idCreneau;
...
  • ثم يوجد كود سبق أن صادفناه بشكل أو بآخر؛
  • السطر 67: إعداد مرشحات JSON [creneauFilter] و [rvFilter]. تُرجع الطريقة سلسلة JSON من النوع [Response<Rv>]، حيث تم الحصول على Rv في السطر 61. يُغلف الكائن [Rv] كائن [Creneau] وكذلك كائن [Client]. يحتوي كائن [Creneau] على تبعية [FetchType.LAZY] لكائن [Medecin] وتم استرداده في الأسطر 36-44. تم جلبه من سياق JPA عبر مفتاحه الأساسي وتم استرداده بدون تبعية [FetchType.LAZY]. في النهاية،
    • يحتوي الكائن [Rv] على جميع تبعياته. يمكن تسلسلها؛
    • لا يحتوي كائن [Creneau] على تبعية [medecin] الخاصة به. لذلك، يجب عدم تسلسل هذه التبعية؛

يفي مخطط JSON [jsonMapperLongRv] المحدد في فئة [WebConfig] بهذه القيود:


    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
        return jsonMapperLongRv;
}

تبدو النتائج التي تم الحصول عليها كما يلي باستخدام عميل [Advanced Rest Client]:

  • في [1]، عنوان URL لطلب POST؛
  • في [2]، طلب POST؛
  • في [3]، القيمة المرسلة؛
  • في [4a]، هذه القيمة المنشورة هي JSON؛
  • في [4b]، يشير العميل إلى أنه يرسل JSON؛
  • في [5]، يشير الخادم إلى أنه يعيد JSON؛
  • في [6]، استجابة JSON من الخادم تمثل الموعد المضاف. وهي تعرض معرف [id] للموعد المضاف؛

نحصل على ما يلي مع رقم فتحة غير موجود:

8.4.11.17. عنوان URL [/deleteAppointment]

يتم التعامل مع عنوان URL [/deleteAppointment] بواسطة الطريقة التالية في وحدة التحكم [RdvMedecinsController]:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
        // the answer
        Response<Void> response = null;
        boolean erreur = false;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
        if (messages != null) {
            response = new Response<>(-1, messages, null);
            erreur = true;
        }
        // retrieve posted values
        long idRv = post.getIdRv();
        // recovering the rv
        if (!erreur) {
            Response<Rv> responseRv = getRv(idRv);
            if (responseRv.getStatus() != 0) {
                response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
                erreur = true;
            }
        }
        if (!erreur) {
            // rv deletion
            try {
                application.supprimerRv(idRv);
                response = new Response<Void>(0, null, null);
            } catch (RuntimeException e1) {
                response = new Response<>(3, Static.getErreursForException(e1), null);
            }
        }
        // answer
        return jsonMapper.writeValueAsString(response);
    }
  • السطر 5: النوع [Void] هو الفئة المطابقة للنوع البدائي [void
  • السطران 5 و 34: تُرجع الطريقة سلسلة JSON من النوع [Response<Void>] التي لا تحتوي على مرشحات JSON. لذلك، في السطر 34، نستخدم أداة تعيين JSON بدون مرشحات؛
  • السطر 3: تأخذ الطريقة نص POST كمعلمة، أي القيمة المرسلة. يتم استلام هذه القيمة بتنسيق JSON [content-type="application/json; charset=UTF-8"] ويتم تحويلها تلقائيًا إلى النوع [PostSupprimerRv] التالي:

public class PostSupprimerRv {
 
    // pOST DATA
    private long idRv;
 
  • السطر 28: عند نجاح الحذف، يتم إرسال استجابة مع [status=0]؛

النتائج التي تم الحصول عليها هي كما يلي:

  • في [5]، يشير الحقل [status=0] إلى أن عملية الحذف تمت بنجاح؛

مع معرف موعد غير موجود، نحصل على ما يلي:

لقد انتهينا من وحدة التحكم. والآن لنرى كيفية تشغيل المشروع.

8.4.11.18. الفئة القابلة للتنفيذ لخدمة الويب

فئة [Boot] [1] هي كما يلي:


package rdvmedecins.web.boot;
 
import org.springframework.boot.SpringApplication;
 
import rdvmedecins.web.config.AppConfig;
 
public class Boot {
 
    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}

السطر 10: يتم تنفيذ الطريقة الثابتة [SpringApplication.run] مع فئة تكوين المشروع [AppConfig] كمعلمة أولى لها. ستقوم هذه الطريقة بتكوين المشروع تلقائيًا، وتشغيل خادم Tomcat المضمن في التبعيات، ونشر وحدة التحكم [RdvMedecinsController] عليه.

يتم التحكم في السجلات بواسطة الملفات التالية [2]:

[logback.xml]


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- log level control -->
        <root level="info"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>
  • السطر 9: تم تعيين مستوى السجل العام على [info

[application.properties]


logging.level.org.springframework.web=INFO
logging.level.org.hibernate=OFF
spring.main.show-banner=false

تحدد السطران 1 و2 مستوى تسجيل محددًا لأجزاء معينة من التطبيق:

  • السطر 1: نريد سجلات من طبقة [web]؛
  • السطر 2: لا نريد سجلات من طبقة [JPA
  • السطر 3: لا نريد شعار Spring Boot؛

السجلات أثناء التنفيذ هي كما يلي:


11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,342 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:06:04,357 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:06:04,404 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:06:04,420 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point
 
11:06:04.732 [main] INFO  rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 420 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:06:04.775 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:05.538 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:06:05.688 [main] INFO  o.a.catalina.core.StandardService - Starting service Tomcat
11:06:05.689 [main] INFO  o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:06:05.833 [localhost-startStop-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:06:05.833 [localhost-startStop-1] INFO  o.s.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1061 ms
11:06:06.231 [localhost-startStop-1] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:06:09.234 [localhost-startStop-1] INFO  o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@12d14fa, org.springframework.security.web.context.SecurityContextPersistenceFilter@29823fb6, org.springframework.security.web.header.HeaderWriterFilter@662d93b2, org.springframework.security.web.authentication.logout.LogoutFilter@2d81ee0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52aa47ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@60bd7a74, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a374232, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ddb4452, org.springframework.security.web.session.SessionManagementFilter@2cd9855f, org.springframework.security.web.access.ExceptionTranslationFilter@2263f0a2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@192ce7f6]
11:06:09.255 [localhost-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:06:09.255 [localhost-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Medecin> rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Client> rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Client>> rdvmedecins.web.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Medecin>> rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
11:06:09.677 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:09.770 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:06:09.786 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:06:09.802 [main] INFO  o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:06:09.817 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:06:09.817 [main] INFO  rdvmedecins.web.boot.Boot - Started Boot in 5.319 seconds (JVM running for 6.053)
  • السطر 18: خادم Tomcat نشط؛
  • السطر 21: يجري تهيئة سياق Spring؛
  • الأسطر 27–38: يتم اكتشاف عناوين URL التي تعرضها خدمة الويب؛
  • السطر 44: خادم Tomcat جاهز وينتظر الطلبات على المنفذ 8080؛

إذا قمنا بتعديل ملف [application.properties] على النحو التالي:


logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false

نحصل على السجلات التالية:

11:12:12,107 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:12:12,108 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:12:12,108 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:12:12,108 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:12:12,108 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:12:12,108 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:12:12,172 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:12:12,174 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:12:12,186 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:12:12,205 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:12:12,255 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:12:12,255 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:12:12,256 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:12:12,257 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point

11:12:12.567 [main] INFO  rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 5856 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:12:12.602 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:12:12 CEST 2015]; root of context hierarchy
11:12:13.363 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:12:13.503 [main] INFO  o.a.catalina.core.StandardService - Starting service Tomcat
11:12:13.503 [main] INFO  o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:12:13.644 [localhost-startStop-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:12:14.044 [localhost-startStop-1] INFO  o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:12:17.229 [localhost-startStop-1] INFO  o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@141859ba, org.springframework.security.web.context.SecurityContextPersistenceFilter@19925f3b, org.springframework.security.web.header.HeaderWriterFilter@3083c83b, org.springframework.security.web.authentication.logout.LogoutFilter@7c22ac3b, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@126fe543, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@8eecab2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@91b42ad, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e33581f, org.springframework.security.web.session.SessionManagementFilter@10abfbc1, org.springframework.security.web.access.ExceptionTranslationFilter@3e933729, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3c8f6f86]
11:12:17.259 [localhost-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:12:17.259 [localhost-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:12:17.837 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:12:17.853 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:12:17.869 [main] INFO  o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:12:17.900 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:12:17.902 [main] INFO  rdvmedecins.web.boot.Boot - Started Boot in 5.545 seconds (JVM running for 6.305)

علاوة على ذلك، إذا قمنا بتعديل ملف [logback.xml] على النحو التالي:


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- log level control -->
        <root level="off"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>

يتم الحصول على السجلات التالية:

11:14:53,862 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:14:53,862 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:14:53,862 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:14:53,862 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:14:53,862 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:14:53,862 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:14:53,924 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:14:53,924 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:14:53,940 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:14:53,956 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:14:54,002 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to OFF
11:14:54,002 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:14:54,002 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:14:54,002 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point

يمكننا أن نرى، بالتالي، أن لدينا بعض التحكم في السجلات التي تظهر في وحدة التحكم. غالبًا ما يكون مستوى [info] هو مستوى السجل المناسب.

لدينا الآن خدمة ويب تشغيلية يمكن الاستعلام عنها باستخدام عميل ويب. سنقوم الآن بمعالجة مسألة تأمين هذه الخدمة: نريد أن يتمكن أشخاص معينون فقط من إدارة مواعيد الأطباء. للقيام بذلك، سنستخدم إطار عمل Spring Security، وهو أحد مكونات نظام Spring.

8.4.12. مقدمة إلى Spring Security

سنقوم باستيراد دليل Spring مرة أخرى باتباع الخطوات من 1 إلى 3 أدناه:

  

يتألف المشروع من العناصر التالية:

  • في مجلد [templates]، ستجد صفحات HTML الخاصة بالمشروع؛
  • [Application]: هي فئة المشروع القابلة للتنفيذ؛
  • [MvcConfig]: هي فئة تكوين Spring MVC؛
  • [WebSecurityConfig]: هي فئة تكوين Spring Security؛

8.4.12.1. تكوين Maven

المشروع [3] هو مشروع Maven. دعونا نفحص ملف [pom.xml] الخاص به لنرى تبعياته:


<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework</groupId>
    <artifactId>gs-securing-web</artifactId>
    <version>0.1.0</version>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- tag::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- end::security[] -->
    </dependencies>
 
    <properties>
        <start-class>hello.Application</start-class>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>
  • الأسطر 10–14: المشروع هو مشروع Spring Boot؛
  • الأسطر 17–20: التبعية لإطار عمل [Thymeleaf
  • الأسطر 22–25: التبعية لإطار عمل Spring Security؛

8.4.12.2. طرق عرض Thymeleaf

  

تبدو طريقة عرض [home.html] كما يلي:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>
 
    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • السطر 12: ستقوم السمة [th:href="@{/hello}"] بإنشاء السمة [href] لعلامة <a>. وستقوم القيمة [@{/hello}] بإنشاء المسار [<context>/hello]، حيث [context] هو سياق تطبيق الويب؛

فيما يلي كود HTML الذي تم إنشاؤه:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>
 
        <p>
            Click
            <a href="/hello">here</a>
            to see a greeting.
        </p>
    </body>
</html>

تبدو طريقة العرض [hello.html] كما يلي:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • السطر 9: ستقوم السمة [th:inline="text"] بإنشاء نص علامة <h1>. يحتوي هذا النص على تعبير $ يجب تقييمه. العنصر [[${#httpServletRequest.remoteUser}]] هو قيمة السمة [RemoteUser] لطلب HTTP الحالي. هذا هو اسم المستخدم الذي قام بتسجيل الدخول؛
  • السطر 10: نموذج HTML. ستقوم السمة [th:action="@{/logout}"] بإنشاء السمة [action] لعلامة [form]. ستقوم القيمة [@{/logout}] بإنشاء المسار [<context>/logout]، حيث [context] هو سياق تطبيق الويب؛

الرمز HTML الذي تم إنشاؤه هو كما يلي:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello user!</h1>
        <form method="post" action="/logout">
            <input type="submit" value="Sign Out" />
            <input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
        </form>
    </body>
</html>
  • السطر 8: ترجمة Hello [[${#httpServletRequest.remoteUser}]]!;
  • السطر 9: ترجمة @{/logout};
  • السطر 11: حقل مخفي باسم (سمة الاسم) _csrf؛

الطريقة النهائية [login.html] هي كما يلي:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • السطر 9: تضمن السمة [th:if="${param.error}"] أن علامة <div> لن يتم إنشاؤها إلا إذا كان عنوان URL الذي يعرض صفحة تسجيل الدخول يحتوي على المعلمة [error] (http://context/login?error
  • السطر 10: تضمن السمة [th:if="${param.logout}"] أن علامة <div> لن يتم إنشاؤها إلا إذا كان عنوان URL الذي يعرض صفحة تسجيل الدخول يحتوي على المعلمة [logout] (http://context/login?logout
  • الأسطر 11–23: نموذج HTML؛
  • السطر 11: سيتم إرسال النموذج إلى عنوان URL [<context>/login]، حيث يمثل <context> سياق تطبيق الويب؛
  • السطر 13: حقل إدخال باسم [username
  • السطر 17: حقل إدخال باسم [password

الرمز HTML الذي تم إنشاؤه هو كما يلي:


<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
 
        <div>
            You have been logged out.
        </div>
        <form method="post" action="/login">
            <div>
                <label>
                    User Name :
                    <input type="text" name="username" />
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" name="password" />
                </label>
            </div>
            <div>
                <input type="submit" value="Sign In" />
            </div>
            <input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
        </form>
    </body>
</html>

لاحظ في السطر 28 أن Thymeleaf قد أضاف حقلًا مخفيًا باسم [_csrf].

8.4.12.3. تكوين Spring MVC

  

تقوم فئة [MvcConfig] بتكوين إطار عمل Spring MVC:


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
 
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }
 
}
  • السطر 7: تجعل العلامة [@Configuration] فئة [MvcConfig] فئة تكوين؛
  • السطر 8: تمتد فئة [MvcConfig] إلى فئة [WebMvcConfigurerAdapter] لتجاوز طرق معينة؛
  • السطر 10: إعادة تعريف طريقة من الفئة الأصلية؛
  • الأسطر 11–16: تسمح الطريقة [addViewControllers] بربط عناوين URL بعروض HTML. يتم إجراء الروابط التالية هناك:
عنوان URL
عرض
/, /home
/templates/home.html
/hello
/القوالب/hello.html
/تسجيل_الدخول
/القوالب/تسجيل_الدخول.html

اللاحقة [html] والمجلد [templates] هما القيمتان الافتراضيتان اللتان يستخدمهما Thymeleaf. يمكن تغييرهما عبر التكوين. يجب أن يكون المجلد [templates] في جذر مسار فئات المشروع:

في [1] أعلاه، يعد كل من مجلدي [java] و[resources] مجلدين للمصدر. وهذا يعني أن محتوياتهما ستكون في جذر مسار فئات المشروع. وبالتالي، في [2]، سيكون مجلدا [hello] و[templates] في جذر مسار فئات المشروع.

8.4.12.4. تكوين Spring Security

  

تقوم فئة [WebSecurityConfig] بتكوين إطار عمل Spring Security:


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
 
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • السطر 9: تجعل العلامة [@Configuration] فئة [WebSecurityConfig] فئة تكوين؛
  • السطر 10: تجعل العلامة [@EnableWebSecurity] فئة [WebSecurityConfig] فئة تكوين Spring Security؛
  • السطر 11: تمتد فئة [WebSecurity] إلى فئة [WebSecurityConfigurerAdapter] لتجاوز طرق معينة؛
  • السطر 12: إعادة تعريف طريقة من الفئة الأصلية؛
  • الأسطر 13-16: يتم تجاوز الطريقة [configure(HttpSecurity http)] لتعريف حقوق الوصول لمختلف عناوين URL الخاصة بالتطبيق؛
  • السطر 14: تسمح الطريقة [http.authorizeRequests()] بربط عناوين URL بحقوق الوصول. يتم إجراء الروابط التالية هناك:
عنوان URL
القاعدة
الرمز
/، /home
الوصول بدون مصادقة

http.authorizeRequests().antMatchers("/", "/home").permitAll()
عناوين URL أخرى
الوصول المصادق عليه فقط
http.anyRequest().authenticated();
  • السطر 15: يحدد طريقة المصادقة. تتم المصادقة عبر نموذج URL [/login] متاح للجميع [http.formLogin().loginPage("/login").permitAll()]. كما أن تسجيل الخروج متاح للجميع؛
  • الأسطر 19-21: تعيد تعريف الطريقة [configure(AuthenticationManagerBuilder auth)] التي تدير المستخدمين؛
  • السطر 20: تتم المصادقة باستخدام مستخدمين مبرمجين [auth.inMemoryAuthentication()]. يتم تعريف المستخدم هنا باستخدام اسم المستخدم [user] وكلمة المرور [password] والدور [USER]. يمكن منح المستخدمين الذين لديهم نفس الدور نفس الأذونات؛

8.4.12.5. فئة قابلة للتنفيذ

  

فئة [Application] هي كما يلي:


package hello;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
 
    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }
 
}
  • السطر 8: توجه العلامة [@EnableAutoConfiguration] Spring Boot (السطر 3) إلى تنفيذ التكوين الذي لم يقم المطور بإعداده صراحةً؛
  • السطر 9: يجعل فئة [Application] فئة تكوين Spring؛
  • السطر 10: يوجه النظام إلى فحص الدليل الذي يحتوي على فئة [Application] للبحث عن مكونات Spring. وبالتالي سيتم اكتشاف الفئتين [MvcConfig] و [WebSecurityConfig] لأنهما تحتويان على تعليق [@Configuration
  • السطر 13: الطريقة [main] للفئة القابلة للتنفيذ؛
  • السطر 14: يتم تنفيذ الطريقة الثابتة [SpringApplication.run] باستخدام فئة التكوين [Application] كمعلمة. لقد صادفنا هذه العملية من قبل ونعلم أن خادم Tomcat المضمن في تبعيات Maven للمشروع سيتم تشغيله ونشر المشروع عليه. رأينا أن أربعة عناوين URL تمت معالجتها [/, /home, /login, /hello] وأن بعضها محمي بحقوق الوصول.

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

لنبدأ بطلب عنوان URL [/]، وهو أحد عناوين URL الأربعة المقبولة. وهو مرتبط بالعرض [/templates/home.html]:

 

عنوان URL المطلوب [/] متاح للجميع. ولهذا السبب تمكنا من استرداده. الرابط [هنا] هو كما يلي:

Click <a href="/hello">here</a> to see a greeting.

سيتم طلب عنوان URL [/hello] عند النقر على الرابط. هذا العنوان محمي:

عنوان URL
قاعدة
رمز
/، /home
الوصول بدون مصادقة

http.authorizeRequests().antMatchers("/", "/home").permitAll()
عناوين URL الأخرى
الوصول المصادق عليه فقط
http.anyRequest().authenticated();

يجب أن تكون مصادقًا عليه للوصول إليه. سيقوم Spring Security بعد ذلك بإعادة توجيه متصفح العميل إلى صفحة المصادقة. استنادًا إلى التكوين الموضح، هذه هي الصفحة الموجودة على عنوان URL [/login]. هذه الصفحة متاحة للجميع:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

وبذلك نحصل على [1]:

فيما يلي شفرة المصدر للصفحة الناتجة:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • السطر 7، يظهر حقل مخفي غير موجود في الصفحة الأصلية [login.html]. أضافه Thymeleaf. تم تصميم هذا الرمز، المعروف باسم CSRF (تزوير الطلبات عبر المواقع)، للقضاء على ثغرة أمنية. يجب إرسال هذا الرمز إلى Spring Security مع المصادقة حتى يتم قبوله؛

نتذكر أن Spring Security لا يتعرف إلا على زوج المستخدم/كلمة المرور. إذا أدخلنا شيئًا آخر في [2]، فسنحصل على نفس الصفحة مع رسالة خطأ في [3]. قام Spring Security بإعادة توجيه المتصفح إلى عنوان URL [http://localhost:8080/login?error]. أدى وجود المعلمة [error] إلى عرض العلامة:


<div th:if="${param.error}">Invalid username and password.</div>

الآن، دعونا ندخل قيم اسم المستخدم وكلمة المرور المتوقعة [4]:

  • في [4]، نقوم بتسجيل الدخول؛
  • في [5]، يقوم Spring Security بإعادة توجيهنا إلى عنوان URL [/hello] لأن هذا هو العنوان الذي طلبناه عندما تمت إعادة توجيهنا إلى صفحة تسجيل الدخول. وقد عُرضت هوية المستخدم في السطر التالي من ملف [hello.html]:

    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

تعرض الصفحة [5] النموذج التالي:


    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
</form>

عند النقر على زر [تسجيل الخروج]، يتم إرسال طلب POST إلى عنوان URL [/logout]. ومثل عنوان URL [/login]، فإن هذا العنوان متاح للجميع:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

في تعيين عناوين URL/العرض لدينا، لم نحدد أي شيء لعنوان URL [/logout]. ماذا سيحدث؟ دعونا نجرب ذلك:

  • في [6]، نضغط على زر [Sign Out
  • في [7]، نرى أنه تمت إعادة توجيهنا إلى عنوان URL [http://localhost:8080/login?logout]. طلب Spring Security إعادة التوجيه هذه. تسبب وجود المعلمة [logout] في عنوان URL في عرض السطر التالي في العرض:

<div th:if="${param.logout}">You have been logged out.</div>

8.4.12.7. الخلاصة

في المثال السابق، كان بإمكاننا كتابة تطبيق الويب أولاً ثم تأمينه لاحقًا. Spring Security غير تدخلي. يمكنك تنفيذ الأمان لتطبيق ويب تمت كتابته بالفعل. علاوة على ذلك، اكتشفنا النقاط التالية:

  • من الممكن تعريف صفحة مصادقة؛
  • يجب أن تكون المصادقة مصحوبة برمز CSRF الصادر عن Spring Security؛
  • في حالة فشل المصادقة، يتم إعادة توجيهك إلى صفحة المصادقة مع معلمة خطأ إضافية في عنوان URL؛
  • إذا نجحت المصادقة، يتم إعادة توجيهك إلى الصفحة المطلوبة وقت المصادقة. إذا طلبت صفحة المصادقة مباشرةً دون المرور بصفحة وسيطة، فإن Spring Security يعيد توجيهك إلى عنوان URL [/] (لم يتم توضيح هذه الحالة)؛
  • يمكنك تسجيل الخروج عن طريق طلب عنوان URL [/logout] باستخدام طلب POST. ثم يقوم Spring Security بإعادة توجيهك إلى صفحة المصادقة مع معلمة "logout" في عنوان URL؛

تستند جميع هذه الاستنتاجات إلى السلوك الافتراضي لـ Spring Security. يمكن تغيير هذا السلوك من خلال التكوين عن طريق تجاوز طرق معينة لفئة [WebSecurityConfigurerAdapter].

لن يكون الدرس السابق مفيدًا لنا كثيرًا في المستقبل. سنستخدم، في الواقع، ما يلي:

  • قاعدة بيانات لتخزين المستخدمين وكلمات مرورهم وأدوارهم؛
  • المصادقة المستندة إلى رأس HTTP؛

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

8.4.13. تنفيذ الأمان على خدمة الويب الخاصة بالمواعيد

8.4.13.1. قاعدة البيانات

يتم تحديث قاعدة بيانات [rdvmedecins] لتشمل المستخدمين وكلمات مرورهم وأدوارهم. ويجري إضافة ثلاثة جداول جديدة:

Image

الجدول [USERS]: users

  • ID: المفتاح الأساسي؛
  • VERSION: عمود إصدار الصف؛
  • IDENTITY: معرف وصف للمستخدم؛
  • LOGIN: اسم تسجيل دخول المستخدم؛
  • PASSWORD: كلمة المرور الخاصة به؛

في جدول USERS، لا يتم تخزين كلمات المرور بنص عادي:

 

الخوارزمية المستخدمة لتشفير كلمات المرور هي خوارزمية BCRYPT.

جدول [ROLES]: الأدوار

  • ID: المفتاح الأساسي؛
  • VERSION: عمود الإصدار للصف؛
  • NAME: اسم الدور. بشكل افتراضي، يتوقع Spring Security أن تكون الأسماء بالصيغة ROLE_XX، على سبيل المثال ROLE_ADMIN أو ROLE_GUEST؛
 

الجدول [USERS_ROLES]: جدول ربط USERS/ROLES

يمكن أن يكون للمستخدم أدوار متعددة، ويمكن أن يشمل الدور مستخدمين متعددين. هذه علاقة متعددة إلى متعددة يمثلها جدول [USERS_ROLES].

  • ID: المفتاح الأساسي؛
  • VERSION: عمود إصدار الصف؛
  • USER_ID: معرف المستخدم؛
  • ROLE_ID: معرف الدور؛
 

نظرًا لأننا نقوم بتعديل قاعدة البيانات، يجب تعديل جميع طبقات المشروع [منطق الأعمال، DAO، JPA]:

8.4.13.2. مشروع STS الجديد لـ [منطق الأعمال، DAO، JPA]

يتطور مشروع [rdvmedecins-business-dao] على النحو التالي:

  • في [1]: المشروع الجديد؛
  • في [2]: تم تجميع التغييرات التي أدخلها تطبيق الأمان في حزمة واحدة [rdvmedecins.security]. تنتمي هذه العناصر الجديدة إلى طبقات [JPA] و[DAO]، ولكن تم تجميعها في الحزمة نفسها لتبسيط الأمور.

8.4.13.3. الكيانات [JPA] الجديدة

تحدد طبقة JPA ثلاث كيانات جديدة:

  

تمثل فئة [User] الجدول [USERS]:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // properties
    private String identity;
    private String login;
    private String password;
 
    // manufacturer
    public User() {
    }
 
    public User(String identity, String login, String password) {
        this.identity = identity;
        this.login = login;
        this.password = password;
    }
 
    // identity
    @Override
    public String toString() {
        return String.format("User[%s,%s,%s]", identity, login, password);
    }
 
    // getters and setters
....
}
  • السطر 9: تمتد الفئة إلى فئة [AbstractEntity] المستخدمة بالفعل للكيانات الأخرى؛
  • الأسطر 13-15: لم يتم تحديد أسماء الأعمدة لأنها تحمل نفس أسماء الحقول المرتبطة بها؛

تعكس فئة [Role] الجدول [ROLES]:


package rdvmedecins.entities;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
 
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
 
    // properties
    private String name;
 
    // manufacturers
    public Role() {
    }
 
    public Role(String name) {
        this.name = name;
    }
 
    // identity
    @Override
    public String toString() {
        return String.format("Role[%s]", name);
    }
 
    // getters and setters
...
}

تمثل فئة [UserRole] الجدول [USERS_ROLES]:


package rdvmedecins.entities;
 
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
 
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
 
    private static final long serialVersionUID = 1L;
 
    // a UserRole refers to a User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
    // a UserRole refers to a Role
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;
 
    // getters and setters
...
}
  • الأسطر 15-17: تعريف المفتاح الخارجي من جدول [USERS_ROLES] إلى جدول [USERS
  • الأسطر 19-21: تنفيذ المفتاح الخارجي من الجدول [USERS_ROLES] إلى الجدول [ROLES

8.4.13.4. التغييرات التي طرأت على طبقة [DAO]

تم تحسين طبقة [DAO] بإضافة ثلاثة [مستودعات] جديدة:

  

تدير واجهة [UserRepository] الوصول إلى كيانات [User]:


package rdvmedecins.repositories;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
 
public interface UserRepository extends CrudRepository<User, Long> {
 
    // liste des rôles d'un utilisateur identifié par son id
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);
 
    // liste des rôles d'un utilisateur identifié par son login et son mot de passe
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);
 
    // recherche d'un utilisateur via son login
    User findUserByLogin(String login);
}
  • السطر 9: واجهة [UserRepository] تمتد واجهة Spring Data [CrudRepository] (السطر 4)؛
  • السطران 12-13: تسترد طريقة [getRoles(User user)] جميع الأدوار الخاصة بمستخدم محدد بواسطة [id]
  • السطران 16-17: كما هو مذكور أعلاه، ولكن بالنسبة لمستخدم يتم تحديده بواسطة اسم تسجيل الدخول وكلمة المرور؛
  • السطر 20: للبحث عن مستخدم باستخدام اسم تسجيل الدخول الخاص به؛

تدير واجهة [RoleRepository] الوصول إلى كيانات [Role]:


package rdvmedecins.security;
 
import org.springframework.data.repository.CrudRepository;
 
public interface RoleRepository extends CrudRepository<Role, Long> {
 
    // search for a role by name
    Role findRoleByName(String name);
 
}
  • السطر 5: واجهة [RoleRepository] تمتد من واجهة [CrudRepository
  • السطر 8: يمكنك البحث عن دور حسب اسمه؛

تدير واجهة [userRoleRepository] الوصول إلى كيانات [UserRole]:


package rdvmedecins.security;
 
import org.springframework.data.repository.CrudRepository;
 
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
 
}
  • السطر 5: واجهة [UserRoleRepository] تكتفي بتمديد واجهة [CrudRepository] دون إضافة أي طرق جديدة؛

8.4.13.5. فئات إدارة المستخدمين والأدوار

  

يتطلب Spring Security إنشاء فئة تنفذ واجهة [UsersDetail] التالية:

 

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


package rdvmedecins.security;
 
import java.util.ArrayList;
import java.util.Collection;
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
 
public class AppUserDetails implements UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    // properties
    private User user;
    private UserRepository userRepository;
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }
 
    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
 
    @Override
    public String getPassword() {
        return user.getPassword();
    }
 
    @Override
    public String getUsername() {
        return user.getLogin();
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
 
    // getters and setters
    ...
}
  • السطر 10: تنفذ فئة [AppUserDetails] واجهة [UserDetails
  • السطران 15-16: تغلف الفئة مستخدمًا (السطر 15) والمستودع الذي يوفر تفاصيل عن هذا المستخدم (السطر 16)؛
  • الأسطر 22-25: المنشئ الذي ينشئ مثيلًا للفئة باستخدام مستخدم ومستودعه؛
  • الأسطر 28–35: تنفيذ طريقة [getAuthorities] لواجهة [UserDetails]. يجب أن تنشئ مجموعة من العناصر من النوع [GrantedAuthority] أو نوع مشتق. هنا، نستخدم النوع المشتق [SimpleGrantedAuthority] (السطر 32)، الذي يغلف اسم أحد أدوار المستخدم من السطر 15؛
  • الأسطر 31–33: نكرر عبر قائمة أدوار المستخدم من السطر 15 لإنشاء قائمة من العناصر من النوع [SimpleGrantedAuthority
  • الأسطر 38-40: تنفيذ طريقة [getPassword] لواجهة [UserDetails]. نُرجع كلمة مرور المستخدم من السطر 15؛
  • الأسطر 38-40: ننفذ طريقة [getUserName] لواجهة [UserDetails]. نُرجع اسم تسجيل الدخول للمستخدم من السطر 15؛
  • الأسطر 47-50: لا تنتهي صلاحية حساب المستخدم أبدًا؛
  • الأسطر 52–55: لا يتم قفل حساب المستخدم أبدًا؛
  • الأسطر 57–60: بيانات اعتماد المستخدم لا تنتهي صلاحيتها أبدًا؛
  • الأسطر 62–65: حساب المستخدم نشط دائمًا؛

يتطلب Spring Security أيضًا وجود فئة تنفذ واجهة [AppUserDetailsService]:

 

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


package rdvmedecins.security;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
 
@Service
public class AppUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // search for user via login
        User user = userRepository.findUserByLogin(login);
        // found?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(user, userRepository);
    }
 
}
  • السطر 9: ستكون الفئة مكونًا في Spring، لذا ستكون متاحة في سياقها؛
  • السطران 12-13: سيتم إدخال مكون [UserRepository] هنا؛
  • الأسطر 16-25: تنفيذ طريقة [loadUserByUsername] لواجهة [UserDetailsService] (السطر 10). المعلمة هي اسم تسجيل دخول المستخدم؛
  • السطر 18: يتم البحث عن المستخدم باستخدام اسم تسجيل الدخول الخاص به؛
  • الأسطر 20-22: إذا لم يتم العثور على المستخدم، يتم إصدار استثناء؛
  • السطر 24: يتم إنشاء كائن [AppUserDetails] وإرجاعه. وهو بالفعل من النوع [UserDetails] (السطر 16)؛

8.4.13.6. اختبارات طبقة [DAO]

  

أولاً، نقوم بإنشاء فئة قابلة للتنفيذ [CreateUser] قادرة على إنشاء مستخدم له دور:


package rdvmedecins.security;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
 
public class CreateUser {
 
    public static void main(String[] args) {
        // syntax: login password roleName
 
        // three parameters are required
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // parameters are retrieved
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // does the role already exist?
        Role role = roleRepository.findRoleByName(roleName);
        // if it doesn't exist, we create it
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // does the user already exist?
        User user = userRepository.findUserByLogin(login);
        // if it doesn't exist, we create it
        if (user == null) {
            // hash the password with bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // save user
            user = userRepository.save(new User(login, login, crypt));
            // we create the relationship with the role
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // the user already exists - does he/she have the required role?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // if not found, we create the relationship with the role
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }
 
        // closing Spring context
        context.close();
    }
 
}
  • السطر 17: تتوقع الفئة ثلاثة معلمات تحدد هوية المستخدم: اسم المستخدم وكلمة المرور والدور؛
  • الأسطر 25–27: يتم استرداد المعلمات الثلاثة؛
  • السطر 29: يتم إنشاء سياق Spring من فئة التكوين [DomainAndPersistenceConfig]. كانت هذه الفئة موجودة بالفعل في المشروع الأولي. يجب تحديثها على النحو التالي:

@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
  • السطر 1: يجب تحديد أن هناك الآن مكونات [Repository] في حزمة [rdvmedecins.security
  • السطر 4: يجب تحديد أن هناك الآن كيانات JPA في الحزمة [rdvmedecins.security

لنعد إلى الكود الخاص بإنشاء مستخدم:

  • الأسطر 30–32: نسترد مراجع الكائنات الثلاثة [Repository] التي قد تكون مفيدة لإنشاء المستخدم؛
  • السطر 34: نتحقق مما إذا كان الدور موجودًا بالفعل؛
  • الأسطر 36–38: إذا لم يكن موجودًا، نقوم بإنشائه في قاعدة البيانات. سيكون اسمه على شكل [ROLE_XX
  • السطر 40: نتحقق مما إذا كان اسم المستخدم موجودًا بالفعل؛
  • الأسطر 42-49: إذا لم يكن اسم المستخدم موجودًا، نقوم بإنشائه في قاعدة البيانات؛
  • السطر 44: نقوم بتشفير كلمة المرور. هنا، نستخدم فئة [BCrypt] من Spring Security (السطر 4). لذلك نحتاج إلى أرشيفات هذا الإطار. يتضمن ملف [pom.xml] تبعية جديدة:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • السطر 46: يتم حفظ المستخدم في قاعدة البيانات؛
  • السطر 48: وكذلك العلاقة التي تربطه بدوره؛
  • الأسطر 51-57: إذا كان تسجيل الدخول موجودًا بالفعل، نتحقق مما إذا كان الدور الذي نريد تعيينه له موجودًا بالفعل ضمن أدواره؛
  • الأسطر 59–61: إذا لم يتم العثور على الدور المطلوب، يتم إنشاء صف في جدول [USERS_ROLES] لربط المستخدم بدوره؛
  • لم نقم بالحماية من الاستثناءات المحتملة. هذه فئة مساعدة لإنشاء مستخدم بدور بسرعة.

عند تنفيذ الفئة مع الحجج [x x guest]، يتم الحصول على النتائج التالية في قاعدة البيانات:

جدول [USERS]

جدول [الأدوار]

 

جدول [USERS_ROLES]

 

الآن دعونا ننظر إلى الفئة الثانية [UsersTest]، وهي اختبار JUnit:

  

package rdvmedecins.security;
 
import java.util.List;
 
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
 
import com.google.common.collect.Lists;
 
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
 
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;
 
    @Test
    public void findAllUsersWithTheirRoles() {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(user);
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }
 
    @Test
    public void findUserByLogin() {
        // user [admin] is retrieved
        User user = userRepository.findUserByLogin("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // check admin / admin role
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }
 
    @Test
    public void loadUserByUsername() {
        // user [admin] is retrieved
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // check admin / admin role
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }
 
    // utility method - displays items in a collection
    private void display(String message, Iterable<?> elements) {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(element);
        }
    }
}
  • الأسطر 27–34: اختبار مرئي. نعرض جميع المستخدمين مع أدوارهم؛
  • الأسطر 36–46: نتحقق من أن المستخدم [admin] لديه كلمة المرور [admin] والدور [ROLE_ADMIN] باستخدام [UserRepository
  • السطر 41: [admin] هي كلمة المرور النصية العادية. في قاعدة البيانات، يتم تشفيرها باستخدام خوارزمية BCrypt. تتحقق طريقة [BCrypt.checkpw] من أن كلمة المرور النصية العادية المشفرة تتطابق مع تلك الموجودة في قاعدة البيانات؛
  • الأسطر 48–59: نتحقق من أن المستخدم [admin] لديه كلمة المرور [admin] والدور [ROLE_ADMIN] باستخدام [appUserDetailsService

تم تشغيل الاختبارات بنجاح مع السجلات التالية:

User[admin,admin,$2a$10$FN1LMKjPU46aPffh9Zaw4exJOLo51JJPWrxqzak/eJrbt3CO9WzVG]
Roles :
Role[ROLE_ADMIN]
User[user,user,$2a$10$SJehR9Mv2VdyRZo9F0rXa.hKAoGLhJg6kSdyfExi40mEJrNOj0BTq]
Roles :
Role[ROLE_USER]
User[guest,guest,$2a$10$ubyWJb/vg2XZnUOAUjspZuz9jpHP3fIbPTbwQU115EtLdeSZ2PB7q]
Roles :
Role[ROLE_GUEST]
User[x,x,$2a$10$kEXA56wpKHFReVqwQTyWguKguK8I4uhA2zb6t3wGxag8Dyv7AhLom]
Roles :
Role[ROLE_GUEST]

8.4.13.7. استنتاج مؤقت

تمت إضافة الفئات الضرورية لـ Spring Security مع إجراء تغييرات طفيفة على المشروع الأصلي. للتلخيص:

  • إضافة تبعية لـ Spring Security في ملف [pom.xml
  • إنشاء ثلاثة جداول إضافية في قاعدة البيانات؛
  • إنشاء كيانات JPA ومكونات Spring في حزمة [rdvmedecins.security

ينبع هذا السيناريو المواتي للغاية من حقيقة أن الجداول الثلاثة المضافة إلى قاعدة البيانات مستقلة عن الجداول الموجودة. بل كان بإمكاننا وضعها في قاعدة بيانات منفصلة. كان ذلك ممكنًا لأننا قررنا أن المستخدم موجود بشكل مستقل عن الأطباء والعملاء. لو كان هؤلاء الأخيرون مستخدمين محتملين، لكان علينا إنشاء روابط بين جدول [USERS] وجداول [MEDECINS] و[CLIENTS]. وكان ذلك سيؤثر بشكل كبير على المشروع الحالي.

8.4.13.8. مشروع STS لطبقة [web]

يتطور مشروع [rdvmedecins-webjson] على النحو التالي[1]:

يجب إجراء التغييرات الرئيسية في ملف [rdvmedecins.web.config]، حيث يجب تكوين Spring Security. وهناك تغييرات ثانوية أخرى في فئتي [AppConfig] و[ApplicationModel]. وقد سبق أن تعاملنا مع فئة تكوين Spring Security:


package hello;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
 
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

سنتبع نفس الإجراء:

  • السطر 11: تعريف فئة تمتد من فئة [WebSecurityConfigurerAdapter
  • السطر 13: تعريف طريقة [configure(HttpSecurity http)] التي تحدد حقوق الوصول إلى عناوين URL المختلفة لخدمة الويب؛
  • السطر 19: تعريف طريقة [configure(AuthenticationManagerBuilder auth)] التي تحدد المستخدمين وأدوارهم؛

يتم التعامل مع تكوين Spring Security بواسطة فئة [SecurityConfig]:


package rdvmedecins.web.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;
 
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;
    @Autowired
    private ApplicationModel application;
 
    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // authentication is performed by bean [appUserDetailsService]
        // the password is encrypted using the BCrypt hash algorithm
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // secure application?
        if (application.isSecured()) {
            // the password is transmitted by the header Authorization: Basic xxxx
            http.httpBasic();
            // the HTTP OPTIONS method must be authorized for all
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // only the ADMIN role can use the application
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // all URL
                    .hasRole("ADMIN");
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • السطر 15: فئة [SecurityConfig] هي فئة تكوين Spring؛
  • السطر 16: لإعداد أمان المشروع؛
  • السطران 19-20: يتم إدخال فئة [AppUserDetails]، التي توفر الوصول إلى مستخدمي التطبيق؛
  • السطران 21-22: يتم إدخال فئة [ApplicationModel]، التي تعمل كذاكرة تخزين مؤقتة لتطبيق الويب. نختار استخدامها هنا أيضًا لتكوين تطبيق الويب في مكان واحد. وهي تحدد القيمة المنطقية [isSecured] في السطر 36. هذه القيمة المنطقية تؤمن (true) أو لا تؤمن (false) تطبيق الويب؛
  • الأسطر 25–29: تحدد طريقة [configure(HttpSecurity http)] المستخدمين وأدوارهم. وهي تأخذ نوع [AuthenticationManagerBuilder] كمعلمة. يتم إثراء هذه المعلمة بمعلومتين (السطر 28):
    • إشارة إلى [appUserDetailsService] من السطر 20، والتي توفر الوصول إلى المستخدمين المسجلين. لاحظ هنا أن حقيقة تخزينهم في قاعدة بيانات لم يتم ذكرها صراحةً. لذلك، يمكن أن يكونوا في ذاكرة التخزين المؤقت، أو يتم توفيرهم بواسطة خدمة ويب، إلخ.
    • نوع التشفير المستخدم لكلمة المرور. تذكر أننا استخدمنا خوارزمية BCrypt؛
  • الأسطر 38-47: تحدد طريقة [configure(HttpSecurity http)] حقوق الوصول إلى عناوين URL لخدمة الويب؛
  • السطر 34: رأينا في المشروع التمهيدي أن Spring Security تدير افتراضيًا رمز CSRF (تزوير الطلبات عبر المواقع) الذي يجب على المستخدم الذي يحاول المصادقة إرساله مرة أخرى إلى الخادم. هنا، يتم تعطيل هذه الآلية. بالاقتران مع القيمة المنطقية (isSecured=false)، يسمح هذا باستخدام تطبيق الويب بدون أمان؛
  • السطر 38: نقوم بتمكين المصادقة عبر رؤوس HTTP. يجب على العميل إرسال رأس HTTP التالي:
Authorization:Basic code

حيث code هو ترميز Base64 لسلسلة login:password. على سبيل المثال، ترميز Base64 لسلسلة admin:admin هو YWRtaW46YWRtaW4=. لذلك، سيقوم المستخدم الذي يستخدم اسم المستخدم [admin] وكلمة المرور [admin] بإرسال رأس HTTP التالي للمصادقة:

Authorization:Basic YWRtaW46YWRtaW4=
  • الأسطر 40–42: تشير إلى أن جميع عناوين URL لخدمة الويب متاحة للمستخدمين الذين لديهم دور [ROLE_ADMIN]. وهذا يعني أن المستخدم الذي لا يمتلك هذا الدور لا يمكنه الوصول إلى خدمة الويب؛
  • السطر 47: قد يتم تخزين كلمة مرور المستخدم في جلسة عمل أو لا يتم تخزينها. إذا تم تخزينها، فلن يحتاج المستخدم إلى المصادقة إلا في المرة الأولى. وفي الطلبات اللاحقة، لن يُطلب منه تقديم بيانات اعتماده. هنا، اخترنا وضعًا بدون جلسة عمل. ويجب أن يكون كل طلب مصحوبًا ببيانات اعتماد أمنية؛

يتم تحديث فئة [AppConfig]، التي تقوم بتكوين التطبيق بأكمله، على النحو التالي:

  

package rdvmedecins.web.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
import rdvmedecins.config.DomainAndPersistenceConfig;
 
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
 
}
  • يحدث التغيير في السطر 11: تمت إضافة فئة التكوين [SecurityConfig

وأخيرًا، تم تحسين فئة [ApplicationModel] بإضافة قيمة منطقية:


@Component
public class ApplicationModel implements IMetier {
 
...
    // configuration data
    private boolean secured = false;
 
    public boolean isSecured() {
        return secured;
}
  • السطر 6: قم بتعيين القيمة المنطقية [secured] إلى [true / false] حسب ما إذا كنت تريد تمكين الأمان أم لا.

8.4.13.9. اختبار خدمة الويب

سنقوم باختبار خدمة الويب باستخدام عميل Chrome [Advanced Rest Client]. سنحتاج إلى تحديد رأس مصادقة HTTP:

Authorization:Basic code

حيث [code] هي السلسلة المشفرة بـ Base64 [login:password]. لإنشاء هذا الرمز، يمكنك استخدام البرنامج التالي:

  

package rdvmedecins.helpers;
 
import org.springframework.security.crypto.codec.Base64;
 
public class Base64Encoder {
 
    public static void main(String[] args) {
        // we expect two arguments: login password
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // we retrieve the two arguments
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // encode the string
        byte[] data = Base64.encode(chaîne.getBytes());
        // displays its Base64 encoding
        System.out.println(new String(data));
    }
 
}

إذا قمنا بتشغيل هذا البرنامج مع الحجتين [admin admin]:

  

نحصل على النتيجة التالية:

YWRtaW46YWRtaW4=

الآن بعد أن عرفنا كيفية إنشاء رأس مصادقة HTTP، نقوم بتشغيل خدمة الويب الآمنة الآن:


@Component
public class ApplicationModel implements IMetier {
...
private boolean secured = true;

ثم، باستخدام عميل Chrome [Advanced Rest Client]، نطلب قائمة بجميع الأطباء:

  • في [1]، نطلب عنوان URL الخاص بالأطباء؛
  • في [2]، باستخدام طريقة GET؛
  • في [3]، نقدم رأس مصادقة HTTP. الرمز [YWRtaW46YWRtaW4=] هو ترميز Base64 للسلسلة [admin:admin
  • في [4]، نرسل طلب HTTP؛

استجابة الخادم هي كما يلي:

  • في [1]، رأس مصادقة HTTP؛
  • في [2]، يقوم الخادم بإرجاع استجابة JSON؛
  • في [3]، قائمة برؤوس HTTP المتعلقة بأمن تطبيق الويب؛

نجحنا في الحصول على قائمة الأطباء:

 

الآن دعونا نجرب طلب HTTP برأس مصادقة غير صحيح. تكون الاستجابة عندئذ كما يلي:

  • في [1] و[3]: رأس المصادقة HTTP؛
  • في [2]: استجابة خدمة الويب؛

الآن، دعونا نجرب المستخدم "user / user". إنه موجود ولكنه لا يملك حق الوصول إلى خدمة الويب. إذا قمنا بتشغيل برنامج الترميز Base64 مع الحجتين [user user]:

  

نحصل على النتيجة التالية:

dXNlcjp1c2Vy
  • في [1] و[3]: رأس المصادقة HTTP؛
  • في [2]: استجابة خدمة الويب. وهي تختلف عن السابقة، التي كانت [401 غير مصرح به]. هذه المرة، تمت مصادقة المستخدم بنجاح ولكنه لا يمتلك أذونات كافية للوصول إلى عنوان URL؛

أصبحت خدمة الويب الآمنة جاهزة للعمل الآن. سنقوم بتوسيعها للسماح بالطلبات عبر النطاقات. تم ذكر هذا المطلب في الوثيقة [AngularJS / Spring 4 Tutorial]، وعلى الرغم من أنه لا ينطبق هنا، إلا أننا سنعالجه على أي حال.

8.4.14. تنفيذ الطلبات عبر النطاقات

دعونا ندرس مسألة الطلبات عبر النطاقات. في الوثيقة [AngularJS / Spring 4 Tutorial]، نقوم بتطوير تطبيق عميل/خادم حيث يكون العميل تطبيق AngularJS:

  • تأتي صفحات HTML/CSS/JS لتطبيق Angular من الخادم [1]؛
  • في [2]، ترسل خدمة [dao] طلبًا إلى خادم آخر، وهو الخادم [2]. حسنًا، هذا أمر محظور من قبل المتصفح الذي يشغل تطبيق Angular لأنه يمثل ثغرة أمنية. لا يمكن للتطبيق الاستعلام إلا عن الخادم الذي نشأ منه، أي الخادم [1]؛

في الواقع، من غير الدقيق القول إن المتصفح يمنع تطبيق Angular من الاستعلام عن الخادم [2]. فهو في الواقع يستعلم عنه ليسأله عما إذا كان يسمح لعميل لا ينشأ منه بالاستعلام عنه. تسمى تقنية المشاركة هذه CORS (مشاركة الموارد عبر الأصول). يمنح الخادم [2] الإذن عن طريق إرسال رؤوس HTTP محددة.

لتوضيح المشكلات التي قد تنشأ، سننشئ تطبيق عميل/خادم حيث:

  • سيكون الخادم هو خادم الويب/JSON الخاص بنا؛
  • سيكون العميل عبارة عن صفحة HTML بسيطة مزودة برمز JavaScript الذي سيقوم بإرسال الطلبات إلى خادم الويب/JSON؛

8.4.14.1. مشروع العميل

  

المشروع هو مشروع Maven يحتوي على ملف [pom.xml] التالي:


<?xml version="1.0" encoding="UTF-8"?>
<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>rdvmedecins-webjson-client-cors</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
 
        <name>rdvmedecins-webjson-client-cors</name>
        <description>Client for webjson server</description>
 
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
                <relativePath /> <!-- lookup parent from repository -->
        </parent>
 
        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <start-class>istia.st.rdvmedecins.Client</start-class>
                <java.version>1.8</java.version>
        </properties>
 
        <dependencies>
                <!-- spring MVC -->
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
        </dependencies>
</project>
  • الأسطر 14–19: هذا مشروع Spring Boot؛
  • الأسطر 29–32: نستخدم التبعية [spring-boot-starter-web]، التي تتضمن خادم Tomcat و Spring MVC؛

صفحة HTML هي كما يلي:

 

يتم إنشاؤه بواسطة الكود التالي:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
    <h2>Client du service web / jSON</h2>
    <form id="formulaire">
        <!--  method HTTP -->
        Méthode HTTP :
        <!--  -->
        <input type="radio" id="get" name="method" value="get" checked="checked" />GET
        <!--  -->
        <input type="radio" id="post" name="method" value="post" />POST
        <!--  URL -->
        <br /> <br />URL cible : <input type="text" id="url" size="30"><br />
        <!-- posted value -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
        <!-- validation button -->
        <br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
    </form>
    <hr />
    <h2>Réponse du serveur</h2>
    <div id="response"></div>
</body>
</html>
  • السطر 6: نستورد مكتبة jQuery؛
  • السطر 7: نستورد الكود الذي سنكتبه؛

الكود [client.js] هو كما يلي:


// global data
var url;
var posted;
var response;
var method;
 
function requestServer() {
    // retrieve information from the form
    var urlValue = url.val();
    var postedValue = posted.val();
    method = document.forms[0].elements['method'].value;
    // make a manual Ajax call
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}
 
function doGet(url) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8080' + url,
        type : 'GET',
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(jqXHR.responseText);
        }
    })
}
 
function doPost(url, posted) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8080' + url,
        type : 'POST',
        contentType : 'application/json',
        data : posted,
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(jqXHR.responseText);
        }
    })
}
 
// document loading
$(document).ready(function() {
    // retrieve page component references
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});

سنترك للقارئ مهمة فهم هذا الكود. فقد تمت تغطية كل شيء في وقت أو آخر. ومع ذلك، هناك بعض الأسطر التي تستحق التوضيح:

  • السطر 11:
    • يشير [document] إلى المستند الذي تم تحميله بواسطة المتصفح، والمعروف باسم DOM (نموذج كائن المستند)،
    • [document.forms[0]] يشير إلى النموذج الأول في المستند؛ قد يحتوي المستند على نماذج متعددة. هنا، يوجد نموذج واحد فقط،
    • [document.forms[0].elements['method']] يشير إلى عنصر النموذج الذي يحتوي على السمة [name='method']. وهناك اثنان منهما:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • السطر 11:
    • [document.forms[0].elements['method'].value] هي القيمة التي سيتم إرسالها للمكون الذي يحمل السمة [name='method']. ونحن نعلم أن القيمة المرسلة هي قيمة السمة [value] الخاصة بزر الاختيار المحدد. وبالتالي، ستكون هنا إحدى السلاسل ['get', 'post'];
  • الأسطر 23-25: نحن نتواصل مع خادم يتطلب رأس HTTP [Authorization: Basic code]. نقوم بإنشاء هذا الرأس للمستخدم [admin / admin]، وهو الوحيد المصرح له بالاستعلام عن الخادم؛
  • السطر 26: سيقوم المستخدم بإدخال عناوين URL بالشكل [/getAllDoctors, /deleteAppointment, ...]. لذلك يجب إكمال عناوين URL هذه؛
  • السطر 28: يعرض الخادم JSON، وهو تنسيق نصي. نحدد النوع [text/plain] كنوع الاستجابة بحيث يتم عرضه تمامًا كما تم استلامه؛
  • السطر 33: عرض استجابة الخادم النصية؛
  • السطر 39: يعرض أي رسائل خطأ بتنسيق نصي؛
  • السطر 52: للإشارة إلى أن العميل يرسل JSON؛

في تطبيق العميل/الخادم الذي نقوم ببنائه:

  • العميل هو تطبيق ويب متاح على عنوان URL [http://localhost:8081]. هذا هو التطبيق الذي نقوم ببنائه حاليًا؛
  • الخادم هو تطبيق ويب متاح على عنوان URL [http://localhost:8080]. هذا هو خادم الويب/JSON الخاص بنا؛

نظرًا لأن العميل لا يعمل على نفس المنفذ الذي يعمل عليه الخادم، تنشأ مشكلة الطلبات عبر النطاقات. [http://localhost:8080] و [http://localhost:8081] هما نطاقان مختلفان.

تطبيق Spring Boot هو تطبيق وحدة تحكم يتم تشغيله بواسطة الفئة القابلة للتنفيذ التالية [Client]:


package istia.st.rdvmedecins;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {
 
    public static void main(String[] args) {
        SpringApplication.run(Client.class, args);
    }
 
    // static pages
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
    }
 
    // configuration dispatcherServlet
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }
 
    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }
 
    // embedded Tomcat server
    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8081);
    }
 
}
  • السطر 14: فئة [Client] هي فئة تكوين Spring؛
  • السطر 15: يتم تكوين تطبيق Spring MVC. يؤدي هذا التعليق التوضيحي إلى تشغيل عدد من التكوينات التلقائية؛
  • السطر 16: لتجاوز بعض القيم الافتراضية لإطار عمل Spring MVC، يجب توسيع فئة [WebMvcConfigurerAdapter
  • الأسطر 23–26: تسمح لك طريقة [addResourceHandlers] بتحديد الدلائل التي توجد فيها الموارد الثابتة للتطبيق (HTML، CSS، JS، إلخ). هنا، نحدد الدليل [static] الموجود في مسار فئات المشروع:
  
  • الأسطر 29–37: تكوين عنصر [dispatcherServlet]، الذي يحدد سيرفلت Spring MVC؛
  • الأسطر 40-43: سيعمل خادم Tomcat المدمج على المنفذ 8081؛

8.4.14.2. عنوان URL [/getAllMedecins]

نقوم بتشغيل:

  • خادم الويب/JSON على المنفذ 8080؛
  • العميل لهذا الخادم على المنفذ 8081؛

ثم نطلب عنوان URL [http://localhost:8081/client.html] [1]:

  • في [2]، نقوم بإجراء طلب GET على عنوان URL [http://localhost:8080/getAllMedecins

لا نتلقى أي استجابة من الخادم. وعندما ننظر إلى وحدة تحكم المطور (Ctrl-Shift-I)، نرى خطأً:

  • في [1]، نحن في علامة التبويب [الشبكة]؛
  • في [2]، نرى أن طلب HTTP الذي تم إرساله ليس [GET] بل [OPTIONS]. في حالة الطلب عبر النطاقات، يتحقق المتصفح من الخادم للتأكد من استيفاء شروط معينة عن طريق إرسال طلب HTTP [OPTIONS]. في هذه الحالة، الطلبات هي تلك المشار إليها بالدوائر [5-6]؛
  • في [5]، يسأل المتصفح عما إذا كان يمكن الوصول إلى عنوان URL الهدف باستخدام GET. يطلب رأس الطلب [Access-Control-Request-Method] استجابة برأس HTTP [Access-Control-Allow-Methods] يشير إلى قبول الطريقة المطلوبة؛
  • في [5]، يرسل المتصفح رأس HTTP [Origin: http://localhost:8081]. يطلب هذا الرأس استجابة في رأس HTTP [Access-Control-Allow-Origin] تشير إلى قبول المنشأ المحدد؛
  • في [6]، يسأل المتصفح عما إذا كان رأسا HTTP [Accept] و [Authorization] مقبولين. يتوقع رأس الطلب [Access-Control-Request-Headers] استجابة برأس HTTP [Access-Control-Allow-Headers] يشير إلى أن الرؤوس المطلوبة مقبولة؛
  • يحدث خطأ في [3]. يؤدي النقر على الرمز إلى ظهور الخطأ [4]؛
  • في [4]، تشير الرسالة إلى أن الخادم لم يرسل رأس HTTP [Access-Control-Allow-Origin]، الذي يحدد ما إذا كان مصدر الطلب مقبولاً أم لا؛
  • في [7]، يمكننا أن نرى أن الخادم لم يرسل هذا الرأس بالفعل. ونتيجة لذلك، رفض المتصفح إجراء طلب HTTP GET الذي تم طلبه في البداية؛

نحتاج إلى تعديل خادم الويب/JSON. نقوم بإجراء تغيير أولي في [ApplicationModel]، وهو أحد عناصر تكوين خدمة الويب:

 

@Component
public class ApplicationModel implements IMetier {
 
    ...
    // configuration data
    private boolean corsAllowed = true;
    private boolean secured = true;
 
...
    public boolean isCorsAllowed() {
        return corsAllowed;
}
  • السطر 6: نقوم بإنشاء متغير منطقي يشير إلى ما إذا كان يتم قبول العملاء خارج نطاق الخادم أم لا؛
  • الأسطر 10-12: الطريقة للوصول إلى هذه المعلومات؛

ثم نقوم بإنشاء وحدة تحكم Spring MVC جديدة:

  

فيما يلي فئة [RdvMedecinsCorsController]:


package rdvmedecins.web.controllers;
 
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
import rdvmedecins.web.models.ApplicationModel;
 
@Controller
public class RdvMedecinsCorsController {
 
    @Autowired
    private ApplicationModel application;
 
    // sending options to the customer
    public void sendOptions(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // we authorize GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        sendOptions(origin, response);
    }
}
  • السطران 12-13: فئة [RdvMedecinsCorsController] هي وحدة تحكم Spring؛
  • الأسطر 33–36: تعريف إجراء يتعامل مع عنوان URL [/getAllMedecins] عند طلبه باستخدام طريقة HTTP [OPTIONS
  • السطر 34: تقبل طريقة [getAllMedecins] المعلمات التالية:
    • الكائن [@RequestHeader(value = "Origin", required = false)] الذي يسترد رأس HTTP [Origin] من الطلب. وقد أرسل هذا الرأس مرسل الطلب:
Origin:http://localhost:8081

نحدد أن رأس HTTP [Origin] اختياري [required = false]. في هذه الحالة، إذا كان الرأس مفقودًا، فستكون قيمة المعلمة [String origin] هي null. مع [required = true]، وهي القيمة الافتراضية، يتم إلقاء استثناء إذا كان الرأس مفقودًا. أردنا تجنب هذا السيناريو؛

  • السطر 34:
    • كائن [HttpServletResponse response] الذي سيتم إرساله إلى العميل الذي قدم الطلب؛

يتم حقن هذين المعلمتين بواسطة Spring؛

  • السطر 35: نحيل معالجة الطلب إلى الطريقة الموجودة في الأسطر 19-30؛
  • السطور 15-16: يتم إدخال كائن [ApplicationModel
  • الأسطر 21-23: إذا تم تكوين التطبيق لقبول الطلبات عبر النطاقات، وإذا أرسل المرسل رأس HTTP [Origin]، وإذا كان هذا الأصل يبدأ بـ [http://localhost]، فإننا نقبل الطلب عبر النطاقات؛ وإلا، فإننا نرفضه؛
  • السطر 25: إذا كان العميل في المجال [http://localhost:port]، يتم إرسال رأس HTTP:

Access-Control-Allow-Origin:  http://localhost:port

مما يعني أن الخادم يقبل أصل العميل؛

  • السطر 25: لقد حددنا رأسين HTTP محددين في طلب HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

استجابةً لرأس HTTP [Access-Control-Request-X]، يرد الخادم برأس HTTP [Access-Control-Allow-X] يحدد ما هو مسموح به. الأسطر 23–26 تكرر ببساطة طلب العميل للإشارة إلى أنه قد تم قبوله؛

نحن الآن جاهزون لإجراء المزيد من الاختبارات. نقوم بتشغيل الإصدار الجديد من خدمة الويب ونجد أن المشكلة لم تتغير. لم يتغير شيء. إذا أضفنا إخراج وحدة التحكم في السطر 35 أعلاه، فلن يتم عرضه أبدًا، مما يشير إلى أن طريقة [getAllMedecins] في السطر 34 لم يتم استدعاؤها أبدًا.

بعد إجراء بعض الأبحاث، نكتشف أن Spring MVC تتعامل مع طلبات HTTP [OPTIONS] بنفسها باستخدام معالجتها الافتراضية. لذلك، فإن Spring هي التي تستجيب دائمًا، وليس أبدًا الطريقة [getAllMedecins] في السطر 34. يمكن تغيير هذا السلوك الافتراضي لـ Spring MVC. نقوم بتعديل فئة [WebConfig] الحالية:

  

package rdvmedecins.web.config;
 
...
import org.springframework.web.servlet.DispatcherServlet;
 
@Configuration
public class WebConfig {
 
    // dispatcherservlet configuration for CORS headers
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
 
    // mapping jSON
...
  • السطران 10-11: يُستخدم bean [dispatcherServlet] لتعريف السيرفلت الذي يتعامل مع طلبات العميل. هنا، يكون من النوع [DispatcherServlet]، وهو السيرفلت من إطار عمل Spring MVC؛
  • السطر 12: نقوم بإنشاء مثيل من النوع [DispatcherServlet
  • السطر 13: نوجه السيرفلت لإعادة توجيه طلبات HTTP [OPTIONS] إلى التطبيق؛
  • السطر 14: نقوم بتشغيل السيرفلت الذي تم تكوينه بهذه الطريقة؛

نعيد تشغيل الاختبارات باستخدام هذا التكوين الجديد. ونحصل على النتيجة التالية:

  • في [1]، نرى أن هناك طلبين HTTP إلى عنوان URL [http://localhost:8080/getAllMedecins
  • في [2]، طلب [OPTIONS
  • في [3]، الرؤوس الثلاثة لـ HTTP التي قمنا بتكوينها للتو في استجابة الخادم؛

الآن دعونا نفحص الطلب الثاني:

  • في [1]، الطلب قيد الفحص؛
  • في [2]، هذا هو طلب GET. بفضل طلب [OPTIONS] الأول، تلقى المتصفح المعلومات التي طلبها. وهو الآن يقوم بتنفيذ طلب [GET] الذي تم طلبه في البداية؛
  • في [3]، استجابة الخادم؛
  • في [4]، يرسل الخادم JSON؛
  • في [5]، حدث خطأ؛
  • في [6]، رسالة الخطأ؛

من الصعب شرح ما حدث هنا. استجابة الخادم [3] طبيعية [HTTP/1.1 200 OK]. لذلك، يجب أن يكون لدينا المستند المطلوب. من الممكن أن يكون الخادم قد أرسل المستند بالفعل، ولكن المتصفح يمنع استخدامه لأنه يتطلب، بالنسبة لطلب GET أيضًا، أن تتضمن الاستجابة رأس HTTP [Access-Control-Allow-Origin:http://localhost:8081].

نقوم بتعديل وحدة التحكم [RdvMedecinsController] على النحو التالي:


    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;
...
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllMedecins(HttpServletResponse httpServletResponse,
            @RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
        // the answer
        Response<List<Medecin>> response;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
...
  • السطران 1-2: يتم حقن وحدة التحكم [RdvMedecinsCorsController
  • السطران 7-8: يتم إدخال كائن HttpServletResponse، الذي يغلف الاستجابة المراد إرسالها إلى العميل، ورأس HTTP [Origin] في معلمات طريقة [getAllMedecins
  • السطر 12: يتم استدعاء طريقة [sendOptions] الخاصة بوحدة التحكم [RdvMedecinsCorsController] — وهي نفس الطريقة التي تم استدعاؤها لمعالجة طلب HTTP [OPTIONS]. وبالتالي، سترسل نفس رؤوس HTTP الخاصة بذلك الطلب؛

بعد هذا التعديل، تكون النتائج كما يلي:

 

لقد نجحنا في الحصول على قائمة الأطباء.

8.4.14.3. عناوين URL [GET] الأخرى

سنلقي الآن نظرة على عناوين URL الأخرى التي تم الاستعلام عنها عبر طلب GET. في وحدات التحكم، يتبع كود الإجراءات التي تتعامل معها نفس نمط الإجراءات التي تعاملت سابقًا مع عنوان URL [/getAllMedecins]. يمكن للقارئ التحقق من الكود في الأمثلة المرفقة مع هذا المستند. وإليك مثال:

في [RdvMedecinsCorsController]


    // list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
    public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin,    HttpServletResponse response) {
        sendOptions(origin, response);
}

في [RdvMedecinsController]


    // list of doctor's appointments
    @RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour,
            HttpServletResponse httpServletResponse, @RequestHeader(value = "Origin", required = false) String origin)
                    throws JsonProcessingException {
        // the answer
        Response<List<Rv>> response = null;
        boolean erreur = false;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
...

فيما يلي بعض لقطات الشاشة لعملية التنفيذ:

 
 
 
 
 
 

8.4.14.4. عناوين URL [POST]

دعونا ندرس السيناريو التالي:

  • نقوم بإرسال طلب POST [1] إلى عنوان URL [2]؛
  • في [3]، القيمة المرسلة. هذه سلسلة JSON؛
  • بشكل عام، نحاول حذف الموعد ذي [id] 100؛

نحن لا نقوم بتعديل أي كود في هذه المرحلة. والنتيجة التي تم الحصول عليها هي كما يلي:

  • في [1]، كما هو الحال مع طلبات [GET]، يقوم المتصفح بإرسال طلب [OPTIONS
  • في [2]، يطلب المتصفح إذن الوصول لطلب [POST]. في السابق، كان [GET
  • في [3]، يطلب الإذن لإرسال رؤوس HTTP [accept، authorization، content-type]. في السابق، لم يكن لدينا سوى الرؤوس الأولى والثانية؛

نقوم بتعديل طريقة [RdvMedecinsCorsController.sendOptions] على النحو التالي:


    public void sendOptions(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // we authorize GET
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
  • السطر 9: أضفنا رأس HTTP [Content-Type] (لا يهم استخدام الأحرف الكبيرة أو الصغيرة)؛
  • السطر 11: أضفنا طريقة HTTP [POST

وهذا يعني أن طرق [POST] تُعالج بنفس طريقة معالجة طلبات [GET]. فيما يلي مثال على عنوان URL [/deleteAppointment]:

في [RdvMedecinsController]


    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public String supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse httpServletResponse,
            @RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
        // the answer
        Response<Void> response = null;
        boolean erreur = false;
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
        // application status
        if (messages != null) {
...

في [RdvMedecinsCorsController]


    @RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
    public void supprimerRv(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        sendOptions(origin, response);
}

والنتيجة هي كما يلي:

 

بالنسبة لعنوان URL [/addRv]، يتم الحصول على النتيجة التالية:

 

8.4.14.5. الخلاصة

يدعم تطبيقنا الآن الطلبات عبر النطاقات. يمكن تمكين هذه الطلبات أو تعطيلها من خلال التكوين في فئة [ApplicationModel]:


    // données de configuration
    private boolean corsAllowed = false;

8.5. عميل خدمة الويب / JSON

لنعد إلى البنية العامة للتطبيق الذي نريد إنشاؤه:

تمت كتابة الجزء العلوي من الرسم التخطيطي. هذا هو خادم الويب/JSON. سنقوم الآن بمعالجة الجزء السفلي، بدءًا من طبقة [DAO] الخاصة به. سنقوم بكتابة هذه الطبقة ثم اختبارها باستخدام عميل وحدة التحكم. ستكون بنية الاختبار كما يلي:

8.5.1. مشروع عميل وحدة التحكم

سيكون مشروع STS لعميل وحدة التحكم كما يلي:

  

8.5.2. تكوين 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.rdvmedecins</groupId>
        <artifactId>rdvmedecins-webjson-client-console</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>rdvmedecins-webjson-client-console</name>
        <description>Client console du serveur web / jSON</description>
 
        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <java.version>1.8</java.version>
        </properties>
 
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.6.RELEASE</version>
                <relativePath /> <!-- lookup parent from repository -->
        </parent>
 
        <dependencies>
                <!-- Spring -->
                <dependency>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-web</artifactId>
                </dependency>
                <!-- jSON library used by Spring -->
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-core</artifactId>
                </dependency>
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-databind</artifactId>
                </dependency>
                <!-- component used by Spring RestTemplate -->
                <dependency>
                        <groupId>org.apache.httpcomponents</groupId>
                        <artifactId>httpclient</artifactId>
                </dependency>
        </dependencies>
</project>
  • الأسطر 15–20: مشروع Spring Boot الأصلي؛
  • الأسطر 24–27: يعتمد خادم الويب/عميل وحدة التحكم JSON على مكون يسمى [RestTemplate] توفره التبعية [spring-web
  • الأسطر 29–36: يتطلب تسلسل كائنات JSON وإلغاء تسلسلها مكتبة JSON. نستخدم نسخة معدلة من مكتبة Jackson التي يستخدمها Spring Web؛
  • الأسطر 38–41: على المستوى الأدنى، يتواصل مكون [RestTemplate] مع الخادم عبر مآخذ TCP/IP. نريد تعيين [timeout] لهذه المآخذ، أي الحد الأقصى لوقت الانتظار لاستلام استجابة من الخادم. لا يسمح لنا مكون [RestTemplate] بتعيين هذا. للقيام بذلك، سنمرر مكونًا منخفض المستوى مقدمًا من التبعية [org.apache.httpcomponents.httpclient] إلى منشئ [RestTemplate]. هذه التبعية هي التي ستسمح لنا بتعيين [timeout] للاتصال؛

8.5.3. حزمة [rdvmedecins.client.entities]

  

تحتوي حزمة [rdvmedecins.client.entities] على جميع الكيانات التي ترسلها خدمة الويب / JSON عبر عناوين URL المختلفة الخاصة بها. لن ندخل في تفاصيلها مرة أخرى. ويكفي القول إن كيانات JPA [Client، Slot، Doctor، Appointment، Person] قد تم تجريدها من جميع تعليقات JPA الخاصة بها وكذلك تعليقات JSON الخاصة بها. وهنا، على سبيل المثال، فئة [Appointment]:


package rdvmedecins.client.entities;
 
import java.util.Date;
 
public class Rv extends AbstractEntity {
    private static final long serialVersionUID = 1L;
 
    // day of appointment
    private Date jour;
 
    // an appointment is linked to a customer
    private Client client;
 
    // an appointment is linked to a time slot
    private Creneau creneau;
 
    // foreign keys
    private long idClient;
    private long idCreneau;
 
    // default builder
    public Rv() {
    }
 
    // with parameters
    public Rv(Date jour, Client client, Creneau creneau) {
        this.jour = jour;
        this.client = client;
        this.creneau = creneau;
    }
 
    // toString
    public String toString() {
        return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
    }
 
// getters and setters
...
}

8.5.4. حزمة [rdvmedecins.client.requests]

  

تحتوي حزمة [rdvmedecins.client.requests] على الفئتين اللتين يتم إرسال قيم JSON الخاصة بهما إلى عناوين URL [/ajouterRv] و [supprimerRv]. وهما مطابقتان لنظيرتيهما على جانب الخادم.

8.5.5. حزمة [rdvmedecins.client.responses]

  

[Response] هو نوع جميع استجابات خدمات الويب / JSON. وهو نوع عام:


package rdvmedecins.client.responses;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}
  • السطر 5: يختلف النوع [T] اعتمادًا على عنوان URL لخدمة الويب / JSON؛

8.5.6. حزمة [rdvmedecins.client.dao]

  
  • [IDao] هي واجهة طبقة [DAO]، و[Dao] هي تطبيقها. سنعود إلى هذا التطبيق لاحقًا؛

8.5.7. حزمة [rdvmedecins.client.config]

  

تقوم فئة [DaoConfig] بتكوين التطبيق. وفيما يلي شفرة البرمجة الخاصة بها:


package rdvmedecins.client.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
 
    @Bean
    public RestTemplate restTemplate() {
        // creation of the RestTemplate component
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // result
        return restTemplate;
    }
 
    // mappers jSON
 
    @Bean
    public ObjectMapper jsonMapper(){
        return new ObjectMapper();
    }
 
    @Bean
    public ObjectMapper jsonMapperShortCreneau() {
        ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
        return jsonMapperShortCreneau;
    }
 
    @Bean
    public ObjectMapper jsonMapperLongRv() {
        ObjectMapper jsonMapperLongRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
        SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
        jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",
                creneauFilter));
        return jsonMapperLongRv;
    }
 
    @Bean
    public ObjectMapper jsonMapperShortRv() {
        ObjectMapper jsonMapperShortRv = new ObjectMapper();
        SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
        jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
        return jsonMapperShortRv;
    }
 
}
  • السطر 13: فئة [DaoConfig] هي فئة تكوين Spring؛
  • السطر 14: سيتم البحث في حزمة [rdvmedecins.client.dao] عن مكونات Spring. سيتم العثور على مكون [Dao] هناك؛
  • الأسطر 17-24: تعريف عنصر Spring فريد باسم [restTemplate] (اسم الطريقة). تُرجع هذه الطريقة مثيل [RestTemplate]، وهو الأداة الأساسية التي يوفرها Spring للتواصل مع خدمة ويب أو JSON؛
  • السطر 21: يمكننا كتابة [RestTemplate restTemplate = new RestTemplate();]. وهذا يكفي في معظم الحالات. ولكن هنا، نريد تعيين [timeouts] للعميل. للقيام بذلك، نقوم بحقن مكون منخفض المستوى من النوع [HttpComponentsClientHttpRequestFactory] (السطر 20) في مكون [RestTemplate]، مما سيسمح لنا بتعيين هذه [timeouts]. تم توفير التبعية المطلوبة لـ Maven؛
  • الأسطر 28–57: تعريف مخططات JSON. هذه هي مخططات JSON المستخدمة على جانب الخادم (انظر القسم 8.4.11.3) لتسلسل النوع T من استجابة [Response<T>]. سيتم الآن استخدام هذه المحولات نفسها على جانب العميل لإلغاء تسلسل النوع T؛

8.5.8. واجهة [IDao]

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

تعمل طبقة [DAO] كمحول بين طبقة [console] وعناوين URL التي تعرضها خدمة الويب / JSON. وستكون واجهة [IDao] الخاصة بها على النحو التالي:


package rdvmedecins.client.dao;
 
import java.util.List;
 
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
 
public interface IDao {
    // Web service url
    public void setUrlServiceWebJson(String url);
 
    // timeout
    public void setTimeout(int timeout);
 
    // authentication
    public void authenticate(User user);
 
    // customer list
    public List<Client> getAllClients(User user);
 
    // list of doctors
    public List<Medecin> getAllMedecins(User user);
 
    // list of physician slots
    public List<Creneau> getAllCreneaux(User user, long idMedecin);
 
    // find a customer identified by its id
    public Client getClientById(User user, long id);
 
    // find a customer identified by its id
    public Medecin getMedecinById(User user, long id);
 
    // find an Rv identified by its id
    public Rv getRvById(User user, long id);
 
    // find a time slot identified by its id
    public Creneau getCreneauById(User user, long id);
 
    // add a RV to the list
    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
 
    // delete a RV
    public void supprimerRv(User user, long idRv);
 
    // list of doctor's appointments on a given day
    public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
 
    // agenda
    public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
 
}
  • السطر 14: الطريقة الخاصة بتعيين عنوان URL الجذري لخدمة الويب / JSON، على سبيل المثال [http://localhost:8080
  • السطر 17: الطريقة المستخدمة لتعيين [timeouts] من جانب العميل. نريد التحكم في هذا المعامل لأن بعض عملاء HTTP قد يستغرقون وقتًا طويلاً جدًا في انتظار استجابة لن تأتي أبدًا؛
  • السطر 20: طريقة مصادقة المستخدم [login, passwd]. ترمي استثناءً إذا لم يتم التعرف على المستخدم؛
  • الأسطر 22–53: يرتبط كل عنوان URL الذي تعرضه خدمة الويب / JSON بطريقة من طرق الواجهة، والتي يُشتق توقيعها من توقيع الطريقة من جانب الخادم التي تتعامل مع عنوان URL المعروض. خذ على سبيل المثال عنوان URL للخادم التالي:

    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Response<String> getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin,    @PathVariable("jour") String jour, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
  • السطر 1: نرى أن [idMedecin] و [jour] هما معلمات URL. وستكون هذه هي معلمات الإدخال للطريقة المرتبطة بـ URL هذا على جانب العميل؛
  • السطر 2: نرى أن طريقة الخادم تُرجع نوع [Response<String>]. هذا النوع [String] هو نوع قيمة JSON من النوع [AgendaMedecinJour]. سيكون نوع نتيجة الطريقة المرتبطة بهذا الرابط على جانب العميل هو [AgendaMedecinJour

على جانب العميل، نعلن الطريقة التالية:


public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);

يعمل هذا التوقيع عندما يرسل الخادم استجابة من النوع [int status, List<String> messages, String body] مع [status0]. في هذه الحالة، يكون لدينا [messagesnull && body!=null]. ولا يعمل عندما يكون [status!=0]. في هذه الحالة، يكون لدينا [messages!=null && body==null]. نحتاج إلى الإشارة بطريقة ما إلى حدوث خطأ. للقيام بذلك، سنقوم بإلقاء استثناء من النوع [RdvMedecinsException] على النحو التالي:


package rdvmedecins.client.dao;
 
import java.util.List;
 
public class RdvMedecinsException extends RuntimeException {
 
    private static final long serialVersionUID = 1L;
    // error code
    private int status;
    // list of error messages
    private List<String> messages;
 
    public RdvMedecinsException() {
    }
 
    public RdvMedecinsException(int code, List<String> messages) {
        super();
        this.status = code;
        this.messages = messages;
    }
 
    // getters and setters
...
}
  • السطران 9 و 11: سيأخذ الاستثناء قيم حقول [status, messages] من كائن [Response<T>] المرسل من الخادم؛
  • السطر 5: تمتد فئة [RdvMedecinsException] إلى فئة [RuntimeException]. وبالتالي فهي استثناء غير معالج، مما يعني أنه لا يوجد أي شرط لمعالجتها باستخدام كتلة try/catch أو الإعلان عنها في توقيعات طرق الواجهة؛

علاوة على ذلك، تحتوي جميع طرق واجهة [IDao] التي تستعلم عن خدمة الويب/JSON على النوع [User] التالي كمعلمة:


package rdvmedecins.client.entities;
 
public class User {
 
    // data
    private String login;
    private String passwd;
 
    // manufacturers
    public User() {
    }
 
    public User(String login, String passwd) {
        this.login = login;
        this.passwd = passwd;
    }
 
    // getters and setters
    ...
}

في الواقع، يجب أن يكون كل تبادل مع خدمة الويب / JSON مصحوبًا برأس مصادقة HTTP.

8.5.9. حزمة [rdvmedecins.clients.console]

الآن بعد أن أصبحنا على دراية بواجهة طبقة [DAO]، يمكننا تقديم تطبيق وحدة التحكم.

  

الفئة [Main] هي كما يلي:


package rdvmedecins.clients.console;
 
import java.io.IOException;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Main {
 
    // serializer jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // connection timeout in milliseconds
    static private int TIMEOUT = 1000;
 
    public static void main(String[] args) throws IOException {
        // we retrieve a reference on the [DAO] layer
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // set the URL of the web/json service
        dao.setUrlServiceWebJson("http://localhost:8080");
        // set timeouts in milliseconds
        dao.setTimeout(TIMEOUT);
 
        // Authentication
        String message = "/authenticate [admin,admin]";
        try {
            dao.authenticate(new User("admin", "admin"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [user,user]";
        try {
            dao.authenticate(new User("user", "user"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [user,x]";
        try {
            dao.authenticate(new User("user", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [x,x]";
        try {
            dao.authenticate(new User("x", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        message = "/authenticate [admin,x]";
        try {
            dao.authenticate(new User("admin", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // customer list
        message = "/getAllClients";
        try {
            showResponse(message, dao.getAllClients(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // list of doctors
        message = "/getAllMedecins";
        try {
            showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // list of slots for doctor 2
        message = "/getAllCreneaux/2";
        try {
            showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // customer no. 1
        message = "/getClientById/1";
        try {
            showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor no. 2
        message = "/getMedecinById/2";
        try {
            showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // slot no. 3
        message = "/getCreneauById/3";
        try {
            showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // rv n° 4
        message = "/getRvById/4";
        try {
            showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // adding an appointment
        message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
        long idRv = 0;
        try {
            Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
            idRv = response.getId();
            showResponse(message, response);
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor's appointment list 1 on 2015-01-08
        message = "/getRvMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor's agenda 1 on 2015-01-08
        message = "/getAgendaMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
        // delete added rv
        message = String.format("/supprimerRv [idRv=%s]", idRv);
        try {
            dao.supprimerRv(new User("admin", "admin"), idRv);
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // doctor's appointment list 1 on 2015-01-08
        message = "/getRvMedecinJour/1/2015-01-08";
        try {
            showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
        // closing context
        context.close();
    }
 
    private static void showException(String message, RdvMedecinsException e) {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
        for (String msg : e.getMessages()) {
            System.out.println(msg);
        }
    }
 
    private static <T> void showResponse(String message, T response) throws JsonProcessingException {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(mapper.writeValueAsString(response));
    }
}
  • السطر 19: مُسلسل JSON الذي سيسمح لنا بعرض استجابة الخادم، السطر 184؛
  • السطر 25: مكون [AnnotationConfigApplicationContext] هو مكون Spring قادر على استخدام تعليقات التكوين من تطبيق Spring. نمرر فئة [AppConfig]، التي تقوم بتكوين التطبيق، إلى منشئها؛
  • السطر 26: نسترد مرجعًا إلى طبقة [DAO
  • الأسطر 27-30: نقوم بتكوينها؛
  • الأسطر 32–169: نختبر جميع طرق واجهة [IDao

النتائج التي تم الحصول عليها هي كما يلي:


09:20:56.935 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
/authenticate [admin,admin] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [user,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/authenticate [x,x]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [admin,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/getAllClients]
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]
URL [/getAllMedecins]
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]
URL [/getAllCreneaux/2]
[{"id":25,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":2},{"id":26,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":2},{"id":27,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":2},{"id":28,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":2},{"id":29,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":2},{"id":30,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":2},{"id":31,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":2},{"id":32,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":2},{"id":33,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":2},{"id":34,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":2},{"id":35,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":2},{"id":36,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":2}]
URL [/getClientById/1]
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}
URL [/getMedecinById/2]
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"}
URL [/getCreneauById/3]
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1}
URL [/getRvById/4]
L'erreur n° [2] s'est produite :
Le rendez-vous d'id [4] n'existe pas
URL [/ajouterRv [idClient=4,idCreneau=8,jour=2015-01-08]]
{"id":144,"version":0,"jour":1420671600000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":0,"idCreneau":0}
URL [/getRvMedecinJour/1/2015-01-08]
[{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}]
URL [/getAgendaMedecinJour/1/2015-01-08]
{"medecin":{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},"jour":1420671600000,"creneauxMedecinJour":[{"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"rv":{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"medecin":null,"idMedecin":1},"rv":null}]}
URL [/getRvMedecinJour/1/2015-01-08]
[]
09:21:00.258 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy

نترك للقارئ مهمة ربط النتائج بالكود. يوضح الكود كيفية استدعاء كل طريقة من طرق طبقة [DAO]. دعونا نلاحظ فقط بعض النقاط:

  • الأسطر 2–14: توضح أنه في حالة حدوث خطأ في المصادقة، يقوم الخادم بإرجاع حالة HTTP [403 Forbidden] أو [401 Unauthorized]، حسب الاقتضاء؛
  • السطران 30-31: تمت إضافة موعد للطبيب رقم 1؛
  • السطران 32-33: نرى هذا الموعد. إنه الموعد الوحيد لهذا اليوم؛
  • السطران 34-35: يظهر الموعد أيضًا في تقويم الطبيب؛
  • السطران 36-37: اختفى الموعد. قام البرنامج بحذفه في هذه الأثناء؛

يتم التحكم في سجلات وحدة التحكم بواسطة الملفات التالية:

 

[application.properties]


logging.level.org.springframework.web=OFF
logging.level.org.hibernate=OFF
spring.main.show-banner=false
logging.level.httpclient.wire=OFF

[logback.xml]


<configuration>
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
                        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
        </appender>
        <!-- log level control -->
        <root level="info"> <!-- off, info, debug, warn -->
                <appender-ref ref="STDOUT" />
        </root>
</configuration>

8.5.10. تنفيذ طبقة [DAO]

نحتاج الآن إلى عرض جوهر طبقة [DAO]: تنفيذ واجهة [IDao] الخاصة بها. سنقوم بذلك خطوة بخطوة.

 

يتم تنفيذ واجهة [IDao] بواسطة الفئة المجردة [AbstractDao] وفئتها الفرعية [Dao].

الفئة الأم [AbstractDao] هي كما يلي:


package rdvmedecins.client.dao;
 
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
 
import rdvmedecins.client.entities.User;
 
public abstract class AbstractDao implements IDao {
 
    // data
    @Autowired
    protected RestTemplate restTemplate;
    protected String urlServiceWebJson;
 
    // URL web service / jSON
    public void setUrlServiceWebJson(String url) {
        this.urlServiceWebJson = url;
    }
 
    public void setTimeout(int timeout) {
        // set the timeout for web client requests
        HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
                .getRequestFactory();
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
    }
 
    private String getBase64(User user) {
        // encodes user and password in base 64 - requires
        // java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }
 
    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
...
    }
 
}
  • السطر 20: الفئة مجردة، مما يمنعنا من تعيينها كمكون Spring. سيتم تعيين فئتها الفرعية على هذا النحو؛
  • السطران 23-24: نقوم بحقن حبة [restTemplate] التي حددناها في فئة التكوين [AppConfig
  • السطر 25: عنوان URL الجذري لخدمة الويب / JSON؛
  • الأسطر 32–38: تعيين مهلة العميل أثناء انتظار استجابة من الخادم؛
  • السطر 34: نسترد مكون [HttpComponentsClientHttpRequestFactory] الذي قمنا بحقنه في حبة [restTemplate] عند إنشائها (انظر [AppConfig])؛
  • السطر 36: نحدد الحد الأقصى لوقت انتظار العميل عند إنشاء اتصال بالخادم؛
  • السطر 37: نحدد الحد الأقصى لوقت انتظار العميل أثناء انتظاره لاستجابة لأحد طلباته؛

سيتم تضمين تنفيذ طرق الاتصال بالخادم في الطريقة العامة التالية:


    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
...
    }
  • السطر 2: معلمات [getResponse] هي كما يلي:
    • [User user]: المستخدم الذي يقوم بالاتصال؛
    • [String url]: عنوان URL المراد الاستعلام عنه. هذا هو الجزء الأخير من عنوان URL؛ أما الجزء الأول فيتم توفيره بواسطة حقل [urlServiceWebJson] الخاص بالفئة،
    • [String jsonPost]: سلسلة JSON المراد إرسالها. إذا كانت هذه القيمة موجودة، فسيتم طلب عنوان URL باستخدام POST؛ وإلا، فسيتم طلبه باستخدام GET؛

لنواصل:


// generic request
    protected String getResponse(User user, String url, String jsonPost) {
        // url : URL to contact
        // jsonPost: the jSON value to be posted
        try {
            // request execution
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }
            // execute the query
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e) {
            throw new RdvMedecinsException(20, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(21, getMessagesForException(e));
        }
    }
  • السطران 23–24: العبارة التي ترسل الطلب إلى الخادم وتستقبل استجابته. يوفر مكون [RestTemplate] مجموعة واسعة من الطرق للتفاعل مع الخادم. كان بإمكاننا اختيار طريقة أخرى غير [exchange]. تحدد المعلمة الثانية للدعوة نوع الاستجابة المتوقعة، وهي في هذه الحالة سلسلة JSON. المعلمة الأولى هي طلب [RequestEntity] (السطر 7). نتيجة طريقة [exchange] هي من النوع [ResponseEntity<String>]. يغلف النوع [ResponseEntity] الرد الكامل للخادم، بما في ذلك رؤوس HTTP ووثيقة المرسلة من الخادم. وبالمثل، يغلف النوع [RequestEntity] الطلب الكامل للعميل، بما في ذلك رؤوس HTTP وأي بيانات منشورة؛
  • السطر 23: هذا هو نص كائن [ResponseEntity<String>] الذي يتم إرجاعه إلى الطريقة المستدعية، أي سلسلة JSON المرسلة من الخادم؛
  • الأسطر 9-21: نحتاج إلى إنشاء طلب [RequestEntity]. ويختلف ذلك اعتمادًا على ما إذا كنا نستخدم طلب GET أو POST؛
  • السطر 9: طلب GET. توفر فئة [RequestEntity] طرقًا ثابتة لإنشاء طلبات GET و POST و HEAD وغيرها. تتيح لك طريقة [RequestEntity.get] إنشاء طلب GET عن طريق ربط الطرق المختلفة التي تبنيه:
    • تأخذ الطريقة [RequestEntity.get] عنوان URL الهدف كمعلمة في شكل مثيل URI؛
    • تسمح لك الطريقة [accept] بتحديد عناصر رأس HTTP [Accept]. هنا، نحدد أننا نقبل النوع [application/json] الذي سيرسله الخادم؛
    • ونتيجة ربط هذه الطرق هي نوع [HeadersBuilder
  • الأسطر 10-12: إذا كانت المعلمة [User user] غير فارغة، فإننا ندرج رأس HTTP [Authorization] في الطلب؛
  • السطر 13: تستخدم طريقة [HeadersBuilder.build] هذه المعلومات لإنشاء نوع [RequestEntity] للطلب؛
  • السطر 15: الطلب هو POST. تتيح لك طريقة [RequestEntity.post] إنشاء طلب POST عن طريق تسلسل الطرق المختلفة التي تبنيه:
    • تأخذ طريقة [RequestEntity.post] عنوان URL الهدف كمعلمة في شكل مثيل URI،
    • تسمح لك طريقة [header] بتحديد رؤوس HTTP التي ترغب في استخدامها، وفي هذه الحالة رأس التفويض،
    • تتضمن طريقة [header] التالية رأس [Content-Type: application/json] في الطلب للإشارة إلى أن البيانات المرسلة ستصل كسلسلة JSON؛
    • تشير طريقة [accept] إلى أننا نقبل النوع [application/json] الذي سيرسله الخادم؛
  • الأسطر 17-19: إذا كانت المعلمة [User user] غير فارغة، يتم تضمين رأس HTTP [Authorization] في الطلب؛
  • السطر 20: تحدد طريقة [BodyBuilder.body] القيمة المنشورة. هذه هي المعلمة الثانية لطريقة [getResponse] العامة (السطر 2)؛
  • الأسطر 25–28: في حالة حدوث أي خطأ، يتم إلقاء استثناء [RdvMedecinsException

طريقة [getMessagesForException] في السطور 26 و 28 هي كما يلي:


    // list of exception error messages
    protected static List<String> getMessagesForException(Exception exception) {
        // retrieve the list of exception error messages
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            // the message is retrieved only if it is !=null and not blank
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return erreurs;
}

تُرجع الطريقة الخاصة [getBase64] ترميز Base64 للسلسلة 'login:passwd' لرأس المصادقة HTTP:


    private String getBase64(User user) {
        // encodes user and password in base 64 - requires java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}

توسع فئة [Dao] فئة [AbstractDao] على النحو التالي:


package rdvmedecins.client.dao;
 
import java.io.IOException;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
 
@Service
public class Dao extends AbstractDao implements IDao {
 
    // mappers jSON
    @Autowired
    ObjectMapper jsonMapper;
 
    @Autowired
    private ObjectMapper jsonMapperShortCreneau;
 
    @Autowired
    private ObjectMapper jsonMapperLongRv;
 
    @Autowired
    private ObjectMapper jsonMapperShortRv;
 
    public List<Client> getAllClients(User user) {
        ...
    }
 
    public List<Medecin> getAllMedecins(User user) {
...
    }
...
}
  • السطر 22: فئة [Dao] هي مكون Spring. تم استخدام تعليق [@Service] هنا. كان بإمكاننا الاستمرار في استخدام تعليق [@Component] المستخدم حتى هذه المرحلة؛
  • الأسطر 26–36: حقن أربعة مخططات JSON محددة في فئة التكوين [DaoConfig

تتبع جميع أساليب فئة [Dao] نفس النمط. سنفصل عملية GET وعملية POST.

أولاً، طلب [GET]:


public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
        // the answer
        Response<AgendaMedecinJour> response;
        // the diary
        String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
        try {
            // diary AgendaMedecinJour
            response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
            });
        } catch (IOException e) {
            throw new RdvMedecinsException(401, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(402, getMessagesForException(e));
        }
        // response analysis
        int status = response.getStatus();
        if (status != 0) {
            throw new RdvMedecinsException(status, response.getMessages());
        } else {
            return response.getBody();
        }
}
  • السطر 5: يتم استدعاء الطريقة العامة [getResponse]. المعلمات الفعلية المستخدمة هي كما يلي:
    • 1: المستخدم؛
    • 2: عنوان URL الهدف؛
    • 3: القيمة المراد إرسالها. في هذه الحالة، لا توجد قيمة؛
  • السطر 5: لم يتم تغليف الاستدعاء في كتلة try/catch. قد ترمي الطريقة [getResponse] استثناء [RdvMedecinsException]. إذا تم رمي هذا الاستثناء، فسوف ينتقل إلى الطريقة التي استدعت الطريقة [getAgendaMedecinJour] أعلاه؛
  • السطر 8: يعرض عنوان URL [/getAgendaMedecinJour] [Response<AgendaMedecinJour>] الذي تم تسلسله إلى JSON على جانب الخادم بواسطة أداة تعيين JSON [jsonMapperLongRv]. نستخدم أداة التعيين هذه نفسها لإلغاء تسلسل سلسلة JSON المستلمة؛
  • الأسطر 10–13: في حالة حدوث خطأ في السطر 9، يتم إلقاء استثناء [RdvMedecinsException
  • الأسطر 16-21: يتم تحليل الاستجابة المرسلة من الخادم؛
  • الأسطر 17-18: إذا أبلغ الخادم عن خطأ، يتم إلقاء استثناء مع المعلومات المقدمة من الخادم؛
  • الأسطر 19-21: خلاف ذلك، يتم إرجاع جدول مواعيد الطبيب؛

سيكون طلب POST قيد الفحص كما يلي:


    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
        // the answer
        Response<Rv> response;
        try {
            // the Rv
            String jsonResponse = getResponse(user, "/ajouterRv",
                    jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
            // the Rv Rv
            response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
            });
        } catch (RdvMedecinsException e) {
            throw e;
        } catch (IOException e) {
            throw new RdvMedecinsException(381, getMessagesForException(e));
        } catch (RuntimeException e) {
            throw new RdvMedecinsException(382, getMessagesForException(e));
        }
        // response analysis
        int status = response.getStatus();
        if (status != 0) {
            throw new RdvMedecinsException(status, response.getMessages());
        } else {
            return response.getBody();
        }
}
  • السطر 6: يتم استدعاء الأسلوب [getResponse] بالمعلمات التالية:
    • 1: المستخدم؛
    • 2: عنوان URL الهدف،
    • 3: القيمة المرسلة: نمرر قيمة JSON من النوع [PostAjouter] التي تم إنشاؤها باستخدام المعلومات التي تلقتها الطريقة كمعلمات. نستخدم أداة تعيين JSON بدون مرشحات؛
  • السطر 9: على جانب الخادم، قام مخطط JSON [jsonMapperLongRv] بتسلسل استجابة الخادم. على جانب العميل، نستخدم نفس المخطط لإلغاء تسلسلها؛
  • السطر 6: يعرض عنوان URL [/ajouterRv] قيمة JSON من النوع [Response<Rv>]؛
  • الأسطر 4-11: هنا، تم وضع طريقة [getResponse] في كتلة try/catch لأن تسلسل القيمة المنشورة قد يؤدي إلى إثارة استثناء. من المحتمل أن تثير طريقة [getResponse] استثناء [RdvMedecinsException]. في هذه الحالة، نعيد المحاولة ببساطة (الأسطر 11-12)؛

الرمز التالي (الأسطر 13–24) مشابه للرمز الذي تمت مناقشته للتو. والفرق الوحيد عن عملية GET هو المعلمة الثانية لطريقة [getResponse]، والتي يجب أن تكون تمثيل JSON للقيمة المراد إرسالها.

الطرق الأخرى مبنية على نفس النموذج.

8.5.11. استثناء

أثناء إجراء اختبارات متنوعة، واجهنا حالة شاذة تم تلخيصها في فئة [Anomalie] التالية:


package rdvmedecins.clients.console;
 
import java.io.IOException;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Anomalie {
 
    // serializer jSON
    static private ObjectMapper mapper = new ObjectMapper();
    // connection timeout in milliseconds
    static private int TIMEOUT = 1000;
 
    public static void main(String[] args) throws IOException {
        // we retrieve a reference on the [DAO] layer
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        IDao dao = context.getBean(IDao.class);
        // set the URL of the web/json service
        dao.setUrlServiceWebJson("http://localhost:8080");
        // set timeouts in milliseconds
        dao.setTimeout(TIMEOUT);
 
        // Authentication
        String message = "/authenticate [admin,admin]";
        try {
            dao.authenticate(new User("admin", "admin"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // Authentication
        message = "/authenticate [admin,x]";
        try {
            dao.authenticate(new User("admin", "x"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // Authentication
        message = "/authenticate [user,user]";
        try {
            dao.authenticate(new User("user", "user"));
            System.out.println(String.format("%s : OK", message));
        } catch (RdvMedecinsException e) {
            showException(message, e);
        }
 
        // closing context
        context.close();
    }
 
    private static void showException(String message, RdvMedecinsException e) {
        System.out.println(String.format("URL [%s]", message));
        System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
        for (String msg : e.getMessages()) {
            System.out.println(msg);
        }
    }
}
  • الأسطر 31–38: تم توثيق المستخدم [admin, admin
  • الأسطر 40-47: مصادقة المستخدم [admin, x]، الذي استخدم كلمة مرور غير صحيحة؛
  • الأسطر 49-56: تم مصادقة المستخدم [user, user]؛ هذا المستخدم موجود ولكنه غير مخول؛

فيما يلي النتائج:

1
2
3
4
5
/authenticate [admin,admin] : OK
/authenticate [admin,x] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
  • السطر 2: خلافًا للتوقعات، تم قبول المستخدم [admin, x

إذا قمنا بتعليق الأسطر 33–38 من الكود، نحصل على النتيجة التالية:

1
2
3
4
5
6
URL [/authenticate [admin,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden

وهذا هو النتيجة المتوقعة. يبدو أنه بمجرد أن يقوم المستخدم [admin, admin] بتسجيل الدخول بنجاح للمرة الأولى، فإن كلمة المرور الخاصة به لم تعد مطلوبة لتسجيلات الدخول اللاحقة. وهذا هو الحال بالفعل. بشكل افتراضي، يستخدم Spring Security آلية جلسة عمل تضمن أنه بمجرد أن يقوم المستخدم بالمصادقة، فإنه لا يحتاج إلى القيام بذلك مرة أخرى في الطلبات اللاحقة. يمكنك تعديل تكوين [Spring Security] في خادم الويب / JSON بحيث لا يكون هذا هو الحال بعد الآن:

  

يجب تعديل ملف [SecurityConfig] على النحو التالي:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
  • يحدد السطر 5 أنه لا ينبغي أن تكون هناك جلسة أمان؛

وقد أدى ذلك إلى حل المشكلة.

8.6. العرض من جانب الخادم في Spring / Thymeleaf

8.6.1. مقدمة

لنعد إلى بنية تطبيق العميل/الخادم المراد إنشاؤه:

  • تم إنشاء خادم الويب/JSON [Web2
  • تم إنشاء طبقة [DAO] لعميل [Web1

العلاقة بين خادم [Web1] ومتصفحات العملاء هي علاقة عميل/خادم حيث يكون الخادم خادم ويب/JSON. في الواقع، سيقوم [Web1] بتسليم تدفقات HTML مغلفة في سلسلة JSON. بنية العميل/الخادم هي كما يلي:

  • لدينا بنية عميل [2] / خادم [1] حيث يتواصل العميل والخادم عبر JSON؛
  • في [1]، تقدم طبقة الويب Spring MVC/Thymeleaf العروض وأجزاء العرض والبيانات بتنسيق JSON. وبالتالي، فإن الخادم هو خادم ويب/JSON مثل الخادم [Web1]. كما أنه عديم الحالة؛
  • في [2]: يتم تنظيم كود JavaScript المضمن في العرض الذي يتم تحميله عند بدء تشغيل التطبيق في طبقات:
    • تتولى طبقة [العرض] معالجة تفاعلات المستخدم،
    • تتعامل طبقة [DAO] مع الوصول إلى البيانات عبر خادم [Web2
  • سيقوم العميل [2] بتخزين بعض العروض مؤقتًا لتقليل الحمل على الخادم؛

سنقوم ببناء خادم الويب/JSON [Web1]، الذي تم تنفيذه باستخدام Spring MVC/Thymeleaf، في عدة خطوات:

  • استكشاف إطار عمل Bootstrap CSS؛
  • كتابة العروض؛
  • كتابة وحدة التحكم؛

ثم، بشكل منفصل، سنقوم ببناء عميل JS للخادم [Web1]. ولإثبات بوضوح أن هذا العميل يتمتع بدرجة معينة من الاستقلالية عن الخادم [Web1]، سنقوم ببنائه باستخدام أداة [WebStorm] بدلاً من STS.

ومن الآن فصاعدًا، سيتم حذف بعض التفاصيل لأنها قد تصرف انتباهنا عن الموضوع الرئيسي، وهو تنظيم الكود. ويمكن للقراء المهتمين العثور على الكود الكامل على الموقع الإلكتروني الخاص بهذا المستند.

8.6.2. مشروع STS

  • في [1]، كود Java؛
  • في [2]، طرق العرض؛

تكوين Maven في [pom.xml] هو كما يلي:


<?xml version="1.0" encoding="UTF-8"?>
<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.rdvmedecins</groupId>
    <artifactId>rdvmedecins-springthymeleaf-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rdvmedecins-springthymeleaf-server</name>
    <description>Gestion de RV Médecins</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>istia.st.rdvmedecins</groupId>
            <artifactId>rdvmedecins-webjson-client-console</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <properties>
        <start-class>rdvmedecins.springthymeleaf.server.boot.Boot</start-class>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.7</java.version>
    </properties>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    ...
</project>
  • الأسطر 16–19: المشروع هو مشروع Thymeleaf؛
  • الأسطر 20–24: والذي يعتمد على طبقة [DAO] التي أنشأناها للتو؛

يتم التعامل مع تكوين Java بواسطة ملفين:

 

يتم تكوين طبقة [web] بواسطة ملف [WebConfig] التالي:


package rdvmedecins.springthymeleaf.server.config;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
 
@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {
 
    // ----------------- layer configuration [web]
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
 
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }
 
    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }
 
    // dispatcherservlet configuration for CORS headers
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
 
}

لقد صادفنا جميع عناصر هذا التكوين في وقت أو آخر. فقط للتذكير، فإن الأسطر 42–47 ضرورية عندما تريد أن تتمكن من الاستعلام عن الخادم باستخدام طلبات عبر الأصول (CORS). وهذا هو الحال هنا.

تقوم فئة [AppConfig] بتكوين التطبيق بأكمله:


package rdvmedecins.springthymeleaf.server.config;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
 
import rdvmedecins.client.config.DaoConfig;
 
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
 
    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // root web service / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout in milliseconds
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;
 
    ...
 
}
  • السطر 11: [AppConfig] يستورد التكوين لطبقة [DAO] وطبقة [web]؛
  • السطران 15-16: بيانات الاعتماد التي ستسمح للتطبيق بالوصول إلى عملية تشغيل التطبيق من أجل تخزين الأطباء والعملاء مؤقتًا؛
  • السطر 18: عنوان URL لخدمة الويب [Web1] / JSON؛
  • السطر 20: مهلة انتظار مكالمات HTTP الخاصة بالتطبيق؛
  • السطر 22: قيمة منطقية لتمكين أو تعطيل المكالمات عبر النطاقات؛

أخيرًا، في [application.properties]، تم تكوين خادم Tomcat للتشغيل على المنفذ 8081:

  

server.port=8081

8.6.3. ميزات التطبيق

تم وصفها في القسم 8.2. سنقوم الآن بمراجعتها. باستخدام متصفح، نطلب عنوان URL [http://localhost:8081/boot.html]:

  • في [1]، صفحة تسجيل الدخول إلى التطبيق؛
  • في [2] و[3]، اسم المستخدم وكلمة المرور للمستخدم الذي يرغب في استخدام التطبيق. يوجد مستخدمان: admin/admin (اسم المستخدم/كلمة المرور) الذي يحمل الدور (ADMIN) و user/user الذي يحمل الدور (USER). لا يمتلك سوى الدور ADMIN الإذن باستخدام التطبيق. أما الدور USER فقد أُدرج فقط لتوضيح استجابة الخادم في حالة الاستخدام هذه؛
  • في [4]، الزر الذي يسمح لك بالاتصال بالخادم؛
  • في [5]، لغة التطبيق. هناك لغتان: الفرنسية (الافتراضية) والإنجليزية؛
  • في [6]، عنوان URL للخادم [rdvmedecins-springthymeleaf-server
  • في [1]، تقوم بتسجيل الدخول؛
  • بمجرد تسجيل الدخول، يمكنك اختيار الطبيب الذي تريد زيارته [2] وتاريخ الموعد [3]. بمجرد اختيار الطبيب والتاريخ، يتم عرض التقويم تلقائيًا:
  • بمجرد عرض تقويم الطبيب، يمكنك حجز موعد [5]؛
  • في [6]، حدد المريض الذي سيحضر الموعد وقم بتأكيد اختيارك في [7]؛

بمجرد تأكيد الموعد، ستعود تلقائيًا إلى التقويم حيث يظهر الموعد الجديد الآن. يمكن حذف هذا الموعد لاحقًا [8].

تم وصف الميزات الرئيسية. إنها بسيطة. لنختتم بإعدادات اللغة:

  • في [1]، يمكنك التبديل من الفرنسية إلى الإنجليزية؛
  • في [2]، تتحول الواجهة إلى اللغة الإنجليزية، بما في ذلك التقويم؛

8.6.4. الخطوة 1: مقدمة إلى إطار عمل Bootstrap CSS

في عميل الويب أعلاه، ستستخدم صفحات HTML إطار عمل Bootstrap CSS [http://getbootstrap.com/]، الذي سنقدمه الآن.

8.6.4.1. مشروع المثال

سيكون المشروع النموذجي على النحو التالي:

  • في [1]: المشروع ككل؛
  • في [2]: كود Java؛
  • في [3]: نصوص JavaScript؛
  • في [4]: مكتبات JavaScript؛
  • في [5]: طرق عرض Thymeleaf؛
  • في [6]: أوراق الأنماط؛

8.6.4.1.1. تكوين Maven

ملف [pom.xml] مخصص لمشروع Thymeleaf Maven:


<?xml version="1.0" encoding="UTF-8"?>
<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>rdvmedecins-webjson-client-bootstrap</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>rdvmedecins-webjson-client-bootstrap</name>
    <description>Démos Bootstrap</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
        <java.version>1.7</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>

8.6.4.1.2. تكوين Java
  

تقوم فئة [BootstrapDemo] بتكوين تطبيق Spring/Thymeleaf:


package istia.st.rdvmedecins;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
 
@EnableAutoConfiguration
@ComponentScan({ "istia.st.rdvmedecins" })
public class BootstrapDemo extends WebMvcConfigurerAdapter {
 
    public static void main(String[] args) {
        SpringApplication.run(BootstrapDemo.class, args);
    }
 
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }
}

لقد صادفنا هذا النوع من الكود من قبل.

8.6.4.1.3. وحدة التحكم Spring
  

وحدة التحكم [BootstrapController] هي كما يلي:


package istia.st.rdvmedecins;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
public class BootstrapController {
 
    @RequestMapping(value = "/bs-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bso1() {
        return "bs-01";
    }
 
    @RequestMapping(value = "/bs-02", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs02() {
        return "bs-02";
    }
 
    @RequestMapping(value = "/bs-03", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs03() {
        return "bs-03";
    }
 
    @RequestMapping(value = "/bs-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs04() {
        return "bs-04";
    }
 
    @RequestMapping(value = "/bs-05", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs05() {
        return "bs-05";
    }
 
    @RequestMapping(value = "/bs-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs06() {
        return "bs-06";
    }
 
    @RequestMapping(value = "/bs-07", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs07() {
        return "bs-07";
    }
 
    @RequestMapping(value = "/bs-08", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String bs08() {
        return "bs-08";
    }
}

الإجراءات موجودة فقط لعرض طرق العرض التي تمت معالجتها بواسطة Thymeleaf.

8.6.4.1.4. ملف [application.properties]

يقوم ملف [application.properties] بتكوين خادم Tomcat المدمج:


server.port=8082

8.6.4.2. مثال رقم 1: الشاشة العملاقة

يعرض الإجراء [/bs-01] طريقة العرض [bs-01.xml] التالية:

طريقة العرض [bs-01.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
    </head>
    <body id="body">
        <div class="container">
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- error -->
            <div id="erreur" class="alert alert-danger">
                <span>Ici, un texte d'erreur</span>
            </div>
        </div>
    </body>
</html>
  • السطر 7: ملف CSS الخاص بإطار عمل Bootstrap؛
  • السطر 8: ملف CSS محلي؛
  • السطر 13: displays [1]؛
  • الأسطر 19–21: العرض [2]؛
  • السطر 11: تحدد فئة CSS [container] منطقة عرض داخل المتصفح؛
  • السطر 19: تعرض فئة CSS [alert] منطقة ملونة. تستخدم فئة [alert-danger] لونًا محددًا مسبقًا. هناك العديد من هذه الفئات [alert-info، alert-warning، ...]؛

يتم إنشاء jumbotron [1] بواسطة طريقة العرض [jumbotron.xml] التالية:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <!-- Bootstrap Jumbotron -->
    <div class="jumbotron">
        <div class="row">
            <div class="col-md-2">
                <img src="resources/images/caduceus.jpg" alt="RvMedecins" />
            </div>
            <div class="col-md-10">
                <h1>
                    Les Médecins
                    <br />
                    associés
                </h1>
            </div>
        </div>
    </div>
</section>
  • السطر 4: المنطقة لها فئة CSS [jumbotron
  • السطر 5: تحدد فئة [row] صفًا مكونًا من 12 عمودًا؛
  • السطر 6: تحدد فئة [col-md-2] منطقة من عمودين داخل الصف؛
  • السطر 7: يتم وضع صورة في هذين العمودين؛
  • الأسطر 9–15: يتم وضع النص في الأعمدة العشرة المتبقية؛

8.6.4.3. مثال رقم 2: شريط التنقل

يعرض الإجراء [/bs-02] العرض التالي [bs-02.xml]:

الميزة الجديدة هي شريط التنقل [1] مع نموذج الإدخال والأزرار الخاصة به:

تبدو طريقة العرض [bs-02.xml] كما يلي:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- scripts JS -->
        <script src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/js/bs-02.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar1"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • السطر 10: نستورد jQuery؛
  • السطر 11: نص برمجي JS محلي؛
  • السطر 16: شريط التنقل؛

يتم إنشاء شريط التنقل بواسطة طريقة العرض [navbar1.xml] التالية:


<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="navbar-collapse collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- identification form -->
                <div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
                    <div class="form-group">
                        <input type="text" placeholder="Utilisateur" class="form-control" />
                    </div>
                    <div class="form-group">
                        <input type="password" placeholder="Mot de passe" class="form-control" />
                    </div>
                    <button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
                </div>
            </div>
        </div>
    </div>
</section>
  • السطر 3: تُحدد فئة [navbar] أنماط شريط التنقل. وتمنحه فئة [navbar-inverse] خلفية سوداء. وتضمن فئة [navbar-fixed-top] بقاء شريط التنقل في أعلى الشاشة عند تمرير الصفحة المعروضة في المتصفح؛
  • الأسطر 5–13: تحدد المنطقة [1]. عادةً ما تكون هذه سلسلة من الفئات التي لا أفهمها. أستخدم المكون كما هو؛
  • الأسطر 14–26: تحدد منطقة "متجاوبة" في شريط التنقل. على الهاتف الذكي، تنكمش هذه المنطقة لتصبح منطقة قائمة؛
  • السطر 15: صورة مخفية حاليًا؛
  • الأسطر 17-25: تقوم فئة [navbar-form] بتصميم نموذج في شريط التنقل. وتقوم فئة [navbar-right] بوضعه على يمين شريط التنقل؛
  • الأسطر 21-23: حقلان للإدخال في النموذج الموجود في السطر 17 [2]. وهما داخل فئة [form-group] التي تغلف عناصر النموذج، ولكل منهما فئة [form-control
  • السطر 24: فئة [btn]، التي تحدد زرًا، معززة بفئة [btn-success]، التي تمنحه لونه الأخضر؛
  • السطر 24: عند النقر على زر [Login]، يتم تنفيذ دالة JS التالية:

function connecter() {
    showInfo("Connexion demandée...");
}
 
function showInfo(message) {
    $("#info").text(message);
}

إليك مثال على ذلك:

Image

8.6.4.4. المثال رقم 3: زر القائمة

يعرض الإجراء [/bs-03] العرض التالي [bs-03.xml]:

  • الميزة الجديدة هي زر القائمة [1]، المعروف أيضًا باسم "القائمة المنسدلة"؛

فيما يلي كود عرض [bs-03.xml]:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script src="resources/vendor/bootstrap.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-03.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar2"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • السطر 11: يتطلب زر القائمة المنسدلة ملف Bootstrap JS؛
  • السطر 18: شريط التنقل الجديد؛

تبدو طريقة العرض [navbar2.xml] كما يلي:


<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="navbar-collapse collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- identification form -->
                <div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
                    <div class="form-group">
                        <input type="text" placeholder="Utilisateur" class="form-control" />
                    </div>
                    <div class="form-group">
                        <input type="password" placeholder="Mot de passe" class="form-control" />
                    </div>
                    <button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
                    <!-- languages -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger">Langues</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')">Français</a>
                            </li>
                            <li>
                                <a href="javascript:setLang('en')">English</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initNavBar2();
        /*]]>*/
    </script>
</section>
  • الأسطر 25–40: تعريف زر القائمة المنسدلة؛
  • السطر 27: تمنحه فئة [btn-danger] لونه الأحمر؛
  • الأسطر 32–39: عناصر القائمة. كل عنصر هو رابط مرتبط بوظيفة JavaScript؛
  • الأسطر 46–51: نص برمجي JavaScript يتم تنفيذه بعد تحميل المستند؛

نص JS [bs-03.js] هو كما يلي:


function initNavBar2() {
    // dropdown des langues
    $('.dropdown-toggle').dropdown();
}
 
function connecter() {
    showInfo("Connexion demandée...");
}
 
function setLang(lang) {
    var msg;
    switch (lang) {
    case 'fr':
        msg = "Vous avez choisi la langue française...";
        break;
    case 'en':
        msg = "You have selected english language...";
        break;
    }
    showInfo(msg);
}
 
function showInfo(message) {
    $("#info").text(message);
}
  • الأسطر 1-4: الدالة التي تهيئ [dropdown]. [$('.dropdown-toggle')] تحدد موقع العنصر الذي يحمل فئة [dropdown-toggle]. هذا هو زر القائمة المنسدلة (السطر 28 من العرض). يتم تطبيق الدالة JS [dropdown()] — المحددة في ملف JS [bootstrap.js] — عليها. فقط بعد هذه العملية يتصرف الزر كزر قائمة منسدلة؛
  • الأسطر 10–21: الدالة التي يتم تنفيذها عند اختيار لغة؛

إليك مثال:

Image

8.6.4.5. مثال رقم 4: قائمة

يعرض الإجراء [/bs-04] العرض التالي [bs-04.xml]:

تمت إضافة قائمة [1].

طريقة العرض [bs-04.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script src="resources/vendor/bootstrap.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-04.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content">
                <h1>Ici un contenu</h1>
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • السطر 18: أدخل شريط تنقل جديد؛

تبدو طريقة عرض [navbar3.xml] كما يلي:


<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="collapse navbar-collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <ul class="nav navbar-nav">
                    <li class="active" id="lnkAfficherAgenda">
                        <a href="javascript:afficherAgenda()">Agenda </a>
                    </li>
                    <li class="active" id="lnkAccueil">
                        <a href="javascript:retourAccueil()">Retour Accueil </a>
                    </li>
                    <li class="active" id="lnkRetourAgenda">
                        <a href="javascript:retourAgenda()">Retour Agenda </a>
                    </li>
                    <li class="active" id="lnkValiderRv">
                        <a href="javascript:validerRv()">Valider </a>
                    </li>
                </ul>
                <!-- right-hand buttons -->
                <div class="navbar-form navbar-right" role="form">
                    <!-- disconnect -->
                    <button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
                    <!-- languages -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger">Langues</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')">Français</a>
                            </li>
                            <li>
                                <a href="javascript:setLang('en')">English</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initNavBar3();
        /*]]>*/
    </script>
</section>
  • الأسطر 16–29: إنشاء القائمة بأربعة خيارات، كل منها مرتبط بنص برمجي JS؛
  • الأسطر 55-60: نص برمجي يتم تنفيذه عند تحميل الصفحة؛

نص JS [bs-04.js] هو كما يلي:


...
function initNavBar3() {
    // dropdown des langues
    $('.dropdown-toggle').dropdown();
    // l'moving image
    loading = $("#loading");
    loading.hide();
}
 
function afficherAgenda() {
    showInfo("option [Agenda] cliquée...");
}
 
function retourAccueil() {
    showInfo("option [Retour accueil] cliquée...");
}
 
function retourAgenda() {
    showInfo("option [Retour agenda] cliquée...");
}
 
function validerRv() {
    showInfo("option [Valider] cliquée...");
}
 
function setMenu(show) {
    // les liens du menu
    var lnkAfficherAgenda = $("#lnkAfficherAgenda");
    var lnkAccueil = $("#lnkAccueil");
    var lnkValiderRv = $("#lnkValiderRv");
    var lnkRetourAgenda = $("#lnkRetourAgenda");
    // on les met dans un dictionnaire
    var options = {
        "lnkAccueil" : lnkAccueil,
        "lnkAfficherAgenda" : lnkAfficherAgenda,
        "lnkValiderRv" : lnkValiderRv,
        "lnkRetourAgenda" : lnkRetourAgenda
    }
    // on cache tous les liens
    for ( var key in options) {
        options[key].hide();
    }
    // on affiche ceux qui sont demandés
    for (var i = 0; i < show.length; i++) {
        var option = show[i];
        options[option].show();
    }
}
  • الأسطر 2–18: وظيفة تهيئة الصفحة؛
  • السطر 4: لعرض زر اختيار اللغة؛
  • السطران 6-7: إخفاء الصورة المتحركة؛
  • الأسطر 26-48: وظيفة [setMenu] التي تسمح لك بتحديد الخيارات التي يجب أن تكون مرئية؛

لننتقل إلى وحدة تحكم المطور (Ctrl-Shift-I) وندخل الكود التالي [1]:

ثم عد إلى المتصفح. لقد تغيرت القائمة [2]:

8.6.4.6. مثال رقم 5: قائمة منسدلة

يعرض الإجراء [/bs-05] طريقة العرض [bs-05.xml] التالية:

الميزة الجديدة موجودة في [1]. نستخدم هنا مكونًا متوفرًا خارج Bootstrap، [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].

فيما يلي كود عرض [bs-05.xml]:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-05.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content" th:include="choixmedecin">
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • السطر 8: CSS المطلوب للقائمة المنسدلة؛
  • السطر 13: ملف JS المطلوب للقائمة المنسدلة؛
  • السطر 24: القائمة المنسدلة؛

تبدو طريقة العرض [choixmedecin.xml] كما يلي:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info">Veuillez choisir un médecin</div>
    <div class="row">
        <div class="col-md-3">
            <h2>Médecin</h2>
            <select id="idMedecin" class="combobox" data-style="btn-primary">
                <option value="1">Mme Marie Pélissier</option>
                <option value="2">Mr Jean Pardon</option>
                <option value="3">Mlle Jeanne Jirou</option>
                <option value="4">Mr Paul Macou</option>
            </select>
        </div>
    </div>
    <!-- local script -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initChoixMedecin();
        /*]]>*/
    </script>
</section>
  • الأسطر 7–12: هذا عنصر [select] قياسي، ولكنه يحمل فئة محددة [combobox]. وتمنح السمة [data-style="btn-primary"] المكون لونه الأزرق؛
  • الأسطر 16–21: نص برمجي يتم تنفيذه عند تحميل الصفحة؛

ملف JS [bs-05.js] هو كما يلي:


...
function afficherAgenda() {
    var idMedecin = $('#idMedecin option:selected').val();
    showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
 
function initChoixMedecin() {
    // le select des médecins
    $('#idMedecin').selectpicker();
    // le menu
    setMenu([ "lnkAfficherAgenda" ]);
}
  • الأسطر 7–12: الدالة التي يتم تنفيذها عند تحميل الصفحة؛
  • السطر 9: التعليمات التي تحول [select] في الصفحة إلى قائمة منسدلة Bootstrap. [$('#idMedecin')] تشير إلى [select] (السطر 7 من عرض [choixmedecin]) ووظيفة JS [selectpicker] تأتي من ملف JS [bootstrap-select.js
  • السطر 11: يتم عرض خيار واحد فقط من خيارات القائمة؛
  • الأسطر 2-5: وظيفة JavaScript التي يتم تنفيذها عند النقر على خيار القائمة [Agenda
  • السطر 3: نسترد قيمة الخيار المحدد في القائمة المنسدلة: [$('#idMedecin option:selected')] يبحث أولاً عن المكون [id=idMedecin] ثم، داخل هذا المكون، عن الخيار المحدد. ثم تسترد عملية [..].val() قيمة العنصر الذي تم العثور عليه، أي سمة [value] للخيار المحدد؛

فيما يلي مثال على اختيار طبيب:

 

8.6.4.7. المثال رقم 6: تقويم

يعرض الإجراء [/bs-06] العرض التالي [bs-06.xml]:

Image

يؤدي اختيار طبيب أو تاريخ إلى تشغيل دالة JS تعرض كلاً من الطبيب المحدد والتاريخ المحدد. وإليك مثال على ذلك:

 

باستخدام زر قائمة اللغات، يمكنك تبديل التقويم (والتقويم فقط) إلى اللغة الإنجليزية:

Image

هذا هو المثال الأكثر تعقيدًا في هذه السلسلة. التقويم هو مكون [bootstrap-datepicker] [http://eternicode.github.io/bootstrap-datepicker].

طريقة العرض [bs-06.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-06.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3"></div>
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron"></div>
            <!-- content -->
            <div id="content" th:include="choixmedecinjour">
            </div>
            <!-- info -->
            <div class="alert alert-warning">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • السطر 8: ملف CSS لمكون [bootstrap-datepicker
  • السطر 16: ملف JS لمكون [bootstrap-datepicker
  • السطر 17: ملف JS لإدارة التقويم الفرنسي. بشكل افتراضي، يكون باللغة الإنجليزية؛
  • السطر 15: ملف JS لمكتبة تسمى [moment] توفر الوصول إلى العديد من وظائف حساب الوقت [http://momentjs.com/
  • السطر 28: عرض التقويم؛

عرض [choixmedecinjour.xml] كما يلي:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info">Veuillez choisir un médecin et une date</div>
    <div class="row">
        <div class="col-md-3">
            <h2>Médecin</h2>
            <select id="idMedecin" class="combobox" data-style="btn-primary">
                <option value="1">Mme Marie Pélissier</option>
                <option value="2">Mr Jean Pardon</option>
                <option value="3">Mlle Jeanne Jirou</option>
                <option value="4">Mr Paul Macou</option>
            </select>
        </div>
        <div class="col-md-3">
            <h2>Date</h2>
            <section id="calendar_container">
                <div id="calendar" class="input-group date">
                    <input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
                        <span class="input-group-addon">
                            <i class="glyphicon glyphicon-th"></i>
                        </span>
                    </input>
                </div>
            </section>
        </div>
    </div>
    <!-- local script -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initChoixMedecinJour();
        /*]]>*/
    </script>
</section>
  • الأسطر 17-23: التقويم؛
  • السطر 18: تمنحه فئة [btn-primary] لونه الأزرق؛
  • السطر 18: السمة [disabled="true"] تمنع الإدخال اليدوي للتاريخ. يجب عليك استخدام التقويم؛
  • السطر 16: تم وضع التقويم في قسم [id="calendar_container"]. لتغيير لغة التقويم، يجب عليك حذفه ثم إعادة إنشائه. لذا، احذف محتوى المكون [id="calendar_container"] ثم ضع التقويم الجديد باللغة الجديدة هناك؛
  • الأسطر 28–33: كود تهيئة الصفحة؛

ملف JS [bs-06.js] كما يلي:


...
var calendar_infos = {};
 
function initChoixMedecinJour() {
    // calendrier
    var calendar_container = $("#calendar_container");
    calendar_infos = {
        "container" : calendar_container,
        "html" : calendar_container.html(),
        "today" : moment().format('YYYY-MM-DD'),
        "langue" : "fr"
    }
    // création calendrier
    updateCalendar();
    // le select des médecins
    $('#idMedecin').selectpicker();
    $('#idMedecin').change(function(e) {
        afficherAgenda();
    })
    // le menu
    setMenu([]);
}
  • السطر 2: يتم إدارة التقويم بواسطة عدة وظائف JS. ستجمع المتغير [calendar_infos] معلومات حول التقويم. وهو متغير عام بحيث يمكن الوصول إليه من قبل الوظائف المختلفة؛
  • السطر 6: نحدد حاوية التقويم؛
  • الأسطر 7-12: المعلومات المخزنة للتقويم؛
    • السطر 8: إشارة إلى حاويته،
    • السطر 9: كود HTML للتقويم. باستخدام هاتين المعلومتين، يمكننا إزالة التقويم وإعادة إنشائه؛
    • السطر 10: تاريخ اليوم بتنسيق [yyyy-mm-dd
    • السطر 11: لغة التقويم؛
  • السطر 14: إنشاء التقويم؛
  • السطر 16: القائمة المنسدلة للأطباء؛
  • الأسطر 17-19: في كل مرة تتغير فيها القيمة المحددة في هذه القائمة المنسدلة، سيتم تنفيذ طريقة [displayCalendar
  • السطر 21: لا توجد قائمة في شريط التنقل؛

وظيفة [updateCalendar] هي كما يلي:


function updateCalendar(renew) {
    if (renew) {
        // régénération du calendrier actuel
        calendar_infos.container.html(calendar_infos.html);
    }
    // initialisation du calendrier
    var calendar = $("#calendar");
    var settings = {
        format : "yyyy-mm-dd",
        startDate : calendar_infos.today,
        language : calendar_infos.langue,
    };
    calendar.datepicker(settings);
    // sélection de la date courante
    if (calendar_infos.date) {
        calendar.datepicker('setDate', calendar_infos.date)
    }
    // évts
    calendar.datepicker().on('hide', function(e) {
        // affichage jour sélectionné
        displayJour();
    });
    calendar.datepicker().on('changeDate', function(e) {
        // on note la nouvelle date
        calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
        // affichage infos agenda
        afficherAgenda();
        // affichage jour sélectionné
        displayJour();
    });
    // affichage jour sélectionné
    displayJour();
}
  • السطر 1: تقبل الدالة [updateCalendar] معلمة قد تكون موجودة أو غير موجودة. إذا كانت موجودة، يتم إعادة إنشاء التقويم (السطر 4) بناءً على المعلومات الموجودة في [calendar_infos
  • السطر 7: تتم الإشارة إلى التقويم؛
  • الأسطر 8-12: معلمات التهيئة الخاصة به؛
    • السطر 9: تنسيق التواريخ التي يتم التعامل معها [yyyy-mm-dd
    • السطر 10: التاريخ الأول الذي يمكن تحديده في التقويم. هنا، تاريخ اليوم. لا يمكن تحديد التواريخ السابقة لهذا التاريخ؛
    • السطر 11: لغة التقويم. سيكون هناك لغتان: ['en'] و ['fr']؛
  • السطر 13: يتم تكوين التقويم؛
  • الأسطر 15-17: إذا تم تهيئة التاريخ من [calendar_infos]، فسيتم تعيين هذا التاريخ كتاريخ التقويم الحالي؛
  • الأسطر 19-22: في كل مرة يُغلق التقويم، سيتم عرض التاريخ المحدد؛
  • الأسطر 23–30: في كل مرة يحدث فيها تغيير في التاريخ في التقويم:
    • السطر 25: يتم تسجيل التاريخ المحدد في [calendar_infos
    • السطر 27: نعرض معلومات حول التقويم،
    • السطر 29: نعرض اليوم المحدد؛
  • السطر 32: عرض اليوم المحدد، إن وجد؛

طريقة [displayJour] التي تعرض اليوم المحدد هي كما يلي:


// affiche le jour sélectionné
function displayJour() {
    if (calendar_infos.date) {
        var displayjour = $("#displayjour");
        moment.locale(calendar_infos.langue);
        jour = moment(calendar_infos.date).format('LL');
        displayjour.val(jour);
    }
}
  • السطر 3: إذا تم تحديد تاريخ بالفعل (في البداية، لا يحتوي التقويم على تاريخ محدد)؛
  • السطر 4: نحدد المكون الذي سنقوم بإدخال التاريخ فيه؛
  • السطر 5: يمكن كتابة هذا التاريخ باللغة الإنجليزية أو الفرنسية. نحدد لغة المكتبة [لحظة]؛
  • السطر 6: عرض التاريخ المحدد باللغة المختارة وبتنسيق طويل؛
  • السطر 7: يتم عرض هذا التاريخ؛

فيما يلي مثالان:

عندما يتغير الطبيب أو الموعد، يتم تنفيذ طريقة [displayCalendar]:


function afficherAgenda() {
    // on affiche médecin et date
    var idMedecin = $('#idMedecin option:selected').val();
    if (calendar_infos.date) {
        showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin + " et le jour " + calendar_infos.date);
    }
}

8.6.4.8. مثال رقم 7: جدول HTML "متجاوب"

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

يعرض الإجراء [/bs-07] طريقة العرض [bs-07.xml] التالية (شاشة كاملة):

الميزة الجديدة هي جدول HTML [1]. يتم إدارة هذا الجدول بواسطة مكتبة JS [footable]: [https://github.com/fooplugins/FooTable].

إذا قمت بتغيير حجم نافذة المتصفح، فستحصل على ما يلي:

  • تكيّف جدول HTML مع حجم الشاشة؛
  • في [1]، لرؤية رابط [كتاب]، يجب النقر على علامة [+]؛
  • في [2]، ما تراه عند النقر على علامة [+]؛

عرض [bs-07.xml] كما يلي:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
        <script type="text/javascript" src="resources/vendor/footable.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-07.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3" />
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron" />
            <!-- content -->
            <div id="content" th:include="choixmedecinjour" />
            <div id="agenda" th:include="agenda" />
            <!-- info -->
            <div class="alert alert-success">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • السطر 10: CSS لمكتبة [footable
  • السطر 19: جافا سكريبت لمكتبة [footable
  • السطر 31: جدول HTML للتقويم؛

عرض [agenda.xml] كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <div class="row alert alert-danger">
            <div class="col-md-6">
                <table id="creneaux" class="table">
                    <thead>
                        <tr>
                            <th data-toggle="true">
                                <span>Créneau horaire</span>
                            </th>
                            <th>
                                <span>Client</span>
                            </th>
                            <th data-hide="phone">
                                <span>Action</span>
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>
                                <span class='status-metro status-active'>
                                    9h00-9h20
                                </span>
                            </td>
                            <td>
                                <span></span>
                            </td>
                            <td>
                                <a href="javascript:reserver(14)" class="status-metro status-active">
                                    Réserver
                                </a>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                <span class='status-metro status-suspended'>
                                    9h20-9h40
                                </span>
                            </td>
                            <td>
                                <span>Mme Paule MARTIN</span>
                            </td>
                            <td>
                                <a href="javascript:supprimer(17)" class="status-metro status-suspended">
                                    Supprimer
                                </a>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
        <!-- init page -->
        <script th:inline="javascript">
            /*<![CDATA[*/
            // on initialise la page
            initAgenda();
        /*]]>*/
        </script>
    </body>
</html>
  • السطر 4: يضع الجدول في صف [row] ومربع ملون [alert alert-danger
  • السطر 5: سيمتد الجدول على 6 أعمدة [col-md-6
  • السطر 6: يتم تنسيق جدول HTML بواسطة Bootstrap [class='table'];
  • السطر 9: تحدد السمة [data-toggle] العمود الذي يحتوي على رمز [+/-] الذي يوسع/يطوي الصف؛
  • السطر 15: تحدد السمة [data-hide='phone'] أنه يجب إخفاء العمود إذا كان حجم الشاشة بحجم شاشة الهاتف. يمكن أيضًا استخدام القيمة 'tablet
  • السطر 31: ترتبط وظيفة JS بالرابط [Book
  • السطر 46: ترتبط دالة JS بالرابط [Delete
  • الأسطر 56–61: تهيئة الصفحة؛

يأتي عدد من فئات CSS المستخدمة أعلاه من ملف CSS [bootstrapDemo.css]:


@CHARSET "UTF-8";
 
#creneaux th {
    text-align: center;
}
 
#creneaux td {
    text-align: center;
    font-weight: bold;
}
 
.status-metro {
  display: inline-block;
  padding: 2px 5px;
  color:#fff;
}
 
.status-metro.status-active {
  background: #43c83c;
}
 
.status-metro.status-suspended {
  background: #fa3031;
}

تأتي أنماط [status-*] من مثال لاستخدام الجدول [footable] الموجود على موقع المكتبة.

في ملف JS [bs-07.js]، يتم تهيئة الصفحة على النحو التالي:


function initAgenda() {
    // time slot table
    $("#creneaux").footable();
}

هذا كل شيء. يشير [$("#creneaux")] إلى جدول HTML الذي نريد جعله متجاوبًا. بالإضافة إلى ذلك، إليك وظائف JS المرتبطة بالرابطين [حجز] و[حذف]:


function reserver(idCreneau) {
    showInfo("Réservation du créneau n° " + idCreneau);
}
 
function supprimer(idRv) {
    showInfo("Suppression du rv n° " + idRv);
}

8.6.4.9. مثال #8: مربع منبثق

يعرض الإجراء [/bs-08] العرض التالي [bs-08.xml]:

 

Image

في حين أنه في السابق، كان النقر على رابط [Book] يعرض المعلومات في مربع المعلومات، فإننا هنا سنعرض مربعًا منبثقًا لاختيار عميل للموعد:

Image

المكون المستخدم هو مكون [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/].

طريقة العرض [bs-08.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>RdvMedecins</title>
        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
        <link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
        <!-- Bootstrap core JavaScript ================================================== -->
        <script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
        <script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
        <script type="text/javascript" src="resources/vendor/bootstrap-modal.js"></script>
        <script type="text/javascript" src="resources/vendor/footable.js"></script>
        <!-- local script -->
        <script type="text/javascript" src="resources/js/bs-08.js"></script>
    </head>
    <body id="body">
        <div class="container">
            <!-- navigation bar -->
            <div th:include="navbar3" />
            <!-- Bootstrap Jumbotron -->
            <div th:include="jumbotron" />
            <!-- content -->
            <div id="content" th:include="choixmedecinjour" />
            <div id="agenda" th:include="agenda-modal" />
            <div th:include="resa" />
            <!-- info -->
            <div class="alert alert-success">
                <span id="info">Ici, un texte d'information</span>
            </div>
        </div>
    </body>
</html>
  • السطر 19: ملف JS المطلوب لمربعات النوافذ المنبثقة؛
  • السطر 32: عرض [agenda-modal] مطابق لعرض [agenda] باستثناء تفصيل واحد: وظيفة JS التي تتعامل مع رابط [Book]:

<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>

وظيفة [showDialogResa] مسؤولة عن عرض المربع المنبثق لاختيار العميل؛

  • السطر 33: عرض [resa.xml] هو المربع المنبثق لاختيار العميل:

<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div id="resa" class="modal fade">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">
                        </span>
                    </button>
                    <!-- <h4 class="modal-title">Modal title</h4> -->
                </div>
                <div class="modal-body">
                    <div class="alert alert-info">
                        <h3>
                            <span>Prise de rendez-vous</span>
                        </h3>
                    </div>
                    <div class="row">
                        <div class="col-md-3">
                            <h2>Clients</h2>
                            <select id="idClient" class="combobox" data-style="btn-primary">
                                <option value="1">Mme Marguerite Planton</option>
                                <option value="2">Mr Maxime Franck</option>
                                <option value="3">Mlle Elisabeth Oron</option>
                                <option value="4">Mr Gaëtan Calot</option>
                            </select>
                        </div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()">Annuler</button>
                    <button type="button" class="btn btn-primary" onclick="javascript:validateResa()">Valider</button>
                </div>
            </div><!-- /.modal-content -->
        </div><!-- /.modal-dialog -->
    </div><!-- /.modal -->
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initResa();
        /*]]>*/
    </script>
</section>
  • الأسطر 3-37: مربع النوافذ المنبثقة؛
  • الأسطر 13-30: محتوى هذا المربع (ما سيتم عرضه)؛
  • الأسطر 31-34: أزرار مربع الحوار؛
  • السطر 32: زر [إلغاء] يتم التعامل معه بواسطة دالة JS [cancelDialogResa
  • السطر 33: زر [تأكيد] يتم التعامل معه بواسطة دالة JS [validateResa
  • الأسطر 39–44: البرنامج النصي لتهيئة مربع الحوار؛

ينتج عن ذلك العرض التالي:

 

لاحظ أن مربع الحوار لا يتم عرضه بشكل افتراضي. ولهذا السبب لا يظهر عند بدء تشغيل التطبيق، على الرغم من وجود كود HTML الخاص به في المستند.

ملف JS [bs-08.js] هو كما يلي:


var idCreneau;
var idClient;
var resa;
 
function showDialogResa(idCreneau) {
    // on mémorise l'id du créneau
    this.idCreneau = idCreneau;
    // on affiche le dialogue de réservation
    var resa = $("#resa");
    resa.modal('show');
    // log
    showInfo("Réservation du créneau n° " + idCreneau);
}
 
function cancelDialogResa() {
    // on cache la boîte de dialogue
    resa.modal('hide');
}
 
// validation résa
function validateResa() {
    // on récupère les infos
    var idClient = $('#idClient option:selected').val();
    // on cache la boîte de dialogue
    resa.modal('hide');
    // infos
    showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
 
function initResa() {
    // le select des clients
    $('#idClient').selectpicker();
    // boîte modale
    resa = $("#resa");
    resa.modal({});    
}
  • الأسطر 30–36: وظيفة تهيئة مربع النوافذ المنبثقة؛
  • السطر 32: يحتوي مربع النموذج على قائمة منسدلة تحتاج إلى التهيئة؛
  • السطور 34-35: تهيئة النافذة المنبثقة نفسها؛
  • الأسطر 5-13: دالة JS المرتبطة برابط [Book
  • السطر 7: يتم تخزين معلمة الدالة في المتغير العام من السطر 1؛
  • السطور 9-10: يتم إظهار مربع النوافذ المنبثقة؛
  • السطر 12: يتم تسجيل المعلومات في مربع المعلومات؛
  • الأسطر 15–18: التعامل مع زر [Cancel]. نقوم ببساطة بإخفاء مربع النموذج (السطر 17)؛
  • الأسطر 21–31: دالة JS المرتبطة بزر [Submit
  • السطر 23: استرداد سمة [value] للعميل المحدد؛
  • السطر 25: إخفاء مربع الحوار؛
  • السطر 27: نقوم بتسجيل معلومتين: رقم الحجز والعميل الذي يخصه؛

8.6.5. الخطوة 2: كتابة طرق العرض

سنقوم الآن بوصف طرق العرض التي يعرضها خادم [Web1] بالإضافة إلى قوالبها.

  

8.6.5.1. طريقة العرض [navbar-start]

يعرض شريط التنقل على صفحة التمهيد:

Image

فيما يلي كود ملف [navbar-start.xml]:


<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="navbar-collapse collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- identification form -->
                <div class="navbar-form navbar-right" role="form" id="formulaire">
                    <div class="form-group">
                        <input type="text" th:placeholder="#{service.url}" class="form-control" id="urlService" />
                    </div>
                    <div class="form-group">
                        <input type="text" th:placeholder="#{username}" class="form-control" id="login" />
                    </div>
                    <div class="form-group">
                        <input type="password" th:placeholder="#{password}" class="form-control" id="passwd" />
                    </div>
                    <button type="button" class="btn btn-success" th:text="#{login}" onclick="javascript:connecter()">Sign in</button>
                    <!-- languages -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger" th:text="#{langues}">Action</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
                            </li>
                            <li>
                                <a href="javascript:setLang('en')" th:text="#{langues.en}" />
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initNavBarStart();
        /*]]>*/
    </script>
</section>

لا يحتوي هذا العرض على قالب. ويحتوي على معالجات الأحداث التالية:

معالج
معالج
النقر على زر تسجيل الدخول
connect() - السطر 27
انقر على رابط [الفرنسية]
setLang('fr') - السطر 37
انقر على رابط [English]
setLang('en') - السطر 40

8.6.5.2. طريقة عرض [jumbotron]

هذه هي طريقة العرض التي تظهر أسفل شريط التنقل [navbar-start] في صفحة التمهيد:

Image

فيما يلي كود [jumbotron.xml]:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <!-- Bootstrap Jumbotron -->
    <div class="jumbotron">
        <div class="row">
            <div class="col-md-2">
                <img src="resources/images/caduceus.jpg" alt="RvMedecins" />
            </div>
            <div class="col-md-10">
                <h1 th:utext="#{application.header}" />
            </div>
        </div>
    </div>
</section>

لا يحتوي عرض [jumbotron] على أي قالب أو أحداث.

8.6.5.3. عرض [login]

هذه هي طريقة العرض التي تظهر أسفل jumbotron في صفحة التمهيد:

Image

رمزها [login.xml] هو كما يلي:


<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info" th:text="#{identification}">Identification
    </div>
</section>

لا يحتوي العرض على قالب أو أحداث.

8.6.5.4. عرض [navbar-run]

هذا هو شريط التنقل الذي يظهر عند نجاح تسجيل الدخول:

Image

فيما يلي كود [navbar-run.xml]:


<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">RdvMedecins</a>
            </div>
            <div class="collapse navbar-collapse">
                <img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
                <!-- right-hand buttons -->
                <form class="navbar-form navbar-right" role="form">
                    <!-- disconnect -->
                    <button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
                    <!-- languages -->
                    <div class="btn-group">
                        <button type="button" class="btn btn-danger" th:text="#{langues}">Langue</button>
                        <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
                            <span class="caret"></span>
                            <span class="sr-only">Toggle Dropdown</span>
                        </button>
                        <ul class="dropdown-menu" role="menu">
                            <li>
                                <a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
                            </li>
                            <li>
                                <a href="javascript:setLang('en')" th:text="#{langues.en}" />
                            </li>
                        </ul>
                    </div>
                </form>
            </div>
        </div>
    </div>
    <!-- init page -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initNavBarRun();
        /*]]>*/
    </script>
</section>

لا يحتوي هذا العرض على قالب. ويحتوي على معالجات الأحداث التالية:

معالج
معالج
انقر على زر تسجيل الخروج
logout() - السطر 19
انقر على رابط [الفرنسية]
setLang('fr') - السطر 29
انقر على رابط [English]
setLang('en') - السطر 32

8.6.5.5. عرض [الصفحة الرئيسية]

هذه هي طريقة العرض التي تظهر مباشرة أسفل شريط التنقل [navbar-run]:

Image

وإليك كودها [home.html]:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-info" th:text="#{choixmedecinjour.title}">Veuillez choisir un médecin et une date</div>
    <div class="row">
        <div class="col-md-3">
            <h2 th:text="#{rv.medecin}">Médecin</h2>
            <select name="idMedecin" id="idMedecin" class="combobox" data-style="btn-primary">
                <option th:each="medecinItem : ${rdvmedecins.medecinItems}" th:text="${medecinItem.texte}" th:value="${medecinItem.id}"/>
            </select>
        </div>
        <div class="col-md-3">
            <h2 th:text="#{rv.jour}">Date</h2>
            <section id="calendar_container">
                <div id="calendar" class="input-group date">
                    <input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
                        <span class="input-group-addon">
                            <i class="glyphicon glyphicon-th"></i>
                        </span>
                    </input>
                </div>
            </section>
        </div>
    </div>
    <!-- agenda -->
    <div id="agenda"></div>
    <!-- local script -->
    <script th:inline="javascript">
        /*<![CDATA[*/
            // on initialise la page
            initChoixMedecinJour();
        /*]]>*/
    </script>
</html>

ونموذجها كما يلي:

  • [rdvmedecins.medecinItems] (السطر 8): قائمة الأطباء؛

في شكله الحالي، لا يبدو أن العرض يحتوي على أي معالجات أحداث. في الواقع، يتم تعريف هذه المعالجات في الدالة [initChoixMedecinJour]. تم عرض هذه الدالة في القسم 8.6.4.7، في الصفحة 466 وبشكل أكثر تحديدًا في الصفحة 469. وهي تحتوي على معالجات الأحداث التالية:

معالج
معالج
اختيار الطبيب
getAgenda
تحديد تاريخ
الحصول على جدول الأعمال

8.6.5.6. عرض [التقويم]

تعرض طريقة عرض [جدول الأعمال] يومًا واحدًا من تقويم الطبيب:

Image

فيما يلي كود [agenda.xml] الخاص بها:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h3 class="alert alert-info" th:text="${agenda.titre}">Agenda de Mme Pélissier le 13/10/2014</h3>
        <h4 class="alert alert-danger" th:if="${agenda.creneaux.length}==0" th:text="#{agenda.medecinsanscreneaux}">Ce médecin n'a pas encore de créneaux
            de consultation</h4>
        <th:block th:if="${agenda.creneaux.length}!=0">
            <div class="row tab-content alert alert-warning">
                <div class="tab-pane active col-md-6">
                    <table id="creneaux" class="table">
                        <thead>
                            <tr>
                                <th data-toggle="true">
                                    <span th:text="#{agenda.creneauhoraire}">Créneau horaire</span>
                                </th>
                                <th>
                                    <span th:text="#{agenda.client}">Client</span>
                                </th>
                                <th data-hide="phone">
                                    <span th:text="#{agenda.action}">Action</span>
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr th:each="creneau,iter : ${agenda.creneaux}">
                                <td>
                                    <span th:if="${creneau.action}==1" class="status-metro status-active" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
                                    <span th:if="${creneau.action}==2" class="status-metro status-suspended" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
                                </td>
                                <td>
                                    <span th:text="${creneau.client}">Client</span>
                                </td>
                                <td>
                                    <a th:if="${creneau.action}==1" th:href="@{'javascript:reserverCreneau('+${creneau.id}+')'}" th:text="${creneau.commande}"
                                        class="status-metro status-active">Réserver
                                    </a>
                                    <a th:if="${creneau.action}==2" th:href="@{'javascript:supprimerRv('+${creneau.idRv}+')'}" th:text="${creneau.commande}"
                                        class="status-metro status-suspended">Supprimer
                                    </a>
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
            <!-- reservation -->
            <section th:include="resa" />
        </th:block>
        <!-- init page -->
        <script th:inline="javascript">
            /*<![CDATA[*/
            // on initialise la page
            initAgenda();
        /*]]>*/
        </script>
    </body>
</html>

يحتوي قالب هذا العرض على عنصر واحد فقط:

  • [agenda] (السطر 4): قالب معقد إلى حد ما مصمم خصيصًا لعرض التقويم؛

ويحتوي على معالجات الأحداث التالية:

معالج
معالج
انقر على زر [حذف]
deleteAppt(apptId) - السطر 37
انقر على رابط [حجز]
reserveSlot(idSlot) - السطر 34

عرض [resa] في السطر 47 هو العرض الذي يظهر عندما ينقر المستخدم على رابط [حجز]:

Image

وفيما يلي كود [resa.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <div id="resa" class="modal fade">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">
                            </span>
                        </button>
                        <!-- <h4 class="modal-title">Modal title</h4> -->
                    </div>
                    <div class="modal-body">
                        <div class="alert alert-info">
                            <h3>
                                <span th:text="#{resa.titre}">Prise de rendez-vous</span>
                            </h3>
                        </div>
                        <div class="row">
                            <div class="col-md-3">
                                <h2 th:text="#{resa.client}">Client</h2>
                                <select name="idClient" id="idClient" class="combobox" data-style="btn-primary">
                                    <option th:each="clientItem : ${clientItems}" th:text="${clientItem.texte}" th:value="${clientItem.id}" />
                                </select>
                            </div>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()" th:text="#{resa.annuler}">Annuler</button>
                        <button type="button" class="btn btn-primary" onclick="javascript:validerRv()" th:text="#{resa.valider}">Valider</button>
                    </div>
                </div><!-- /.modal-content -->
            </div><!-- /.modal-dialog -->
        </div><!-- /.modal -->
        <!-- init page -->
        <script th:inline="javascript">
            /*<![CDATA[*/
            // on initialise la page
            initResa();
        /*]]>*/
        </script>
    </body>
</html>

يحتوي نموذجه على عنصر واحد فقط:

  • [clientItems] (السطر 24): قائمة العملاء؛

ويحتوي على معالجات الأحداث التالية:

event
معالج
النقر على زر [إلغاء]
cancelDialogResa() - السطر 30
انقر على زر [تأكيد]
validerRv() - السطر 31

8.6.5.7. عرض [الأخطاء]

هذه هي طريقة العرض التي تظهر إذا تعذر إكمال الإجراء الذي طلبه المستخدم:

Image

فيما يلي كود [errors.xml]:


<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
    <div class="alert alert-danger">
        <h4>
            <span th:text="#{erreurs.titre}">Les erreurs suivantes se sont produites :</span>
        </h4>
        <ul>
            <li th:each="message : ${erreurs}" th:text="${message}" />
        </ul>
    </div>
</section>

يحتوي القالب على عنصر واحد فقط:

  • [errors] (السطر 8): قائمة الأخطاء المراد عرضها؛

لا يحتوي العرض على معالج أحداث.

8.6.5.8. ملخص

يسرد الجدول التالي طرق العرض ونماذجها:

عرض
النموذج
معالجات الأحداث
navbar-start

تسجيل الدخول، setLang
جومبوترون


تسجيل الدخول


شريط التنقل

تسجيل الخروج، setLang
الصفحة الرئيسية
rdvmedecins.medecinItems (قائمة الأطباء)
الحصول على التقويم
التقويم
التقويم (يوم واحد من التقويم)
حذف موعد، حجز موعد
حجز
عناصر_العملاء (قائمة العملاء)
مربع حوار إلغاء الحجز، تأكيد الموعد
الأخطاء
الأخطاء (قائمة الأخطاء)

8.6.6. الخطوة 3: كتابة الإجراءات

لنعد إلى بنية خدمة الويب [Web1]:

سنلقي الآن نظرة على عناوين URL التي تعرضها [Web1] وكيفية تنفيذها:

8.6.6.1. عناوين URL التي تعرضها خدمة [Web1]

وهي كما يلي:

  • عنوان URL لكل عرض من العروض السابقة أو مزيج منها؛
  • عنوان URL لإضافة موعد؛
  • عنوان URL لحذف موعد؛

تُرجع جميعها استجابة من النوع [Response] على النحو التالي:


public class Reponse {
 
    // ----------------- properties
    // operation status
    private int status;
    // the navigation bar
    private String navbar;
    // the jumbotron
    private String jumbotron;
    // the body of the page
    private String content;
    // the diary
    private String agenda;
...
}
  • السطر 5: حالة الاستجابة: 1 (موافق)، 2 (خطأ)؛
  • السطر 7: دفق HTML لعروض [navbar-start] أو [navbar-run]، حسب الاقتضاء؛
  • السطر 9: موجز HTML لعرض [jumbotron
  • السطر 13: موجز HTML لعرض [agenda
  • السطر 9: موجز HTML لعروض [home] أو [errors] أو [login]، حسب الاقتضاء؛

عناوين URL المعروضة هي كما يلي

/getNavbarStart
يضع عرض [navbar-start] في [Response.navbar]
/getNavbarRun
يضع عرض [navbar-run] في [Response.navbar]
/getHome
يضع عرض [home] في [Response.content]
/getJumbotron
يضع عرض [jumbotron] في [Response.jumbotron]
/getAgenda
يضع عرض [agenda] في [Response.agenda]
/getLogin
يضع عرض [login] في [Response.content]
/getNavbarRunJumbotronHome
  • إذا نجح الاتصال، ضع عرض [navbar-run] في [Response.navbar]، وعرض [jumbotron] في [Response.jumbotron]، وعرض [home] في [Response.content]
  • إذا فشل الاتصال، ضع عرض [errors] في [Response.content] وقم بتعيين [Response.status] على 2
/getNavbarRunJumbotronHomeCalendar
يضع عرض [navbar-run] في [Response.navbar]، وعرض [jumbotron] في [Response.jumbotron]، وعرض [home] في [Response.content]، وعرض [calendar] في [Response.calendar]
/addAppointment
يضيف الموعد المحدد ويضع جدول الأعمال الجديد في [Response.agenda]
/deleteAppointment
يحذف الموعد المحدد ويضع التقويم الجديد في [Response.calendar]

8.6.6.2. السينجلتون [ApplicationModel]

 

يتم إنشاء مثيل لفئة [ApplicationModel] كمثيل واحد ويتم إدخاله في وحدة التحكم في التطبيق. وفيما يلي شفرة البرمجة الخاصة به:


package rdvmedecins.springthymeleaf.server.models;
 
import java.util.ArrayList;
...
 
@Component
public class ApplicationModel implements IDao {
 
....
}
  • السطر 6: [ApplicationModel] هو مكون Spring؛
  • السطر 7: الذي ينفذ واجهة طبقة [DAO]. نقوم بذلك حتى لا تحتاج الإجراءات إلى معرفة طبقة [DAO]، بل فقط عنصر [ApplicationModel] الفريد. تصبح بنية [Web1] عندئذٍ كما يلي:

لنعد إلى كود فئة [ApplicationModel]:


package rdvmedecins.springthymeleaf.server.models;
 
import java.util.ArrayList;
...
 
@Component
public class ApplicationModel implements IDao {
 
    // the [DAO] layer
    @Autowired
    private IDao dao;
    // configuration
    @Autowired
    private AppConfig appConfig;
 
    // data from the [DAO] layer
    private List<ClientItem> clientItems;
    private List<MedecinItem> medecinItems;
    // configuration data
    private String userInit;
    private String mdpUserInit;
    private boolean corsAllowed;
    // exception
    private RdvMedecinsException rdvMedecinsException;
 
    // manufacturer
    public ApplicationModel() {
    }
 
    @PostConstruct
    public void init() {
        // config
        userInit = appConfig.getUSER_INIT();
        mdpUserInit = appConfig.getMDP_USER_INIT();
        dao.setTimeout(appConfig.getTIMEOUT());
        dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
        corsAllowed = appConfig.isCORS_ALLOWED();
        // caching of physician and customer drop-down lists
        List<Medecin> medecins = null;
        List<Client> clients = null;
        try {
            medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
            clients = dao.getAllClients(new User(userInit, mdpUserInit));
        } catch (RdvMedecinsException ex) {
            rdvMedecinsException = ex;
        }
        if (rdvMedecinsException == null) {
            // create drop-down list items
            medecinItems = new ArrayList<MedecinItem>();
            for (Medecin médecin : medecins) {
                medecinItems.add(new MedecinItem(médecin));
            }
            clientItems = new ArrayList<ClientItem>();
            for (Client client : clients) {
                clientItems.add(new ClientItem(client));
            }
        }
    }
 
    // getters and setters
    ...
 
    // interface implementation [IDao]
    @Override
    public void setUrlServiceWebJson(String url) {
        dao.setUrlServiceWebJson(url);
    }
 
    @Override
    public void setTimeout(int timeout) {
        dao.setTimeout(timeout);
    }
 
    @Override
    public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
        return dao.ajouterRv(user, jour, idCreneau, idClient);
    }
 
    ...
}
  • السطر 11: إدخال الإشارة إلى تنفيذ طبقة [DAO]. ثم تُستخدم هذه الإشارة لتنفيذ واجهة [IDao] (الأسطر 64–80)؛
  • السطر 14: إدخال تكوين التطبيق؛
  • الأسطر 33-37: استخدام هذا التكوين لتكوين عناصر مختلفة من بنية التطبيق؛
  • الأسطر 38–46: نقوم بتخزين المعلومات التي ستملأ القوائم المنسدلة الخاصة بالأطباء والعملاء مؤقتًا. ولذلك نفترض أنه في حالة تغيير الطبيب أو العميل، يجب إعادة تشغيل التطبيق. وتكمن الفكرة هنا في إظهار أن كائن Spring الفريد (singleton) يمكن أن يعمل كذاكرة تخزين مؤقتة لتطبيق الويب؛

تشتق فئتا [MedecinItem] و[ClientItem] من فئة [PersonneItem] التالية:


package rdvmedecins.springthymeleaf.server.models;
 
import rdvmedecins.client.entities.Personne;
 
public class PersonneItem {
 
    // element of a list
    private Long id;
    private String texte;
 
    // manufacturer
    public PersonneItem() {
 
    }
 
    public PersonneItem(Personne personne) {
        id = personne.getId();
        texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
    }
 
    // getters and setters
...
}
  • السطر 8: سيكون الحقل [id] هو قيمة السمة [value] لخيار قائمة منسدلة؛
  • السطر 9: سيكون الحقل [text] هو النص المعروض بواسطة خيار القائمة المنسدلة؛

8.6.6.3. فئة [BaseController]

 

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

  1. طرق المساعدة؛
  2. الطرق التي تعرض طرق العرض مدمجة مع نماذجها؛
  3. الطريقة الخاصة بتهيئة الإجراء

protected List<String>
getErrorsForException(Exception exception)

قائمة محمية من نوع String
getErrorsForModel(BindingResult result,
Locale locale,
WebApplicationContext ctx)
طريقتان مساعدتان توفران قائمة برسائل الخطأ. لقد سبق أن صادفناهما واستخدمناهما؛

protected String getPartialViewHome(WebContext
thymeleafContext)
تُرجع طريقة [home] بدون قالب

protected String getPartialViewAgenda(ActionContext
actionContext,
جدول أعمال AgendaMedecinJour،
الموقع (الموقع)
تُرجع عرض [agenda] وقالبها

protected String getPartialViewLogin(WebContext thymeleafContext)
تُرجع عرض [login] بدون نموذج

protected Response getViewErrors(WebContext thymeleafContext, List<String> errors)
تُرجع الاستجابة إلى العميل عندما ينتهي الإجراء المطلوب من بخطأ

protected ActionContext getActionContext
(String lang, String origin,
HttpServletRequest request،
HttpServletResponse response,
BindingResult result،
RdvMedecinsCorsController rdvMedecinsCorsController)
طريقة التهيئة لجميع الإجراءات الخاصة بوحدة التحكم [RdvMedecinsController]

دعونا نستعرض اثنتين من هذه الطرق.

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


    // feed [agenda]
    protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
        // contexts
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        // build the [agenda] page template
        ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
        // the agenda with its model
        thymeleafContext.setVariable("agenda", modelAgenda);
        thymeleafContext.setVariable("clientItems", application.getClientItems());
        return engine.process("agenda", thymeleafContext);
}
  • السطران 9–10: عنصرا نموذج التقويم:
    • السطر 9: التقويم المعروض.
    • السطر 10: قائمة العملاء التي تظهر عندما يقوم المستخدم بحجز موعد؛

طريقة [setModelforAgenda] في السطر 7 هي كما يلي:


// agenda] page template
    private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
        // page title
        String dateFormat = springContext.getMessage("date.format", null, locale);
        Medecin médecin = agenda.getMedecin();
        String titre = springContext.getMessage("agenda.titre", new String[] { médecin.getTitre(), médecin.getPrenom(),
                médecin.getNom(), new SimpleDateFormat(dateFormat).format(agenda.getJour()) }, locale);
        // reservation slots
        ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
        int i = 0;
        for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
            // doctor's slot
            Creneau créneau = creneauMedecinJour.getCreneau();
            ViewModelCreneau modelCréneau = new ViewModelCreneau();
            modelCréneaux[i] = modelCréneau;
            // id
            modelCréneau.setId(créneau.getId());
            // time slot
            modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
                    créneau.getHfin(), créneau.getMfin()));
            Rv rv = creneauMedecinJour.getRv();
            // customer and order
            String commande;
            if (rv == null) {
                modelCréneau.setClient("");
                commande = springContext.getMessage("agenda.reserver", null, locale);
                modelCréneau.setCommande(commande);
                modelCréneau.setAction(ViewModelCreneau.ACTION_RESERVER);
 
            } else {
                Client client = rv.getClient();
                modelCréneau.setClient(String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom()));
                commande = springContext.getMessage("agenda.supprimer", null, locale);
                modelCréneau.setCommande(commande);
                modelCréneau.setIdRv(rv.getId());
                modelCréneau.setAction(ViewModelCreneau.ACTION_SUPPRIMER);
            }
            // next slot
            i++;
        }
        // we render the agenda model
        ViewModelAgenda modelAgenda = new ViewModelAgenda();
        modelAgenda.setTitre(titre);
        modelAgenda.setCreneaux(modelCréneaux);
        return modelAgenda;
    }
  • السطر 6: جدول الأعمال له عنوان:

Image

أو:

Image

يمكننا أن نرى أن تنسيق التاريخ يعتمد على اللغة. نسترد هذا التنسيق من ملفات الرسائل (السطر 4).

  • الأسطر 11–40: لكل فترة زمنية، يجب أن نعرض العرض:

Image

أو العرض:

Image

  • السطور 19-20: عرض الفترة الزمنية؛
  • الأسطر 25-28: الحالة التي تكون فيها الفترة الزمنية متاحة. في هذه الحالة، يجب عرض زر [حجز]؛
  • السطور 31-36: الحالة التي تكون فيها الفترة الزمنية مشغولة. في هذه الحالة، يجب عرض كل من زر العميل وزر [حذف]؛

الطريقة الأخرى التي سنناقشها بمزيد من التفصيل هي طريقة [getActionContext]. يتم استدعاؤها في بداية كل إجراء في [RdvMedecinsController]. وتكون صيغتها كما يلي:


protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController)

وهي تُرجع النوع [ActionContext] التالي:


public class ActionContext {
 
    // data
    private WebContext thymeleafContext;
    private WebApplicationContext springContext;
    private Locale locale;
    private List<String> erreurs;
...
}
  • السطر 4: سياق Thymeleaf الخاص بالإجراء؛
  • السطر 5: سياق Spring الخاص بالإجراء؛
  • السطر 6: الإعدادات المحلية للإجراء؛
  • السطر 7: قائمة محتملة برسائل الخطأ؛

معلماتها هي كما يلي:

  • [lang]: اللغة المطلوبة للإجراء، "en" أو "fr"؛
  • [origin]: رأس HTTP [origin] في حالة طلب عبر النطاقات؛
  • [request]: طلب HTTP الذي يتم معالجته حاليًا، والذي يُشار إليه منذ بعض الوقت باسم "إجراء"؛
  • [response]: الاستجابة التي سيتم إرسالها ردًا على هذا الطلب؛
  • [result]: يتلقى كل إجراء من [RdvMedecinsController] قيمة منشورة يتم اختبار صحتها. [result] هي نتيجة هذا الاختبار؛
  • [rdvMedecinsController]: وحدة التحكم التي تحتوي على الإجراءات؛

يتم تنفيذ طريقة [getActionContext] على النحو التالي:


    // context of an action
    protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
        // language?
        if (lang == null) {
            lang = "fr";
        }
        // local
        Locale locale = null;
        if (lang.trim().toLowerCase().equals("fr")) {
            // french
            locale = new Locale("fr", "FR");
        } else {
            // everything else in English
            locale = new Locale("en", "US");
        }
        // headers CORS
        rdvMedecinsCorsController.sendOptions(origin, response);
        // ActionContext
        ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
        // initialization errors
        RdvMedecinsException e = application.getRdvMedecinsException();
        if (e != null) {
            actionContext.setErreurs(e.getMessages());
            return actionContext;
        }
        // POST errors?
        if (result != null && result.hasErrors()) {
            actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
            return actionContext;
        }
        // no errors
        return actionContext;
}
  • الأسطر 3–15: بناءً على المعلمة [lang]، نقوم بتعيين الإعدادات المحلية للإجراء؛
  • السطر 17: نرسل رؤوس HTTP المطلوبة لطلبات عبر النطاقات. لن ندخل في التفاصيل هنا. التقنية المستخدمة هي تلك الموصوفة في القسم 8.4.14؛
  • السطر 19: إنشاء كائن [ActionContext] دون أخطاء؛
  • السطر 21: رأينا في القسم 8.6.6.2 أن الكائن الفردي [ApplicationModel] قام بالوصول إلى قاعدة البيانات لاسترداد كل من العملاء والأطباء. قد يفشل هذا الوصول. ثم نقوم بتسجيل الاستثناء الذي يحدث. في السطر 21، نسترد هذا الاستثناء؛
  • الأسطر 22-25: إذا حدث استثناء أثناء بدء تشغيل التطبيق، فلن يكون من الممكن اتخاذ أي إجراء. لذلك، نُرجع كائن [ActionContext] لأي إجراء، يحتوي على رسائل الخطأ من الاستثناء؛
  • الأسطر 27–20: نقوم بتحليل المعلمة [result] لتحديد ما إذا كانت القيمة المرسلة صالحة أم لا. إذا كانت غير صالحة، فإننا نرجع كائن [ActionContext] مع رسائل الخطأ المناسبة؛
  • السطر 32: حالة بدون أخطاء؛

سنقوم الآن بفحص إجراءات [RdvMedecinsController]

8.6.6.4. الإجراء [/getNavBarStart]

يعرض الإجراء [/getNavBarStart] طريقة العرض [navbar-start]. وتكون توقيعه كما يلي:


@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin)

تُرجع النوع [Response] التالي:


public class Reponse {
 
    // ----------------- properties
    // operation status
    private int status;
    // the navigation bar
    private String navbar;
    // the jumbotron
    private String jumbotron;
    // the body of the page
    private String content;
    // the diary
    private String agenda;
...
}

وتحتوي على المعلمات التالية:

  • [PostLang postlang]: القيمة التالية التي سيتم نشرها:

public class PostLang {
 
    // data
    @NotNull
    private String lang;
...
}

تعد فئة [PostLang] الفئة الأم لجميع القيم المرسلة. ويرجع ذلك إلى أن العميل يجب أن يحدد دائمًا اللغة التي سيتم تنفيذ الإجراء بها.

يتم تنفيذ الطريقة [getNavbarStart] على النحو التالي:


    // navbar-start
    @RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // returns the [navbar-start] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
        return reponse;
}
  • السطر 7: تهيئة الإجراء؛
  • الأسطر 10-13: إذا أبلغت طريقة تهيئة الإجراء عن أخطاء، يتم إرسالها في الاستجابة إلى العميل (السطر 12) مع الحالة 2:
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • الأسطر 15-18: إرسال عرض [navbar-start] مع الحالة 1:
 {"status":1,"navbar": navbar-start, "jumbotron": null, "agenda":null, "content":null}

فيما يلي، سنقوم فقط بتفصيل الميزات الجديدة.

8.6.6.5. الإجراء [/getNavbarRun]

يعرض الإجراء [/getNavBarRun] طريقة العرض [navbar-run]:


    // navbar-run
    @RequestMapping(value = "/getNavbarRun", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getNavbarRun(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // returns the [navbar-run] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        return reponse;
}

يمكن أن ترجع هذه العملية نوعين من الاستجابات:

  • الاستجابة مع وجود خطأ (الأسطر 10–13):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • الاستجابة باستخدام طريقة العرض [navbar-run]:
 {"status":1,"navbar": navbar-run, "jumbotron": null, "agenda":null, "content":null}

8.6.6.6. الإجراء [/getJumbotron]

يعرض الإجراء [/getJumbotron] عرض [jumbotron]:


    // jumbotron
    @RequestMapping(value = "/getJumbotron", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getJumbotron(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // return view [jumbotron]
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        return reponse;
}

يمكن أن تُرجع هذه العملية نوعين من الاستجابات:

  • الاستجابة التي تحتوي على خطأ (الأسطر 10–13):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • الاستجابة مع عرض [jumbotron]:
 {"status":1,"navbar": null, "jumbotron": jumbotron, "agenda":null, "content":null}

8.6.6.7. الإجراء [/getLogin]

يعرض الإجراء [/getLogin] عرض [login]:


@RequestMapping(value = "/getLogin", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getLogin(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // returns the [login] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
        reponse.setContent(getPartialViewLogin(thymeleafContext));
        return reponse;
    }

يمكن أن ترجع العملية نوعين من الاستجابات:

  • الاستجابة التي تحتوي على خطأ (الأسطر 9–11):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • الاستجابة مع عرض [login]:
 {"status":1,"navbar": navbar-start, "jumbotron": jumbotron, "agenda":null, "content":login}

8.6.6.8. الإجراء [/getHome]

يعرض الإجراء [/getHome] عرض [home]. وتكون صيغته كما يلي:


    @RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) 
  • السطر 3: القيمة المرسلة هي من النوع [PostUser] كما يلي:

public class PostUser extends PostLang {
    // data
    @NotNull
    private User user;
...
}
  • السطر 1: تمتد فئة [PostUser] من فئة [PostLang] وبالتالي تتضمن لغة؛
  • السطر 4: يحاول المستخدم استرداد العرض؛

فيما يلي كود التنفيذ:


    @RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
    @ResponseBody
    public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
            HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // the [home] view is protected
        try{
            // user
            User user = postUser.getUser();
            // we check identifiers [userName, password]
            application.authenticate(user);
        }catch(RdvMedecinsException e){
            // an error is returned
            return getViewErreurs(thymeleafContext, e.getMessages());
        }
        // returns the [home] view
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        return reponse;
}
  • الأسطر 15–22: لاحظ أن الصفحة [الرئيسية] محمية، لذا يجب مصادقة المستخدم؛

يمكن أن ترجع العملية نوعين من الاستجابات:

  • استجابة الخطأ (السطران 11 و21):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • الاستجابة مع عرض [الصفحة الرئيسية] (الأسطر 24–27):
 {"status":1,"navbar": null, "jumbotron": null, "agenda":null, "content":accueil}

8.6.6.9. الإجراء [/getNavbarRunJumbotronHome]

يعرض الإجراء [/getNavbarRunJumbotronHome] طرق العرض [navbar-run، jumbotron، home]. وله التوقيع التالي:


@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser post, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) 
  • السطر 3: القيمة المرسلة من النوع [PostUser

تنفيذ الإجراء كما يلي:


// navbar+ jumbotron + home
    @RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
                rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // the [home] view is protected
        try {
            // user
            User user = postUser.getUser();
            // we check identifiers [userName, password]
            application.authenticate(user);
        } catch (RdvMedecinsException e) {
            // an error is returned
            return getViewErreurs(thymeleafContext, e.getMessages());
        }
        // we send the answer
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        return reponse;
    }

يمكن أن ترجع العملية نوعين من الاستجابات:

  • الاستجابة مع وجود خطأ (السطران 13 و23):
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}
  • الاستجابة مع العروض [navbar-run، jumbotron، home] (الأسطر 26–31):
 {"status":1,"navbar": navbar-run, "jumbotron": jumbotron, "agenda":null, "content":accueil}

8.6.6.10. الإجراء [/getAgenda]

يعرض الإجراء [/getAgenda] عرض [agenda]. وتكون توقيعه كما يلي:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin)
  • السطر 3: القيمة المرسلة هي من النوع [PostGetAgenda] كما يلي:

public class PostGetAgenda extends PostUser {
 
    // data
    @NotNull
    private Long idMedecin;
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date jour;
...
}
  • السطر 1: فئة [PostGetAgenda] تمتد من فئة [PostUser] وبالتالي تتضمن لغة ومستخدمًا؛
  • السطر 5: معرف الطبيب الذي يُراد الحصول على تقويمه؛
  • السطر 8: اليوم المطلوب من التقويم؛

التنفيذ كما يلي:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result,    rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        WebApplicationContext springContext = actionContext.getSpringContext();
        Locale locale = actionContext.getLocale();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // check the validity of the post
        if (result != null) {
            new PostGetAgendaValidator().validate(postGetAgenda, result);
            if (result.hasErrors()) {
                // returns the [errors] view
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        ...
}
  • حتى السطر 14، أصبح الكود الآن قياسيًا؛
  • الأسطر 16–21: نقوم بإجراء فحص إضافي للقيمة المرسلة. يجب أن يكون التاريخ في يوم اليوم أو بعده. للتحقق من ذلك، نستخدم أداة التحقق من الصحة:

package rdvmedecins.web.validators;
 
import java.text.SimpleDateFormat;
import java.util.Date;
 
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
 
import rdvmedecins.springthymeleaf.server.requests.PostGetAgenda;
import rdvmedecins.springthymeleaf.server.requests.PostValiderRv;
 
public class PostGetAgendaValidator implements Validator {
 
    public PostGetAgendaValidator() {
    }
 
    @Override
    public boolean supports(Class<?> classe) {
        return PostGetAgenda.class.equals(classe) || PostValiderRv.class.equals(classe);
    }
 
    @Override
    public void validate(Object post, Errors errors) {
        // the day chosen for the appointment
        Date jour = null;
        if (post instanceof PostGetAgenda) {
            jour = ((PostGetAgenda) post).getJour();
        } else {
            if (post instanceof PostValiderRv) {
                jour = ((PostValiderRv) post).getJour();
            }
        }
        // transform dates into yyyy-MM-dd format
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String strJour = sdf.format(jour);
        String strToday = sdf.format(new Date());
        // the chosen day must not precede today's date
        if (strJour.compareTo(strToday) < 0) {
            errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
        }
    }
 
}
  • السطر 19: يعمل المدقق مع فئتين: [PostGetAgenda] و [PostValiderRv

لنعد إلى كود الإجراء [/getAgenda]:


@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        ...
                // action
        try {
            // doctor's diary
            AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
                    new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
            // answer
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException e1) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, e1.getMessages());
        } catch (Exception e2) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
}
  • السطران 9-10: باستخدام المعلمات المرسلة، نطلب جدول مواعيد الطبيب؛
  • السطران 12-13: نُرجع الجدول:
 {"status":1,"navbar": null, "jumbotron": null, "agenda":agenda, "content":null}
  • السطران 17 و21: نُرجع استجابة تحتوي على أخطاء:
 {"status":2,"navbar": null, "jumbotron": null, "agenda":null, "content":erreurs}

8.6.6.11. الإجراء [/getNavbarRunJumbotronHomeCalendar]

يعرض الإجراء [/getNavbarRunJumbotronHomeCalendar] طرق العرض [navbar-run، jumbotron، home، calendar]. ويتم تنفيذه على النحو التالي:


    @RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
            HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // agenda
        Reponse agenda = getAgenda(post, result, request, response, null);
        if (agenda.getStatus() != 1) {
            return agenda;
        }
        // we send the answer
        Reponse reponse = new Reponse();
        reponse.setStatus(1);
        reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
        reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
        reponse.setContent(getPartialViewAccueil(thymeleafContext));
        reponse.setAgenda(agenda.getAgenda());
        return reponse;
}
  • الأسطر 15–18: نستفيد من وجود الإجراء [/getAgenda] لاستدعائه. ثم نتحقق من حالة الاستجابة (السطر 16). إذا تم الكشف عن خطأ، نتوقف عند هذا الحد ونُرجع الاستجابة؛
  • السطر 20: نرسل العروض المطلوبة:
 {"status":1,"navbar": navbar-run, "jumbotron": jumbotron, "agenda":agenda, "content":accueil}

8.6.6.12. الإجراء [/supprimerRv]

يتيح لك الإجراء [/deleteRv] حذف موعد. وتكون صيغته كما يلي:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin)
  • السطر 3: القيمة المرسلة هي من النوع [PostSupprimerRv] كما يلي:

public class PostSupprimerRv extends PostUser {
 
    // data
    @NotNull
    private Long idRv;
..
}
  • السطر 1: تمتد فئة [PostSupprimerRv] من فئة [PostUser] وبالتالي تتضمن لغة ومستخدمًا؛
  • السطر 5: رقم الموعد المراد حذفه؛

تنفيذ الإجراء كما يلي:


@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result,    HttpServletRequest request, HttpServletResponse response,
            @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
                rdvMedecinsCorsController);
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // posted values
        User user = postSupprimerRv.getUser();
        long idRv = postSupprimerRv.getIdRv();
        // we delete the appointment
        AgendaMedecinJour agenda = null;
        try {
            // we get it back
            Rv rv = application.getRvById(user, idRv);
            Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
            long idMedecin = creneau.getIdMedecin();
            Date jour = rv.getJour();
            // delete the associated rv
            application.supprimerRv(user, idRv);
            // we regenerate the doctor's diary
            agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // we return the new diary
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
}
  • السطر 22: استرداد الموعد المراد حذفه. إذا لم يكن موجودًا، يتم إصدار استثناء؛
  • الأسطر 23–25: بناءً على هذا الموعد، نحدد الطبيب واليوم ذي الصلة. هذه المعلومات ضرورية لإعادة إنشاء جدول مواعيد الطبيب؛
  • السطر 27: يتم حذف الموعد؛
  • السطر 29: نطلب الجدول الجديد للطبيب. هذا أمر مهم. بالإضافة إلى الفترة الزمنية التي تم تحريرها للتو، ربما أجرى مستخدمون آخرون للتطبيق تغييرات على الجدول. من المهم إعادة أحدث نسخة من الجدول إلى المستخدم؛
  • الأسطر 31–34: يتم إرجاع التقويم:
 {"status":1,"navbar": null, "jumbotron": null, "agenda":agenda, "content":null}

8.6.6.13. الإجراء [/validerRv]

يضيف الإجراء [/validerRv] موعدًا إلى تقويم الطبيب. وتكون صيغته كما يلي:


@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request,    HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
  • السطر 3: القيمة المرسلة هي من النوع [PostValiderRv] كما يلي:

public class PostValiderRv extends PostUser {
 
    // data
    @NotNull
    private Long idCreneau;
    @NotNull
    private Long idClient;
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date jour;
...
}
  • السطر 1: تمتد فئة [PostValiderRv] من فئة [PostUser] وبالتالي تتضمن لغة ومستخدمًا؛
  • السطر 5: رقم الفترة الزمنية؛
  • السطر 7: معرف العميل الذي تم الحجز لصالحه؛
  • السطر 10: يوم الموعد؛

يتم تنفيذ الإجراء على النحو التالي:


// appointment validation
    @RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody
    public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
        // action contexts
        ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
        WebApplicationContext springContext = actionContext.getSpringContext();
        WebContext thymeleafContext = actionContext.getThymeleafContext();
        Locale locale = actionContext.getLocale();
        // mistakes?
        List<String> erreurs = actionContext.getErreurs();
        if (erreurs != null) {
            return getViewErreurs(thymeleafContext, erreurs);
        }
        // check the validity of the appointment date
        if (result != null) {
            new PostGetAgendaValidator().validate(postValiderRv, result);
            if (result.hasErrors()) {
                // returns the [errors] view
                return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
            }
        }
        // posted values
        User user = postValiderRv.getUser();
        long idClient = postValiderRv.getIdClient();
        long idCreneau = postValiderRv.getIdCreneau();
        Date jour = postValiderRv.getJour();
        // action
        try {
            // get information on the niche
            Creneau créneau = application.getCreneauById(user, idCreneau);
            long idMedecin = créneau.getIdMedecin();
            // we add the Rv
            application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
            // we regenerate the agenda
            AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
                    new SimpleDateFormat("yyyy-MM-dd").format(jour));
            // we return the new diary
            Reponse reponse = new Reponse();
            reponse.setStatus(1);
            reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
            return reponse;
        } catch (RdvMedecinsException ex) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, ex.getMessages());
        } catch (Exception e2) {
            // returns the [errors] view
            return getViewErreurs(thymeleafContext, getErreursForException(e2));
        }
    }
}

يشبه هذا الكود كود الإجراء [/deleteRv].

8.6.7. الخطوة 4: اختبار خادم Spring/Thymeleaf

سنقوم الآن باختبار الإجراءات المختلفة الموضحة أعلاه باستخدام المكون الإضافي لـ Chrome [Advanced Rest Client] (انظر القسم 9.6).

8.6.7.1. تكوين الاختبار

تتوقع جميع الإجراءات قيمة منشورة. سننشر أشكالًا مختلفة من سلسلة JSON التالية:

{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

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

8.6.7.2. الإجراء [/getNavbarStart]

  • في [1]، الإجراء قيد الاختبار؛
  • في [2]، القيمة المنشورة؛
  • في [3]، القيمة المرسلة هي سلسلة JSON؛
  • في [4]، يُطلب عرض [navbar-start] باللغة الإنجليزية؛

والنتيجة التي تم الحصول عليها هي كما يلي:

 

تلقينا عرض [navbar-start] باللغة الإنجليزية (المناطق المظللة).

الآن، دعونا نحدث خطأً. نضبط السمة [lang] للقيمة المرسلة على null. نحصل على النتيجة التالية:

 

تلقينا استجابة خطأ (الحالة 2) تشير إلى أن حقل [lang] مطلوب.

8.6.7.3. الإجراء [/getNavbarRun]

نطلب الإجراء [getNavbarRun] بالقيمة المنشورة التالية:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة التي تم الحصول عليها هي كما يلي:

 

8.6.7.4. الإجراء [/getJumbotron]

نطلب إجراء [getJumbotron] باستخدام بيانات POST التالية:


{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة التي تم الحصول عليها هي كما يلي:

 

8.6.7.5. الإجراء [/getLogin]

نطلب الإجراء [getLogin] باستخدام بيانات POST التالية:


{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة هي كما يلي:

 

8.6.7.6. الإجراء [/getAccueil]

نطلب الإجراء [getAccueil] بالقيمة المرسلة التالية:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة التي تم الحصول عليها هي كما يلي:

 

نحاول مرة أخرى مع مستخدم مجهول:


{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة هي كما يلي:

 

نبدأ مرة أخرى بمستخدم موجود غير مخول لاستخدام التطبيق:


{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة هي كما يلي:

 

8.6.7.7. الإجراء [/getAgenda]

نطلب الإجراء [getAgenda] بالقيمة المنشورة التالية:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة التي تم الحصول عليها هي كما يلي:

 

نحاول مرة أخرى بتاريخ أقدم من اليوم:

 

نبدأ مرة أخرى بطبيب غير موجود:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":11, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة هي كما يلي:

 

8.6.7.8. الإجراء [/getNavbarRunJumbotronAccueil]

نطلب الإجراء [getNavbarRunJumbotronAccueil] بالقيمة المنشورة التالية:


{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة هي كما يلي:

 

وينطبق الأمر نفسه على المستخدم المجهول:

 

8.6.7.9. الإجراء [/getNavbarRunJumbotronHomeCalendar]

نطلب الإجراء [getNavbarRunJumbotronHomeCalendar] بالقيمة المنشورة التالية:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة هي كما يلي:

 

ندخل اسم طبيب غير موجود:

 

8.6.7.10. الإجراء [/deleteAppointment]

نطلب إجراء [deleteAppointment] بالقيمة المنشورة التالية:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

الموعد رقم 93 غير موجود. والنتيجة التي تم الحصول عليها هي كما يلي:

 

في حالة وجود موعد:

 

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

8.6.7.11. الإجراء [/validateAppointment]

نطلب الإجراء [validateAppointment] بالقيمة المرسلة التالية:


{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}

والنتيجة هي كما يلي:

 

يمكننا التحقق من قاعدة البيانات أن الموعد قد تم إنشاؤه بنجاح. تم إرجاع التقويم الجديد.

نقوم بنفس الشيء مع رقم فتحة غير موجود:

 

ونفعل الشيء نفسه مع معرف عميل غير موجود:

 

8.6.8. الخطوة 5: كتابة عميل JavaScript

لنعد إلى بنية الخادم [Web1]:

عميل [2] للخادم [Web1] هو عميل JavaScript من نوع SPV (تطبيق صفحة واحدة):

  • يطلب العميل صفحة التمهيد من خادم ويب (ليس بالضرورة [Web1])؛
  • ويطلب الصفحات التالية من الخادم [Web1] عبر مكالمات Ajax؛

لبناء هذا العميل، سنستخدم أداة [Webstorm] (انظر القسم 9.8). وجدت هذه الأداة أكثر عملية من STS. وتتمثل ميزتها الرئيسية في أنها توفر ميزة الإكمال التلقائي للكود بالإضافة إلى بعض خيارات إعادة الهيكلة. وهذا يساعد على تجنب العديد من الأخطاء.

8.6.8.1. مشروع JS

يحتوي مشروع JS على بنية الدليل التالية:

  • في [1]، عميل JS ككل. [boot.html] هي صفحة البدء. ستكون هذه هي الصفحة الوحيدة التي يتم تحميلها بواسطة المتصفح؛
  • في [2]، أوراق الأنماط لمكونات Bootstrap؛
  • في [3]، الصور القليلة التي يستخدمها التطبيق؛
  • في [4]، نصوص JS. هذا هو المكان الذي يتم فيه عملنا؛
  • في [5]، مكتبات JS المستخدمة: بشكل أساسي jQuery، وتلك الخاصة بمكونات Bootstrap؛

8.6.8.2. بنية الكود

تم تقسيم الكود إلى ثلاث طبقات:

  • تحتوي طبقة [العرض] على وظائف تهيئة الصفحة [boot.xml] بالإضافة إلى وظائف مكونات Bootstrap المختلفة. ويتم تنفيذها بواسطة الملف [ui.js
  • تحتوي طبقة [الأحداث] على جميع معالجات الأحداث لطبقة [العرض]. ويتم تنفيذها بواسطة ملف [evts.js
  • تقوم طبقة [DAO] بإرسال طلبات HTTP إلى خادم [Web1]. ويتم تنفيذها بواسطة ملف [dao.js

8.6.8.3. طبقة [presentation]

  

يتم تنفيذ طبقة [العرض] بواسطة ملف [ui.js] التالي:


//la couche [présentation]
var ui = {
// variables globales;
  "agenda": "",
  "resa": "",
  "langue": "",
  "urlService": "http://localhost:8081",
  "page": "login",
  "jourAgenda": "",
  "idMedecin": "",
  "user": {},
  "login": {},
  "exceptionTitle": {},
  "calendar_infos": {},
  "erreur": "",
  "idCreneau": "",
  "done": "",
// composants de la vue
  "body": "",
  "navbar": "",
  "jumbotron": "",
  "content": "",
  "exception": "",
  "exception_text": "",
  "exception_title": "",
  "loading": ""
};
// la couche des evts
var evts = {};
// la couche [dao]
var dao = {};
 
// ------------ document ready
$(document).ready(function () {
  // initialisation document
  console.log("document.ready");
  // composants de la page
  ui.navbar = $("#navbar");
  ui.jumbotron = $("#jumbotron");
  ui.content = $("#content");
  ui.erreur = $("#erreur");
  ui.exception = $("#exception");
  ui.exception_text = $("#exception-text");
  ui.exception_title = $("#exception-title");
  // on mémorise la page de login pour pouvoir la restituer
  ui.login.lang = ui.langue;
  ui.login.navbar = ui.navbar.html();
  ui.login.jumbotron = ui.jumbotron.html();
  ui.login.content = ui.content.html();
  // URL du service
  $("#urlService").val(ui.urlService);
});
 
// ------------------------ Bootstrap component initialization functions
ui.initNavBarStart = function () {
...
};
 
ui.initNavBarRun = function () {
...
};
 
ui.initChoixMedecinJour = function () {
...
};
 
ui.updateCalendar = function (renew) {
...
};
 
// affiche le jour sélectionné
ui.displayJour = function () {
...
};
 
ui.initAgenda = function () {
...
};
 
ui.initResa = function () {
 ...
};
 
  • لعزل الطبقات عن بعضها البعض، تقرر وضعها في ثلاثة كائنات:
    • [ui] لطبقة [العرض] (الأسطر 2–27)،
    • [evts] لطبقة إدارة الأحداث (السطر 29)،
    • [dao] لطبقة [DAO] (السطر 31)؛

يساعد هذا الفصل بين الطبقات إلى ثلاثة كائنات على تجنب عدد من تعارضات أسماء المتغيرات والوظائف. تستخدم كل طبقة متغيرات ووظائف مسبوقة ببادئة الكائن الذي يغلف الطبقة.

  • الأسطر 38–44: نقوم بتخزين الحقول التي ستكون موجودة دائمًا بغض النظر عن طرق العرض المعروضة. وهذا يتجنب عمليات البحث المتكررة وغير الضرورية في jQuery؛
  • الأسطر 46–49: يتم تخزين صفحة التمهيد محليًا بحيث يمكن استعادتها عندما يقوم المستخدم بتسجيل الخروج ولم يقم بتغيير اللغة؛
  • الأسطر 54–83: وظائف تهيئة مكونات Bootstrap. تمت تغطية كل هذه الوظائف في مناقشة مكونات Bootstrap في القسم 8.6.4؛

8.6.8.4. وظائف المساعدة في طبقة [الأحداث]

  

تم وضع معالجات الأحداث في ملف [evts.js]. تستخدم معالجات الأحداث عدة دوال بشكل منتظم. نقدمها الآن:


// début d'attente
evts.beginWaiting = function () {
  // début attente
  ui.loading = $("#loading");
  ui.loading.show();
  ui.exception.hide();
  ui.erreur.hide();
  evts.travailEnCours = true;
};
 
// fin d'attente
evts.stopWaiting = function () {
  // fin attente
  evts.travailEnCours = false;
  ui.loading = $("#loading");
  ui.loading.hide();
};
 
// affichage résultat
evts.showResult = function (result) {
  // on affiche les données reçues
  var data = result.data;
  // on analyse le status
  switch (result.status) {
    case 1:
      // erreur ?
      if (data.status == 2) {
        ui.erreur.html(data.content);
        ui.erreur.show();
      } else {
        if (data.navbar) {
          ui.navbar.html(data.navbar);
        }
        if (data.jumbotron) {
          ui.jumbotron.html(data.jumbotron);
        }
        if (data.content) {
          ui.content.html(data.content)
        }
        if (data.agenda) {
          ui.agenda = $("#agenda");
          ui.resa = $("#resa");
        }
      }
      break;
    case 2:
      // affichage erreur
      evts.showException(data);
      break;
  }
};
 
// ------------ fonctions diverses
evts.showException = function (data) {
  // affichage erreur
  ui.exception.show();
  ui.exception_text.html(data);
  ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
  • السطر 2: يتم استدعاء الدالة [evts.beginwaiting] قبل أي إجراء [DAO] غير متزامن؛
  • السطران 4-5: يتم عرض الصورة المتحركة للانتظار؛
  • السطران 6-7: يتم إخفاء منطقة عرض الأخطاء والاستثناءات (وهما ليسا نفس الشيء)؛
  • السطر 8: نلاحظ أن مهمة غير متزامنة قيد التنفيذ؛
  • السطر 12: يتم استدعاء الدالة [evts.stopwaiting] بعد أن تعود عملية [DAO] غير المتزامنة بنتائجها؛
  • السطر 14: نلاحظ أن العملية غير المتزامنة قد اكتملت؛
  • السطر 15: يتم إخفاء الصورة المتحركة للانتظار؛
  • السطر 20: تعرض الدالة [evts.showResult] نتيجة [result] لعمل [DAO] غير متزامن. والنتيجة عبارة عن كائن JS بالشكل التالي: {'status':status,'data':data,'sendMeBack':sendMeBack}.
  • الأسطر 47–50: تُستخدم إذا كان [result.status == 2]. يحدث هذا عندما يرسل خادم [Web1] استجابة مع رأس خطأ HTTP (على سبيل المثال، 403 Forbidden). في هذه الحالة، [data] هي سلسلة JSON التي أرسلها الخادم للإشارة إلى الخطأ؛
  • السطر 25: الحالة التي تم فيها تلقي استجابة صالحة من خادم [Web1]. عندئذٍ يحتوي حقل [data] على استجابة الخادم: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
  • السطر 27: الحالة التي أرسل فيها الخادم [Web1] استجابة خطأ {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':errors};
  • السطران 28 و29: يتم عرض نافذة [الأخطاء]؛
  • السطور 31-33: عرض شريط التنقل اختياري؛
  • السطور 34-36: عرض اختياري للشاشة العملاقة؛
  • الأسطر 37-39: قد يتم عرض حقل [data.content]. اعتمادًا على الحالة، يمثل هذا أحد العروض [home، calendar
  • الأسطر 40-43: إذا تم إعادة إنشاء التقويم، يتم استرداد بعض الإشارات إلى مكوناته حتى لا يتعين البحث عنها في كل مرة تكون مطلوبة؛
  • السطر 54: تعرض الدالة [evts.showException] نص الاستثناء الموجود في معلمتها [data
  • الأسطر 57-58: يتم عرض نص الاستثناء؛
  • السطر 58: يعتمد عنوان الاستثناء على اللغة الحالية؛

يحتوي ملف [evts.js] على أكثر من 300 سطر من التعليمات البرمجية، ولن أعلق عليها بالكامل. سأكتفي بتسليط الضوء على بعض الأمثلة لتوضيح الغرض من هذه الطبقة.

8.6.8.5. تسجيل دخول المستخدم

Image

يتم التعامل مع تسجيل دخول المستخدم بواسطة الوظيفة التالية:


// ------------------------ connexion
evts.connecter = function () {
  // retrieve the values to be posted
  var login = $("#login").val().trim();
  var passwd = $("#passwd").val().trim();
  // set the server's URL
  ui.urlService = $("#urlService").val().trim();
  dao.setUrlService(ui.urlService);
  // query parameters
  var post = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "lang": ui.langue
  };
  var sendMeBack = {
    "user": {
      "login": login,
      "passwd": passwd
    },
    "caller": evts.connecterDone
  };
  // query
  evts.execute([{
    "name": "accueil-sans-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • السطران 4-5: استرداد اسم المستخدم وكلمة المرور؛
  • السطران 7-8: استرداد عنوان URL لخدمة [Web1]. يتم تخزينه في كل من طبقة [ui] وطبقة [dao]؛
  • الأسطر 10-16: القيمة المراد إرسالها: اللغة الحالية والمستخدم الذي يحاول تسجيل الدخول؛
  • الأسطر 17–23: يتم تمرير الكائن [sendMeBack] إلى الدالة [DAO] التي سيتم استدعاؤها، ويجب أن تعيد هذه الدالة الكائن إلى الدالة الموجودة في السطر 22. هنا، يغلف الكائن [sendMeBack] المستخدم الذي يحاول تسجيل الدخول؛
  • الأسطر 25-29: الدالة [evts.execute] قادرة على تنفيذ سلسلة من الإجراءات غير المتزامنة. هنا، نمرر قائمة تتكون من إجراء واحد. حقولها هي كما يلي:
    • [name]: اسم الإجراء غير المتزامن المراد تنفيذه،
    • [post]: القيمة التي سيتم نشرها على خادم [Web1
    • [sendMeBack]: القيمة التي يجب أن تعيدها الإجراء غير المتزامن مع نتيجته؛

قبل الخوض في تفاصيل الدالة [evts.execute]، دعونا نلقي نظرة على الدالة [evts.connecterDone] في السطر 22. هذه هي الدالة التي يجب أن تعيد إليها الدالة [DAO] غير المتزامنة التي تم استدعاؤها نتيجتها:


evts.connecterDone = function (result) {
  // affichage résultat
  evts.showResult(result);
  // connexion réussie ?
  if (result.status == 1 && result.data.status == 1) {
    // page
    ui.page = "accueil-sans-agenda";
    // on note l'utilisateur
    ui.user = result.sendMeBack.user;
  }
};
  • السطر 3: يتم عرض النتيجة التي أرجعها خادم [Web1
  • السطر 5: إذا كانت هذه النتيجة خالية من الأخطاء، فإننا نقوم بتخزين نوع الصفحة الجديدة (السطر 7) وكذلك المستخدم الذي تمت مصادقته (السطر 9)؛

تقوم الدالة [evts.execute] بتنفيذ سلسلة من الإجراءات غير المتزامنة:


// exécution d'une suite d'actions
evts.execute = function (actions) {
  // travail en cours ?
  if (evts.travailEnCours) {
    // on ne fait rien
    return;
  }
  // attente
  evts.beginWaiting();
  // exécution des actions
  dao.doActions(actions, evts.stopWaiting);
};
  • السطر 2: المعلمة [actions] هي قائمة بالإجراءات غير المتزامنة المراد تنفيذها؛
  • الأسطر 4–7: لا يُقبل التنفيذ إلا إذا لم يكن هناك إجراء آخر قيد التنفيذ بالفعل؛
  • السطر 9: يتم بدء الانتظار؛
  • السطر 11: يُطلب من طبقة [DAO] تنفيذ تسلسل الإجراءات. المعلمة الثانية هي اسم الدالة التي سيتم تنفيذها بمجرد أن تعود جميع الإجراءات في التسلسل بنتائجها؛

لن ندخل في تفاصيل حول الدالة [dao.doActions] في الوقت الحالي. سنقوم بفحص حدث آخر.

8.6.8.6. تغيير اللغة

Image

يتم التعامل مع تغيير اللغة بواسطة الدالة التالية:


// ------------------------ changement de langue
evts.setLang = function (lang) {
  // chgt de langue ?
  if (lang == ui.langue) {
    // on ne fait rien
    return;
  }
  // nouvelle langue
  ui.langue = lang;
  // quelle page faut-il traduire ?
  switch (ui.page) {
    case "login":
      evts.getLogin();
      break;
    case "accueil-sans-agenda":
      evts.getAccueilSansAgenda();
      break;
    case "accueil-avec-agenda":
      evts.getAccueilAvecAgenda(ui);
      break;
  }
};
  • السطر 2: المعلمة [lang] هي اللغة الجديدة: 'fr' أو 'en'؛
  • الأسطر 4–7: إذا كانت اللغة الجديدة هي اللغة الحالية، فلا تفعل شيئًا؛
  • السطر 9: يتم تخزين اللغة الجديدة؛
  • الأسطر 12–20: إذا تغيرت اللغة، يجب إعادة تحميل الصفحة المعروضة حاليًا بواسطة المتصفح. هناك ثلاث صفحات محتملة:
    • الصفحة المسماة [login]، حيث تُعرض صفحة تسجيل الدخول،
    • الصفحة المسماة [home-without-calendar]، وهي الصفحة التي تظهر فور نجاح عملية المصادقة،
    • الصفحة المسماة [home-with-calendar]، وهي الصفحة التي تظهر فور عرض التقويم الأول. ثم تظل على الشاشة حتى يقوم المستخدم بتسجيل الخروج؛

سنتناول حالة صفحة [home-with-calendar]. هناك ثلاثة إصدارات لهذه الوظيفة:

  
  • تنفذ نسخة [getAccueilAvecAgenda-one] إجراءً غير متزامن واحد؛
  • تنفذ النسخة [getAccueilAvecAgenda-parallel] أربعة إجراءات غير متزامنة بالتوازي؛
  • تنفذ النسخة [getAccueilAvecAgenda-sequence] أربعة إجراءات غير متزامنة واحدة تلو الأخرى؛

8.6.8.7. وظيفة [getAccueilAvecAgenda-one]

هذه هي الدالة التالية:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // query parameters
  var post = {
    "user": ui.user,
    "lang": ui.langue,
    "idMedecin": ui.idMedecin,
    "jour": ui.jourAgenda
  };
  var sendMeBack = {
    "caller": evts.getAccueilAvecAgendaDone
  };
  // request
  evts.execute([{
    "name": "accueil-avec-agenda",
    "post": post,
    "sendMeBack": sendMeBack
  }]);
};
  • الأسطر 4-9: القيمة المراد إرسالها تتضمن المستخدم المسجل، واللغة المطلوبة، ومعرف الطبيب الذي يُراد الاطلاع على جدوله، ويوم الموعد المطلوب؛
  • الأسطر 10–12: الكائن [sendMeBack] هو الكائن الذي سيتم إرجاعه إلى الدالة في السطر 11. هنا، لا يحتوي على أي معلومات؛
  • الأسطر 14-18: تنفيذ سلسلة من الإجراءات غير المتزامنة، وتحديدًا الإجراء المسمى [welcome-with-calendar] (السطر 15)؛
  • السطر 11: الدالة التي يتم تنفيذها عندما تعيد الإجراء غير المتزامن [welcome-with-calendar] نتيجتها؛

تعرض الدالة [evts.getAccueilAvecAgendaDone] في السطر 11 نتيجة الدالة غير المتزامنة المسماة [accueil-avec-agenda]:


evts.getAccueilAvecAgendaDone = function (result) {
  // affichage résultat
  evts.showResult(result);
  // nouvelle page ?
  if (result.status == 1 && result.data.status == 1) {
    ui.page = "accueil-avec-agenda";
  }
};
  • السطر 1: [result] هي نتيجة الدالة غير المتزامنة المسماة [home-with-calendar
  • السطر 3: يتم عرض هذه النتيجة؛
  • السطر 5: إذا كانت النتيجة خالية من الأخطاء، يتم تحميل الصفحة الجديدة (السطر 6)؛

8.6.8.8. الدالة [getHomeWithCalendar-parallel]

هذه هي الدالة التالية:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // actions [navbar-run, jumbotron, home, calendar] in //
  // navbar-run
  var navbarRun = {
    "name": "navbar-run"
  };
  navbarRun.post = {
    "lang": ui.langue
  };
  navbarRun.sendMeBack = {
    "caller": evts.showResult
  };
  // jumbotron
  var jumbotron = {
    "name": "jumbotron"
  };
  jumbotron.post = {
    "lang": ui.langue
  };
  jumbotron.sendMeBack = {
    "caller": evts.showResult
  };
  // home
  var accueil = {
    "name": "accueil"
  };
  accueil.post = {
    "lang": ui.langue,
    "user": ui.user
  };
  accueil.sendMeBack = {
    "caller": evts.showResult
  };
  // agenda
  var agenda = {
    "name": "agenda"
  };
  agenda.post = {
    "user": ui.user,
    "lang": ui.langue,
    "idMedecin": ui.idMedecin,
    "jour": ui.jourAgenda
  };
  agenda.sendMeBack = {
    'idMedecin': ui.idMedecin,
    'jour': ui.jourAgenda,
    "caller": evts.getAgendaDone
  };
  // execution actions in //
  evts.execute([navbarRun, jumbotron, accueil, agenda])
};
  • السطر 51: هذه المرة، يتم تنفيذ أربعة إجراءات غير متزامنة. سيتم تنفيذها بالتوازي؛
  • الأسطر 5–13: تعريف الإجراء [navbarRun]، الذي يسترد شريط التنقل [navbar-run
  • السطر 12: الدالة التي سيتم تنفيذها بمجرد أن تعود الإجراء غير المتزامن [navbarRun] بنتيجته؛
  • الأسطر 15–23: تعريف الإجراء [jumbotron]، الذي يسترد عرض [jumbotron
  • السطر 22: الدالة التي يتم تنفيذها عندما يعود الإجراء غير المتزامن [jumbotron] بنتائجه؛
  • الأسطر 25-34: تعريف الإجراء [home]، الذي يسترد عرض [home
  • السطر 33: الدالة التي سيتم تنفيذها عندما يعود الإجراء غير المتزامن [home] بنتائجه؛
  • الأسطر 36–49: تعريف الإجراء [agenda] الذي يسترد عرض [jumbotron
  • السطر 48: الدالة التي يجب تنفيذها عندما يعرض الإجراء غير المتزامن [agenda] نتيجته؛

8.6.8.9. الدالة [getHomeWithAgenda-sequence]

هذه هي الدالة التالية:


// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
  // actions [navbar-run, jumbotron, home, agenda] in order
  // agenda
  var agenda = {
    "name" : "agenda"
  };
  agenda.post = {
    "user" : ui.user,
    "lang" : ui.langue,
    "idMedecin" : ui.idMedecin,
    "jour" : ui.jourAgenda
  };
  agenda.sendMeBack = {
    'idMedecin' : ui.idMedecin,
    'jour' : ui.jourAgenda,
    "caller" : evts.getAgendaDone
  };
  // home
  var accueil = {
    "name" : "accueil"
  };
  accueil.post = {
    "lang" : ui.langue,
    "user" : ui.user
  };
  accueil.sendMeBack = {
    "caller" : evts.showResult,
    "next" : agenda
  };
  // jumbotron
  var jumbotron = {
    "name" : "jumbotron"
  };
  jumbotron.post = {
    "lang" : ui.langue
  };
  jumbotron.sendMeBack = {
    "caller" : evts.showResult,
    "next" : accueil
  };
  // navbar-run
  var navbarRun = {
    "name" : "navbar-run"
  };
  navbarRun.post = {
    "lang" : ui.langue
  };
  navbarRun.sendMeBack = {
    "caller" : evts.showResult,
    "next" : jumbotron
  };
  // execution actions in sequence
  evts.execute([ navbarRun ])
};
  • السطر 54: يتم تنفيذ الإجراء [navbarRun]. عند الانتهاء، ننتقل إلى الإجراء التالي: [jumbotron]، السطر 51. ثم يتم تنفيذ هذا الإجراء بدوره. عند الانتهاء، ننتقل إلى الإجراء التالي: [home]، السطر 40. يتم تنفيذ هذا الإجراء بدوره. عند الانتهاء، ننتقل إلى الإجراء التالي: [agenda]، السطر 29. يتم تنفيذ هذا الإجراء بدوره. عند الانتهاء، نتوقف لأن الإجراء [agenda] لا يتبعه أي إجراء آخر.

8.6.8.10. طبقة [DAO]

  

يحتوي ملف [dao.js] على جميع وظائف طبقة [DAO]. سنقدم هذه الوظائف تدريجيًا:


// URL exposed by the server
dao.urls = {
  "login": "/getLogin",
  "accueil": "/getAccueil",
  "jumbotron": "/getJumbotron",
  "agenda": "/getAgenda",
  "supprimerRv": "/supprimerRv",
  "validerRv": "/validerRv",
  "navbar-start": "/getNavbarStart",
  "navbar-run": "/getNavbarRun",
  "accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
  "accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- interface
// server url
dao.setUrlService = function (urlService) {
  dao.urlService = urlService;
};
  • الأسطر 16–18: الدالة التي تحدد عنوان URL للخدمة [Web1
  • الأسطر 2-13: القاموس الذي يربط اسم الإجراء غير المتزامن بعنوان URL لخادم [Web1] المراد الاستعلام عنه؛

// ------------------ gestion générique des actions
// exécution d'une suite d'actions asynchrones
dao.doActions = function (actions, done) {
  // traitement des actions
  dao.actionsCount = actions.length;
  dao.actionIndex = 0;
  for (var i = 0; i < dao.actionsCount; i++) {
    // requête DAO asynchrone
    var deferred = $.Deferred();
    deferred.done(dao.actionDone);
    dao.doAction(deferred, actions[i], done);
  }
};
  • السطر 3: تقوم الدالة [dao.doActions] بتنفيذ سلسلة من الإجراءات غير المتزامنة [actions]. المعلمة [done] هي الدالة التي سيتم تنفيذها بمجرد أن تعود جميع الإجراءات بنتائجها؛
  • الأسطر 7–12: يتم تنفيذ الإجراءات غير المتزامنة بالتوازي. ومع ذلك، إذا كان لأحدها خليفة، يتم تنفيذ ذلك الخليفة في نهاية الإجراء السابق؛
  • السطر 9: كائن [Deferred] في الحالة [pending
  • السطر 10: عندما يدخل هذا الكائن في حالة [resolved]، سيتم تنفيذ الدالة [dao.actionDone
  • السطر 11: يتم تنفيذ الإجراء رقم i في القائمة بشكل غير متزامن. ويتم تمرير المعلمة [done] من السطر 3 كحجة؛

وظيفة [dao.actionDone]، التي يتم تنفيذها في نهاية كل إجراء غير متزامن، هي كما يلي:


// on a reçu un résultat
dao.actionDone = function (result) {
  // caller ?
  var sendMeBack = result.sendMeBack;
  if (sendMeBack && sendMeBack.caller) {
    sendMeBack.caller(result);
  }
  // next ?
  if (sendMeBack && sendMeBack.next) {
    // requête DAO asynchrone
    var deferred = $.Deferred();
    deferred.done(dao.actionDone);
    dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
  }
  // fini ?
  dao.actionIndex++;
  if (dao.actionIndex == dao.actionsCount) {
    // done ?
    if (sendMeBack && sendMeBack.done) {
      sendMeBack.done(result);
    }
  }
};
  • السطر 2: تستقبل الدالة [dao.actionDone] النتيجة [result] من أحد الإجراءات غير المتزامنة في قائمة الإجراءات المقرر تنفيذها؛
  • الأسطر 4–7: إذا حدد الإجراء غير المتزامن المكتمل دالة يجب إرجاع النتيجة إليها، يتم استدعاء تلك الدالة؛
  • الأسطر 9-14: إذا كان للإجراء غير المتزامن المكتمل إجراء لاحق، يتم تنفيذ هذا الإجراء بدوره؛
  • السطر 16: يتم إكمال الإجراء. يتم زيادة عداد الإجراءات المكتملة. يُحسب الإجراء الذي له عدد غير محدد من الإجراءات اللاحقة كإجراء واحد؛
  • الأسطر 19–21: إذا تم تحديد دالة [done] مسبقًا لتنفيذها عند عودة جميع الإجراءات في التسلسل بنتائجها، فإن هذه الدالة تُنفَّذ الآن؛

تقوم طريقة [dao.doAction] بتنفيذ إجراء غير متزامن:


// exécution d'une action
dao.doAction = function (deferred, action, done) {
  // fonction done à embarquer dans l'action
  if (action.sendMeBack) {
    action.sendMeBack.done = done;
  } else {
    action.sendMeBack = {
      "done": done
    };
  }
  // exécution action
  dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
  • الأسطر 4–10: كما رأينا للتو، يجب أن يكون للدالة التي ستتعامل مع نتيجة الإجراء غير المتزامن المراد تنفيذه حق الوصول إلى الدالة [done]. للقيام بذلك، نضع الدالة [done] في الكائن [sendMeBack]، الذي سيكون جزءًا من نتيجة العملية غير المتزامنة؛
  • السطر 12: نقوم بتنفيذ الدالة [dao.executePost]، التي ترسل طلب HTTP إلى خادم [Web1]. عنوان URL الهدف هو عنوان URL المرتبط باسم الإجراء المراد تنفيذه؛

تقوم الدالة [dao.executePost] بتنفيذ طلب HTTP:


// requête HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
  // on fait un appel Ajax à la main
  $.ajax({
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    url: dao.urlService + url,
    type: 'POST',
    data: JSON3.stringify(post),
    dataType: 'json',
    success: function (data) {
      // on rend le résultat
      deferred.resolve({
        "status": 1,
        "data": data,
        "sendMeBack": sendMeBack
      });
    },
    error: function (jqXHR, textStatus, errorThrown) {
      var data;
      if (jqXHR.responseText) {
        data = jqXHR.responseText;
      } else {
        data = textStatus;
      }
      // on rend l'erreur
      deferred.resolve({
        "status": 2,
        "data": data,
        "sendMeBack": sendMeBack
      });
    }
  });
};

لقد سبق أن تناولنا هذه الوظيفة وناقشناها. لاحظ ببساطة في السطر 9 أن عنوان URL الهدف هو تسلسل عنوان URL للخادم [Web1] مع عنوان URL المرتبط باسم الإجراء.

8.6.8.11. صفحة التمهيد

  

Image

تعرض صفحة التمهيد [boot.html] العرض الموضح أعلاه. وهي الصفحة الوحيدة التي يتم تحميلها مباشرةً بواسطة المتصفح. أما الصفحات الأخرى فيتم استردادها عبر استدعاءات Ajax. وفيما يلي كودها:


<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
  <meta name="viewport" content="width=device-width"/>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title>RdvMedecins</title>
  <!-- Bootstrap core CSS -->
  <link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
  <link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
  <link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
  <link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
  <!-- Custom styles for this template -->
  <link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
  <!-- Bootstrap core JavaScript ================================================== -->
  <script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
  <script type="text/javascript" src="vendor/bootstrap.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-select.js"></script>
  <script type="text/javascript" src="vendor/moment-with-locales.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
  <script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
  <script type="text/javascript" src="vendor/footable.js"></script>
  <!-- user scripts -->
  <script type="text/javascript" src="js/json3.js"></script>
  <script type="text/javascript" src="js/ui.js"></script>
  <script type="text/javascript" src="js/evts.js"></script>
  <script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
  <script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
      <div class="navbar-collapse collapse">
        <img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
        <!-- identification form -->
        <div class="navbar-form navbar-right" role="form" id="formulaire">
          <div class="form-group">
            <input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
          </div>
          <div class="form-group">
            <input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
          </div>
          <div class="form-group">
            <input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
          </div>
          <button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
          <!-- languages -->
          <div class="btn-group">
            <button type="button" class="btn btn-danger">Langue</button>
            <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
              <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
            </button>
            <ul class="dropdown-menu" role="menu">
              <li><a href="javascript:evts.setLang('fr')">Français</a></li>
              <li><a href="javascript:evts.setLang('en')">English</a></li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
<div class="container">
  <!-- Bootstrap Jumbotron -->
  <div id="jumbotron">
    <div class="jumbotron">
      <div class="row">
        <div class="col-md-2">
          <img src="images/caduceus.jpg" alt="RvMedecins"/>
        </div>
        <div class="col-md-10">
          <h1>
            Cabinet médical<br/>Les Médecins associés
          </h1>
        </div>
      </div>
    </div>
  </div>
  <!-- error panels -->
  <div id="erreur"></div>
  <div id="exception" class="alert alert-danger" style="display: none">
    <h3 id="exception-title"></h3>
    <span id="exception-text"></span>
  </div>
  <!-- content -->
  <div id="content">
    <div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
  </div>
</div>
<!-- init page -->
<script>
  // on initialise la page
  ui.langue = 'fr';
  ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
  ui.exceptionTitle['en'] = "The following server error was met:";
  ui.initNavBarStart();
</script>
</body>
</html>
  • لقد سبق أن تناولنا هذا النوع من الصفحات في الفصل الخاص بـ Bootstrap (القسم 8.6.4
  • الأسطر 99–105: تهيئة عناصر معينة من طبقة [العرض]؛
  • السطر 27: يتم استخدام البرنامج النصي [getAccueilAvecAgenda-sequence.js]. من خلال تغيير البرنامج النصي في هذا السطر، نحصل على ثلاثة سلوكيات مختلفة لاسترداد صفحة [accueil-avec-agenda]:
    • يسترد [getAccueilAvecAgenda-one.js] الصفحة بطلب HTTP واحد،
    • [getAccueilAvecAgenda-parallel.js] يسترد الصفحة بأربعة طلبات HTTP متزامنة،
    • [getAccueilAvecAgenda-sequence.js] يسترد الصفحة بأربعة طلبات HTTP متتالية؛

8.6.8.12. الاختبارات

هناك طرق مختلفة لإجراء الاختبارات. هنا، سنستخدم أداة [Webstorm]:

  • في [1] نفتح مشروعًا. نختار ببساطة المجلد [2] الذي يحتوي على بنية الدليل الثابتة (HTML، CSS، JS) للموقع المراد اختباره؛
  • في [3]، الموقع الثابت؛
  • في [4-5]، قم بتحميل صفحة [boot.html
  • في [5]، نرى أن خادمًا مدمجًا بواسطة [Webstorm] قد قدم صفحة [boot.html] من المنفذ [63342]. هذه نقطة مهمة يجب فهمها لأنها تعني أن البرامج النصية الموجودة في صفحة [boot.html] ستقوم بطلبات عبر النطاقات إلى خادم [Web1]، الذي يعمل على [localhost:8081]. يعرف المتصفح الذي قام بتحميل [boot.html] أنه قام بتحميلها من [localhost:63342]. ولذلك لن يسمح لهذه الصفحة بإجراء مكالمات إلى الموقع [localhost:8081] لأنها ليست نفس المنفذ. وبالتالي سيطبق قواعد عبر النطاقات الموضحة في القسم 8.4.14. لهذا السبب، يجب تكوين تطبيق [Web1] لقبول هذه الطلبات عبر النطاقات. يتم تكوين هذا في ملف [AppConfig] لخادم Spring/Thymeleaf:
 

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
 
    // admin / admin
    private final String USER_INIT = "admin";
    private final String MDP_USER_INIT = "admin";
    // root web service / json
    private final String WEBJSON_ROOT = "http://localhost:8080";
    // timeout in milliseconds
    private final int TIMEOUT = 5000;
    // CORS
    private final boolean CORS_ALLOWED=true;
...

نترك للقارئ مهمة اختبار عميل JS. يجب أن يكون قادراً على إعادة إنتاج الوظيفة الموضحة في القسم 8.6.3.

بمجرد التحقق من صحة عميل JavaScript، يمكن نشره في مجلد [Web1] الخاص بالخادم لتجنب الحاجة إلى السماح بالطلبات عبر النطاقات:

  

في الأعلى، قمنا بنسخ الموقع الذي تم اختباره إلى المجلد [src/main/resources/static]. بعد ذلك، يمكننا طلب عنوان URL [http://localhost:8081/boot.html]:

Image

الآن لم نعد بحاجة إلى الطلبات عبر النطاقات، ويمكننا كتابة ما يلي في ملف التكوين [AppConfig] لخادم [Web1]:


    // CORS
    private final boolean CORS_ALLOWED=false;

سيستمر التطبيق أعلاه في العمل. إذا عدنا إلى تطبيق [WebStorm]، فسيتوقف عن العمل:

Image

Image

إذا انتقلنا إلى وحدة تحكم المطور (Ctrl-Shift-I)، فسنرى سبب الخطأ:

Image

هذا خطأ طلب عبر النطاقات غير مصرح به.

8.6.8.13. الخلاصة

لقد قمنا بتنفيذ بنية JS التالية:

  • الطبقات منفصلة بشكل واضح إلى حد ما؛
  • لدينا تطبيق أحادي الصفحة (SPA). وهذه الميزة هي التي ستسمح لنا الآن بإنشاء تطبيق أصلي لمختلف منصات الهواتف المحمولة (Android، iOS، Windows Phone
  • لقد أنشأنا نموذجًا قادرًا على تنفيذ الإجراءات غير المتزامنة بشكل متوازٍ أو متسلسل أو مزيج من الاثنين؛

8.6.9. الخطوة 6: إنشاء تطبيق أصلي لنظام Android

تتيح لك أداة [Phonegap] [http://phonegap.com/] إنتاج ملف قابل للتنفيذ للأجهزة المحمولة (Android و iOS و Windows 8 وغيرها) من تطبيق HTML/JS/CSS. هناك طرق مختلفة لتحقيق ذلك. سنستخدم الطريقة الأبسط: أداة عبر الإنترنت متوفرة على موقع Phonegap [http://build.phonegap.com/apps]. ستقوم هذه الأداة بتحميل ملف ZIP للموقع الثابت المراد تحويله. يجب أن يكون اسم صفحة التمهيد [index.html]. لذا، نقوم بتغيير اسم الصفحة [boot.html] إلى [index.html]:

 

ثم نقوم بضغط المجلد، في هذه الحالة [rdvmedecins-client-js-03]. بعد ذلك، ننتقل إلى موقع Phonegap [http://build.phonegap.com/apps]:

  • قبل [1]، قد تحتاج إلى إنشاء حساب؛
  • في [1]، نبدأ؛
  • في [2]، نختار خطة مجانية تسمح بتطبيق Phonegap واحد فقط؛
  • في [3]، نقوم بتحميل التطبيق المضغوط [4]؛
  • في [5]، قم بتسمية التطبيق؛
  • في [6]، قم بإنشاء التطبيق. قد يستغرق ذلك دقيقة واحدة. انتظر حتى تشير أيقونات المنصات المحمولة المختلفة إلى اكتمال عملية الإنشاء؛
  • تم إنشاء ملفات Android [7] و Windows [8] الثنائية فقط؛
  • انقر على [7] لتنزيل الملف الثنائي لنظام Android؛
  • في [9]، الملف الثنائي [apk] الذي تم تنزيله؛

قم بتشغيل محاكي [GenyMotion] لجهاز لوحي يعمل بنظام Android (انظر القسم 9.9):

 

أعلاه، نقوم بتشغيل محاكي جهاز لوحي يعمل بنظام Android API 19. بمجرد تشغيل المحاكي،

  • قم بإلغاء قفله عن طريق سحب القفل (إن وجد) إلى الجانب ثم تركه؛
  • باستخدام الماوس، اسحب ملف [PGBuildApp-debug.apk] الذي قمت بتنزيله وأسقطه على المحاكي. سيتم بعد ذلك تثبيته وتشغيله؛

تحتاج إلى تغيير عنوان URL إلى [1]. للقيام بذلك، في نافذة موجه الأوامر، اكتب الأمر [ipconfig] (السطر 1 أدناه)، والذي سيعرض عناوين IP المختلفة لجهازك:


C:\Users\Serge Tahé>ipconfig
 
Configuration IP de Windows
 
 
Carte réseau sans fil Connexion au réseau local* 15 :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :
 
Carte Ethernet Connexion au réseau local :
 
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
   Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
   Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
   Masque de sous-réseau. . . . . . . . . : 255.255.0.0
   Passerelle par défaut. . . . . . . . . : 172.19.0.254
 
Carte réseau sans fil Wi-Fi :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :
 
...

قم بتدوين عنوان IP لشبكة Wi-Fi (الأسطر 6–9) أو عنوان IP للشبكة المحلية (الأسطر 11–17). ثم استخدم عنوان IP هذا في عنوان URL لخادم الويب:

بمجرد الانتهاء من ذلك، قم بالاتصال بخدمة الويب:

اختبر التطبيق على المحاكي. من المفترض أن يعمل. على جانب الخادم، يمكنك السماح أو عدم السماح برؤوس CORS في فئة [ApplicationModel]:


    // CORS
    private final boolean CORS_ALLOWED=false;

هذا لا يهم بالنسبة لتطبيق Android. فهو لا يعمل في متصفح. متطلبات رؤوس CORS تأتي من المتصفح، وليس من الخادم.

8.6.10. خلاصة دراسة الحالة

قمنا بتطوير البنية التالية:

إنها بنية معقدة من 3 طبقات. وقد صُممت لإعادة استخدام طبقة [Web2]، التي كانت طبقة الخادم لتطبيق [AngularJS-Spring MVC] من وثيقة [AngularJS / Spring 4 Tutorial] الموجودة على الرابط [http://tahe.developpez.com/angularjs-spring4/]. هذا هو السبب الوحيد لوجود بنية ثلاثية الطبقات. في حين أن عميل [Web2] في تطبيق [AngularJS-Spring MVC] كان عميل [AngularJS]، فإن عميل [Web2] هنا هو بنية ثنائية الطبقات [jQuery] / [Spring MVC / Thymeleaf]. لقد قمنا بزيادة عدد الطبقات، لذا سنفقد بعض الأداء.

تم تطوير التطبيق الذي نناقشه هنا على مدار الوقت في ثلاثة مستندات مختلفة:

  1. [مقدمة إلى JSF2 و PrimeFaces و PrimeFaces Mobile] على الرابط [http://tahe.developpez.com/java/primefaces/]. ثم تم تطوير دراسة الحالة باستخدام أطر عمل JSF2 / PrimeFaces. PrimeFaces هي مكتبة من المكونات التي تدعم AJAX والتي تلغي الحاجة إلى كتابة JavaScript. كان التطبيق الذي تم تطويره في ذلك الوقت أقل تعقيدًا من التطبيق الذي تمت دراسته هنا. كان يحتوي على إصدار ويب كلاسيكي لأجهزة الكمبيوتر وإصدار محمول للهواتف؛
  2. [دليل AngularJS / Spring 4] على الرابط [http://tahe.developpez.com/angularjs-spring4/]. كان التطبيق الذي تم تطويره في ذلك الوقت يتمتع بنفس الميزات التي تمت مناقشتها في هذا المستند. كما تم نقل التطبيق إلى نظام Android؛
  3. هذا المستند؛

من هذا العمل، تبرز لي النقاط التالية:

  • كان تطبيق [Primefaces] هو الأسهل في الكتابة، وأثبتت نسخته على الويب للجوال أنها عالية الأداء. ولا يتطلب أي معرفة بلغة JavaScript. ولا يمكن نقله بشكل أصلي إلى أنظمة تشغيل الأجهزة المحمولة المختلفة، ولكن هل هذا ضروري؟ يبدو من الصعب تغيير نمط التطبيق. فنحن، في الواقع، نعمل مع أوراق أنماط Primefaces. وقد يكون هذا عيبًا؛
  • كان تطبيق [AngularJS-Spring MVC] معقدًا في الكتابة. بدا إطار عمل [AngularJS] صعب الفهم إلى حد ما عندما تريد إتقانه. تتميز بنية [Angular client] / [web service / JSON implemented by Spring MVC] بأنها نظيفة للغاية وعالية الأداء. هذه البنية قابلة للتكرار في أي تطبيق ويب. وهي البنية التي تبدو لي الأكثر واعدة لأنها تتضمن مجموعات مهارات مختلفة على جانبي العميل والخادم (JS+HTML+CSS على جانب العميل، وJava أو أي شيء آخر على جانب الخادم)، مما يسمح بتطوير العميل والخادم بالتوازي؛
  • بالنسبة للتطبيق الذي تم تطويره في هذا المستند باستخدام بنية ثلاثية المستويات [عميل jQuery] / [خادم Web1 / Spring MVC / Thymeleaf] / [خادم Web2 / Spring MVC]، قد يجد البعض أن تقنية [jQuery+Spring MVC+Thymeleaf] أسهل في الفهم من تقنية [AngularJS]. طبقة [DAO] لعميل JavaScript التي كتبناها قابلة لإعادة الاستخدام في تطبيقات أخرى؛