Skip to content

5. التطبيق IMPOTS

5.1. مقدمة

نستعرض هنا التطبيق IMPOTS الذي تم استخدامه مرارًا وتكرارًا في الكتيب الدراسي الخاص بلغة Java لنفس المؤلف. ولنتذكر المشكلة المطروحة. يتعلق الأمر بكتابة تطبيق يسمح بحساب الضريبة المستحقة على أحد دافعي الضرائب. سنأخذ الحالة المبسطة لدافع ضرائب ليس لديه سوى راتبه الوحيد ليصرح به:

  • نحسب عدد حصص الموظف nbParts=nbEnfants/2 +1 إذا كان غير متزوج، nbEnfants/2+2 إذا كان متزوجًا، حيث nbEnfants هو عدد أطفاله.
  • وإذا كان لديه ثلاثة أطفال على الأقل، يحصل على نصف حصة إضافية
  • يُحسب دخله الخاضع للضريبة R=0.72*S حيث S هو راتبه السنوي
  • يُحسب معامل الأسرة الخاص به QF = R/nbParts
  • نحسب ضريبته I. لننظر إلى الجدول التالي:
12620.0
0
0
13190
0.05
631
15640
0.1
1290.5
24740
0.15
2072.5
31810
0.2
3309.5
39970
0.25
4900
48360
0.3
6898.5
55790
0.35
9316.5
92970
0.4
12106
127860
0.45
16754.5
151250
0.50
23147.5
172040
0.55
30710
195000
0.60
39312
0
0.65
49062

يحتوي كل سطر على 3 حقول. لحساب الضريبة I، نبحث عن أول سطر حيث QF <= الحقل 1. على سبيل المثال، إذا كان QF = 23000، فسنجد السطر

    24740        0.15        2072.5

وعندئذٍ تكون الضريبة I مساوية لـ 0.15*R - 2072.5*nbParts. إذا كان QF بحيث لا تتحقق العلاقة QF<=champ1 أبدًا، فسيتم استخدام معاملات السطر الأخير. وهنا:

    0                0.65        49062

مما يعطي الضريبة I=0.65*R - 49062*nbParts.

توجد البيانات التي تحدد الشرائح الضريبية المختلفة في قاعدة بيانات ODBC-MySQL. MySQL هو ملف SGBD متاح للجمهور وقابل للاستخدام على منصات مختلفة، بما في ذلك Windows وLinux. باستخدام ملف SGBD هذا، تم إنشاء قاعدة بيانات dbimpots تحتوي على جدول واحد يسمى impots. يتم التحكم في الوصول إلى قاعدة البيانات عن طريق اسم المستخدم وكلمة المرور، وهما هنا admimpots/mdpimpots. توضح لقطة الشاشة التالية كيفية استخدام قاعدة البيانات dbimpots مع ملف MySQL:


C:\Program Files\EasyPHP\mysql\bin>mysql -u admimpots -p
Enter password: *********
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 18 to server version: 3.23.49-max-nt

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> use dbimpots;
Database changed

mysql> show tables;
+--------------------+
| Tables_in_dbimpots |
+--------------------+
| impots             |
+--------------------+
1 row in set (0.00 sec)

mysql> describe impots;
+---------+--------+------+-----+---------+-------+
| Field   | Type   | Null | Key | Default | Extra |
+---------+--------+------+-----+---------+-------+
| limites | double | YES  |     | NULL    |       |
| coeffR  | double | YES  |     | NULL    |       |
| coeffN  | double | YES  |     | NULL    |       |
+---------+--------+------+-----+---------+-------+
3 rows in set (0.02 sec)

mysql> select * from impots;
+---------+--------+---------+
| limites | coeffR | coeffN  |
+---------+--------+---------+
|   12620 |      0 |       0 |
|   13190 |   0.05 |     631 |
|   15640 |    0.1 |  1290.5 |
|   24740 |   0.15 |  2072.5 |
|   31810 |    0.2 |  3309.5 |
|   39970 |   0.25 |    4900 |
|   48360 |    0.3 |    6898 |
|   55790 |   0.35 |  9316.5 |
|   92970 |    0.4 |   12106 |
|  127860 |   0.45 |   16754 |
|  151250 |    0.5 | 23147.5 |
|  172040 |   0.55 |   30710 |
|  195000 |    0.6 |   39312 |
|       0 |   0.65 |   49062 |
+---------+--------+---------+
14 rows in set (0.00 sec)

mysql>quit

يتم تحويل قاعدة البيانات dbimpots إلى مصدر البيانات ODBC بالطريقة التالية:

  • يتم تشغيل مدير مصادر البيانات ODBC 32 بت

Image

  • استخدام الزر [Add] لإضافة مصدر بيانات جديد ODBC

Image

  • نحدد برنامج التشغيل MySQL ونقوم بـ [Terminer]

54321

Image

  • يطلب برنامج التشغيل MySQL عددًا معينًا من المعلومات:
1
الاسم DSN الذي سيُطلق على مصدر البيانات ODBC - قد يكون أي اسم
2
الجهاز الذي يتم عليه تشغيل SGBD وMySQL - وهنا localhost. ومن الجدير بالذكر أن قاعدة البيانات قد تكون قاعدة بيانات بعيدة. ولن تلاحظ التطبيقات المحلية التي تستخدم مصدر البيانات ODBC ذلك. وينطبق هذا بشكل خاص على تطبيق Java الخاص بنا.
3
قاعدة البيانات MySQL المطلوب استخدامها. MySQL هي قاعدة بيانات من نوع SGBD تدير قواعد البيانات العلائقية، وهي مجموعات من الجداول المرتبطة ببعضها البعض عبر علاقات. هنا، نحدد اسم قاعدة البيانات التي تتم إدارتها.
4
اسم مستخدم لديه حق الوصول إلى هذه القاعدة
5
كلمة مروره

تم تعريف فئتين لحساب الضريبة: impots و impotsJDBC. يتم إنشاء مثيل للفئة impots باستخدام بيانات شرائح الضرائب التي تم تمريرها كمعلمات في مصفوفات:

// إنشاء فئة الضرائب

public class impots{

  // البيانات اللازمة لحساب الضريبة
    // تأتي من مصدر خارجي

  protected double[] limites=null;
  protected double[] coeffR=null;
  protected double[] coeffN=null;

    // منشئ فارغ
  protected impots(){}

    // منشئ
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
        // يتم التحقق من أن الجداول الثلاثة لها نفس الحجم
    boolean OK=LIMITES.length==COEFFR.length && LIMITES.length==COEFFN.length;
    if (! OK) throw new Exception ("Les 3 tableaux fournis n'ont pas la même taille("+
      LIMITES.length+","+COEFFR.length+","+COEFFN.length+")");
    // كل شيء على ما يرام
    this.limites=LIMITES;
    this.coeffR=COEFFR;
    this.coeffN=COEFFN;
  }//المُصنِّع

    // حساب الضريبة
  public long calculer(boolean marié, int nbEnfants, int salaire){
        // حساب عدد الحصص
    double nbParts;
    if (marié) nbParts=(double)nbEnfants/2+2;
    else nbParts=(double)nbEnfants/2+1;
    if (nbEnfants>=3) nbParts+=0.5;
                // حساب الدخل الخاضع للضريبة و«الناتج العائلي»
        double revenu=0.72*salaire;
        double QF=revenu/nbParts;
                // حساب الضريبة
        limites[limites.length-1]=QF+1;
        int i=0;
        while(QF>limites[i]) i++;
        // إرجاع النتيجة
        return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
    }//الحساب
}//التصنيف

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

// الحزم المستوردة
import java.sql.*;
import java.util.*;

public class impotsJDBC extends impots{
  // إضافة مُنشئ يسمح بالإنشاء
    // الجداول limits و coeffr و coeffn انطلاقًا من الجدول
    // الضرائب من قاعدة بيانات
  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

    // dsnIMPOTS: اسم قاعدة البيانات DSN
        // userIMPOTS، mdpIMPOTS: اسم المستخدم/كلمة المرور للوصول إلى قاعدة البيانات

        // جداول البيانات
    ArrayList aLimites=new ArrayList();
    ArrayList aCoeffR=new ArrayList();
    ArrayList aCoeffN=new ArrayList();

    // الاتصال بقاعدة البيانات
    Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
    Connection connect=DriverManager.getConnection("jdbc:odbc:"+dsnIMPOTS,userIMPOTS,mdpIMPOTS);
    // إنشاء كائن Statement
    Statement S=connect.createStatement();
    // استعلام SELECT
    String select="select limites, coeffr, coeffn from impots";
    // تنفيذ الاستعلام
    ResultSet RS=S.executeQuery(select);
    while(RS.next()){
      // معالجة السطر الحالي
      aLimites.add(RS.getString("limites"));
      aCoeffR.add(RS.getString("coeffr"));
      aCoeffN.add(RS.getString("coeffn"));
    }// السطر التالي
        // إغلاق الموارد
    RS.close();
    S.close();
    connect.close();
        // نقل البيانات إلى مصفوفات محددة
    int n=aLimites.size();
    limites=new double[n];
    coeffR=new double[n];
    coeffN=new double[n];
    for(int i=0;i<n;i++){
      limites[i]=Double.parseDouble((String)aLimites.get(i));
      coeffR[i]=Double.parseDouble((String)aCoeffR.get(i));
      coeffN[i]=Double.parseDouble((String)aCoeffN.get(i));
    }//for
  }//منشئ
}//فئة

بمجرد إنشاء مثيل للفئة impotsJDBC، يمكن استدعاء أسلوبها calculer بشكل متكرر لحساب الضريبة:

  public long calculer(boolean marié, int nbEnfants, int salaire){

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

5.2. الإصدار 1

نضع أنفسنا في سياق تطبيق ويب يعرض واجهة HTML للمستخدم من أجل الحصول على المعلمات الثلاثة اللازمة لحساب الضريبة:

  • الحالة الاجتماعية (متزوج أم لا)
  • عدد الأبناء
  • الراتب السنوي

Image

يتم عرض النموذج من خلال الصفحة التالية JSP:

<%@ page import="java.util.*" %>

<%
        // يتم استرداد السمات التي تم تمريرها من قبل السيرفلت الرئيسي
  String chkoui=(String)request.getAttribute("chkoui");
  String chknon=(String)request.getAttribute("chknon");
  String txtEnfants=(String)request.getAttribute("txtEnfants");
  String txtSalaire=(String)request.getAttribute("txtSalaire");
  String txtImpots=(String)request.getAttribute("txtImpots");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
%>

<html>

    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
                    // مسح محتويات النموذج
        with(document.frmImpots){
            optMarie[0].checked=false;
          optMarie[1].checked=true;
          txtEnfants.value="";
          txtSalaire.value="";
          txtImpots.value="";
        }//مع
      }//مسح
        </script>
  </head>

  <body background="/impots/images/standard.jpg">
      <center>
        Calcul d'impôts
        <hr>
      <form name="frmImpots" action="/impots/main" method="POST">
          <table>
            <tr>
              <td>Etes-vous marié(e)</td>
            <td>
                    <input type="radio" name="optMarie" value="oui" <%= chkoui %>>oui
              <input type="radio" name="optMarie" value="non" <%= chknon %>>non
            </td>
          </tr>
          <tr>
              <td>Nombre d'enfants</td>
            <td><input type="text" size="5" name="txtEnfants" value="<%= txtEnfants %>"></td>
          </tr>
          <tr>
              <td>Salaire annuel</td>
            <td><input type="text" size="10" name="txtSalaire" value="<%= txtSalaire %>"></td>
          </tr>
          <tr>
              <td><font color="green">Impôt</font></td>
            <td><input type="text" size="10" name="txtImpots" value="<%= txtImpots %>" readonly></td>
          </tr>
          <tr></tr>
          <tr>
              <td><input type="submit" value="Calculer"></td>
            <td><input type="button" value="Effacer" onclick="effacer()"></td>
          </tr>
        </table>
      </form>
    </center>
    <%
                // هل توجد أخطاء
      if(erreurs!=null){
                    // عرض الأخطاء
        out.println("<hr>");
        out.println("<font color=\"red\">");
        out.println("Les erreurs suivantes se sont produites<br>");
        out.println("<ul>");
        for(int i=0;i<erreurs.size();i++){
            out.println("<li>"+(String)erreurs.get(i));
        }
        out.println("</ul>");
        out.println("</font>");
      } 
   %>
 </body>
</html>

تكتفي الصفحة JSP بعرض المعلومات التي يتم تمريرها إليها من خلال السيرفلت الرئيسي للتطبيق:

        // استرداد السمات التي تم تمريرها من خلال السيرفلت الرئيسي
  String chkoui=(String)request.getAttribute("chkoui");
  String chknon=(String)request.getAttribute("chknon");
  String txtEnfants=(String)request.getAttribute("txtEnfants");
  String txtSalaire=(String)request.getAttribute("txtSalaire");
  String txtImpots=(String)request.getAttribute("txtImpots");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
chkoui, chknon
سمات أزرار الاختيار «نعم» و«لا» - قيمها المحتملة هي "checked" أو "" لتفعيل أو عدم تفعيل زر الاختيار المقابل
txtEnfants
عدد أبناء المكلف
txtSalaire
راتبه السنوي
txtImpots
مبلغ الضريبة المستحقة
erreurs
قائمة بالأخطاء (إن وجدت) إذا كانت الأخطاء!=null

تحتوي الصفحة المرسلة إلى العميل على برنامج جافا سكريبت يحتوي على دالة effacer مرتبطة بالزر "Effacer" الذي يعمل على إعادة النموذج إلى حالته الأولية: الزر غير محدد، وحقول الإدخال فارغة. تجدر الإشارة إلى أنه لم يكن من الممكن الحصول على هذه النتيجة باستخدام زر HTML من النوع "reset". ففي الواقع، عند استخدام هذا النوع من الأزرار، يعيد المتصفح النموذج إلى الحالة التي استلمه بها. لكن في تطبيقنا، يتلقى المتصفح نماذج قد تكون غير فارغة.

يُسمى السيرفلت الرئيسي للتطبيق main.java، ورمزه هو كما يلي:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.regex.*;
import java.util.*;

public class main extends HttpServlet{

    // متغيرات المثيل
    String msgErreur=null;
    String urlAffichageImpots=null;
    String urlErreur=null;
    String DSNimpots=null;
    String admimpots=null;
    String mdpimpots=null;
    impotsJDBC impots=null;

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

        // هل تمت عملية التهيئة بنجاح؟
        if(msgErreur!=null){
                        // يتم التوجيه إلى صفحة الخطأ
            request.setAttribute("msgErreur",msgErreur);
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

                // سمات الطلب
        String chkoui=null;
        String chknon=null;
        String txtImpots=null;

        // يتم استرداد معلمات الطلب
        String optMarie=request.getParameter("optMarie");              // الحالة الاجتماعية
        String txtEnfants=request.getParameter("txtEnfants");       // عدد الأبناء
        if(txtEnfants==null) txtEnfants="";
        String txtSalaire=request.getParameter("txtSalaire");       // الراتب السنوي
        if(txtSalaire==null) txtSalaire="";

                // هل تتوفر جميع المعلمات المتوقعة
        if(optMarie==null || txtEnfants==null || txtSalaire==null){
                        // هناك معلمات مفقودة
            request.setAttribute("chkoui","");
            request.setAttribute("chknon","checked");
            request.setAttribute("txtEnfants","");
            request.setAttribute("txtSalaire","");
            request.setAttribute("txtImpots","");
                        // نقوم بتحويل المعلمة إلى عنوان URL الخاص بعرض الضريبة
            getServletContext().getRequestDispatcher(urlAffichageImpots).forward(request,response);
        }

                // تتوفر جميع المعلمات - يتم التحقق منها
        ArrayList erreurs=new ArrayList();
                // الحالة الاجتماعية
        if( ! optMarie.equals("oui") && ! optMarie.equals("non")){
                        // خطأ
            erreurs.add("Etat marital incorrect");
            optMarie="non";
        }
                // عدد الأطفال
        txtEnfants=txtEnfants.trim();
        if(! Pattern.matches("^\\d+$",txtEnfants)){
            // خطأ
            erreurs.add("Nombre d'enfants incorrect");
        }
                // الراتب
        txtSalaire=txtSalaire.trim();
        if(! Pattern.matches("^\\d+$",txtSalaire)){
            // خطأ
            erreurs.add("Salaire incorrect");
        }

                // في حالة وجود أخطاء، يتم تمريرها كسمة للاستعلام
        if(erreurs.size()!=0){
            request.setAttribute("erreurs",erreurs);
            txtImpots="";
        }else{
                        // يمكن حساب الضريبة المستحقة
            try{
                int nbEnfants=Integer.parseInt(txtEnfants);
                int salaire=Integer.parseInt(txtSalaire);
                txtImpots=""+impots.calculer(optMarie.equals("oui"),nbEnfants,salaire);
            }catch(Exception ex){}
        }

                // السمات الأخرى للاستعلام
        if(optMarie.equals("oui")){
            request.setAttribute("chkoui","checked");
            request.setAttribute("chknon","");
        }else{
            request.setAttribute("chknon","checked");
            request.setAttribute("chkoui","");
        }
        request.setAttribute("txtEnfants",txtEnfants);
        request.setAttribute("txtSalaire",txtSalaire);
        request.setAttribute("txtImpots",txtImpots);

                // نقوم بتحويل السيطرة إلى عنوان URL الخاص بعرض الضريبة
        getServletContext().getRequestDispatcher(urlAffichageImpots).forward(request,response);
    }

        //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

        //-------- INIT
    public void init(){
                // يتم استرداد معلمات التهيئة
        ServletConfig config=getServletConfig();
        urlAffichageImpots=config.getInitParameter("urlAffichageImpots");
        urlErreur=config.getInitParameter("urlErreur");
        DSNimpots=config.getInitParameter("DSNimpots");
        admimpots=config.getInitParameter("admimpots");
        mdpimpots=config.getInitParameter("mdpimpots");

                // هل المعلمات صحيحة؟
        if(urlAffichageImpots==null || DSNimpots==null || admimpots==null || mdpimpots==null){
            msgErreur="Configuration incorrecte";
            return;
        }

                // يتم إنشاء مثيل لـ impotsJDBC
        try{
            impots=new impotsJDBC(DSNimpots,admimpots,mdpimpots);
        }catch(Exception ex){
            msgErreur=ex.getMessage();
        }
    }
}
  • تقوم طريقة init الخاصة بالسيرفلت بأمرين:
    • تسترد معلمات التهيئة الخاصة بها. وتسمح لها هذه المعلمات بالاتصال بقاعدة البيانات ODBC التي تحتوي على بيانات الشرائح الضريبية المختلفة (DSNimpots، admimpots، mdpimpots) وURL للصفحات المرتبطة بالتطبيق: urlAffichageImpots للنموذج، وurlErreur لصفحة الخطأ.
    • تقوم بإنشاء مثيل للفئة impotsJDBC

في كلتا الحالتين، يتم التعامل مع الأخطاء المحتملة ويتم تخزين رسالة الخطأ في المتغير msgErreur.

  • تقوم الطريقة doGET
    • تتحقق أولاً من أن السيرفلت قد تم تهيئته بشكل صحيح. إذا لم يكن الأمر كذلك، يتم عرض صفحة الخطأ
    • تسترد المعلمات المتوقعة من نموذج الضرائب: optMarie، txtEnfants، txtSalaire. وإذا كان أحدها مفقودًا (==null)، يتم إرسال نموذج ضرائب فارغ. قد يظن المرء أن التحقق من صحة المعلمة optMarie غير ضروري. فهذه المعلمة تمثل قيمة زر اختيار (radio button) ولا يمكن أن تأخذ هنا سوى إحدى القيمتين «oui» و«non». لكن هذا يغفل حقيقة أنه لا يوجد ما يمنع أي برنامج من استعلام السيرفلت مباشرةً عن طريق إرسال المعلمات التي يريدها. لا يمكن أبدًا التأكد من أنك متصل فعليًّا بمتصفح. قد يؤدي تجاهل هذه النقطة إلى ثغرات أمنية في التطبيق، ومن الشائع بالفعل العثور على مثل هذه الثغرات حتى في التطبيقات التجارية.
    • يتم التحقق من صحة كل معلمة من المعلمات الثلاثة التي تم استردادها. وتُضاف كل خطأ يتم العثور عليه إلى قائمة الأخطاء (ArrayList أخطاء). إذا لم تكن هناك أخطاء، يتم حساب مبلغ الضريبة، وإلا فلا يتم حسابه.
    • تُوضع المعلومات اللازمة لعرض الصفحة في سمات الطلب، ثم يتم عرض نموذج الضرائب

صفحة الخطأ JSP هي كما يلي:

<%
        // jspService
    // حدث خطأ
  String msgErreur= (String)request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- بداية الصفحة HTML -->
<html>
  <head>
      <title>impots</title>
  </head>
  <body>
      <h3>calcul d'impots</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

يُسمى تطبيق الويب «impots» ويتم تكوينه في ملف server.xml في Tomcat على النحو التالي:

                <Context path="/impots" docBase="e:/data/serge/servlets/impots" />

يحتوي مجلد التطبيق على المجلدات والملفات التالية:

Image

Image

Image

Image

ملف التكوين web.xml لتطبيق impots هو كما يلي:

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

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

<web-app>
    <servlet>
      <servlet-name>main</servlet-name>
    <servlet-class>main</servlet-class>
    <init-param>
          <param-name>urlAffichageImpots</param-name>
        <param-value>/impots.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>DSNimpots</param-name>
        <param-value>mysql-dbimpots</param-value>
    </init-param>
    <init-param>
          <param-name>admimpots</param-name>
        <param-value>admimpots</param-value>
    </init-param>
    <init-param>
          <param-name>mdpimpots</param-name>
        <param-value>mdpimpots</param-value>
    </init-param>    
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreur.jsp</param-value>
    </init-param>    
  </servlet>
  <servlet-mapping>
      <servlet-name>main</servlet-name>
    <url-pattern>/main</url-pattern>
  </servlet-mapping>
</web-app>

يُسمى السيرفلت الرئيسي main، وله الاسم المستعار /main. وبالتالي، يمكن الوصول إليه عبر URL وhttp://localhost:8080/impots/main.

فيما يلي بعض أمثلة الاستخدام:

لكي يتم تهيئة السيرفلت بشكل صحيح، يجب أن يكون لديه حق الوصول إلى قاعدة البيانات mysql-dbimpots. فيما يلي الصفحة التي تظهر إذا لم يتم تشغيل الخادم MySQL على سبيل المثال، وبالتالي تعذر الوصول إلى قاعدة البيانات mysql-dbimpots:

Image

الصفحة التي تظهر في حالة إدخال بيانات غير صحيحة هي التالية:

Image

إذا كانت البيانات المدخلة صحيحة، يتم حساب الضريبة:

Image

5.3. الإصدار 2

في المثال السابق، يتم التحقق من صحة المعلمات txtEnfants و txtSalaire الخاصة بالنموذج بواسطة الخادم. ونقترح هنا التحقق منها بواسطة برنامج نصي جافا سكريبت مضمن في صفحة النموذج. وبذلك يتولى المتصفح مهمة التحقق من صحة المعلمات. ولا يتم اللجوء إلى الخادم إلا إذا كانت هذه المعلمات صالحة. وبذلك يتم توفير «عرض النطاق الترددي». وتصبح صفحة العرض JSP كما يلي:

<%@ page import="java.util.*" %>
............

<html>

    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
.......
      }//مسح

      function calculer(){
                    // التحقق من المعلمات قبل إرسالها إلى الخادم
        with(document.frmImpots){
          //عدد العناصر الفرعية
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // لم يتم التحقق من النموذج
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            nbEnfants.focus();
            return;
          }//if
          //الراتب
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // لم يتم التحقق من النموذج
            alert("Le salaire n'a pas été donné ou est incorrect");
            salaire.focus();
            return;
          }//إذا
          // حسناً - يتم إرسال النموذج إلى الخادم
          submit();
        }//مع
      }//حساب        
        </script>
  </head>

  <body background="/impots/images/standard.jpg">
........
              <td><input type="button" value="Calculer" onclick="calculer()"></td>
          <td><input type="button" value="Effacer" onclick="effacer()"></td>
.....
 </body>
</html>

تجدر الإشارة إلى التغييرات التالية:

  • لم يعد الزر Calculer من النوع submit بل أصبح من النوع button المرتبط بوظيفة تسمى calculer. وهذه الوظيفة هي التي ستقوم بتحليل الحقول txtEnfants و txTsalaire. إذا كانت صحيحة، فسيتم إرسال قيم النموذج إلى الخادم (submit)، وإلا فسيتم عرض رسالة خطأ.

فيما يلي مثال على الشاشة التي تظهر في حالة حدوث خطأ:

Image

5.4. الإصدار 3

نقوم بإجراء تعديل طفيف على التطبيق لإدخال مفهوم الجلسة. نعتبر الآن أن التطبيق هو تطبيق لمحاكاة حساب الضرائب. يمكن للمستخدم عندئذٍ محاكاة «تكوينات» مختلفة للمكلف بالضريبة ومعرفة مقدار الضريبة المستحقة الدفع لكل منها. تقدم الصفحة WEb أدناه مثالاً على ما يمكن الحصول عليه:

Image

تم تعديل السيرفلت الرئيسي وأصبح يُسمى الآن simulations. وقد تم تكوينه على النحو التالي داخل تطبيق impots:

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

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

<web-app>
    ....
</servlet>
    <servlet>
      <servlet-name>simulations</servlet-name>
    <servlet-class>simulations</servlet-class>
    <init-param>
          <param-name>urlSimulationImpots</param-name>
        <param-value>/simulationsImpots.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>DSNimpots</param-name>
        <param-value>mysql-dbimpots</param-value>
    </init-param>
    <init-param>
          <param-name>admimpots</param-name>
        <param-value>admimpots</param-value>
    </init-param>
    <init-param>
          <param-name>mdpimpots</param-name>
        <param-value>mdpimpots</param-value>
    </init-param>    
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreur.jsp</param-value>
    </init-param>    
  </servlet>
......
  <servlet-mapping>
      <servlet-name>simulations</servlet-name>
    <url-pattern>/simulations</url-pattern>
  </servlet-mapping>
</web-app>

تسمى الخدمة الرئيسية simulations وتستند إلى ملف الفئة simulations.class. ولها الاسم المستعار /simulations الذي يجعلها قابلة للوصول عبر URL وhttp://localhost:8080/impots/simulations. ولديها نفس معلمات التهيئة التي تمت دراستها سابقًا في السيرفلت الرئيسي فيما يتعلق بالوصول إلى قاعدة البيانات. تظهر معلمة جديدة، وهي urlSimulationsImpots، وهي تعادل URL الخاصة بصفحة المحاكاة JSP (التي تم عرضها أعلاه).

تشبه السيرفلت simulations.java السيرفلت main.java. وتختلف عنها في النقاط الرئيسية التالية:

  • تقوم السيرفلت الرئيسية بحساب قيمة txtImpots استنادًا إلى المعلمات optmarie، txtEnfants و txtSalaire، وتمرر هذه القيمة إلى صفحة العرض JSP
  • تحسب خدمة «simulations» بنفس الطريقة القيمة txtImpots، وتسجل المعلمات (optMarie، txtEnfants، txtsalaire، txtImpots) في قائمة تسمى simulations. وهذه القائمة هي التي يتم تمريرها كمعلمة إلى صفحة العرض JSP. ولكي تحتوي هذه القائمة على جميع عمليات المحاكاة التي أجراها المستخدم، يتم تسجيلها كسمة للجلسة الحالية.

فيما يلي نص برنامج الخدمة «simulations» (لم يتم الاحتفاظ سوى بأسطر الكود التي تختلف عن تلك الموجودة في التطبيق السابق):

import java.io.*;
.......

public class simulations extends HttpServlet{

    // متغيرات المثيل
    String msgErreur=null;
    String urlSimulationImpots=null;
    String urlErreur=null;
...........

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

...........

                // يتم استرداد عمليات المحاكاة السابقة من الجلسة
        HttpSession session=request.getSession();
        ArrayList simulations=(ArrayList)session.getAttribute("simulations");
        if(simulations==null) simulations=new ArrayList();
        // إدراج عمليات المحاكاة في الاستعلام الحالي
        request.setAttribute("simulations",simulations);

                // سمات أخرى للاستعلام
...........

                // هل لدينا جميع المعلمات المتوقعة
        if(optMarie==null || txtEnfants==null || txtSalaire==null){
........
                        // ننتقل إلى عنوان URL الخاص بعرض محاكاة حسابات الضرائب
            getServletContext().getRequestDispatcher(urlSimulationImpots).forward(request,response);
        }

                // تتوفر جميع المعلمات - يتم التحقق منها
...........

                // في حالة وجود أخطاء، يتم تمريرها إلى سمات الطلب
        if(erreurs.size()!=0){
            request.setAttribute("erreurs",erreurs);
        }else{
            try{
                                // يمكن حساب الضريبة المستحقة
                int nbEnfants=Integer.parseInt(txtEnfants);
                int salaire=Integer.parseInt(txtSalaire);
                txtImpots=""+impots.calculer(optMarie.equals("oui"),nbEnfants,salaire);
                                // نضيف النتيجة الحالية إلى عمليات المحاكاة السابقة
                String[] simulation={optMarie.equals("oui") ? "oui" : "non",txtEnfants, txtSalaire, txtImpots};
                simulations.add(simulation);
                                // يتم إعادة إدخال القيمة الجديدة للمحاكاة في الجلسة
                session.setAttribute("simulations",simulations);
            }catch(Exception ex){}
        }

                // سمات أخرى للاستعلام
..........

                // يتم تمرير السيطرة إلى عنوان URL الخاص بعرض عمليات المحاكاة
        getServletContext().getRequestDispatcher(urlSimulationImpots).forward(request,response);
    }

        //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

        //-------- INIT
    public void init(){
                // يتم استرداد معلمات التهيئة
        ServletConfig config=getServletConfig();
        urlSimulationImpots=config.getInitParameter("urlSimulationImpots");
        urlErreur=config.getInitParameter("urlErreur");
        DSNimpots=config.getInitParameter("DSNimpots");
        admimpots=config.getInitParameter("admimpots");
        mdpimpots=config.getInitParameter("mdpimpots");

                // هل المعلمات صحيحة؟
.........................
    }
}

أصبحت صفحة العرض JSP لصفحة العرض simulationsImpots.jsp كما يلي (لم يتم الاحتفاظ إلا بالرمز الذي يختلف عن صفحة العرض JSP للتطبيق السابق).

<%@ page import="java.util.*" %>

<%
        // يتم استرداد السمات التي تم تمريرها من قبل السيرفلت الرئيسي
...........
  ArrayList simulations=(ArrayList)request.getAttribute("simulations");  
%>

<html>

    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
........
        </script>
  </head>

  <body background="/impots/images/standard.jpg">
      <center>
        Calcul d'impôts
        <hr>
      <form name="frmImpots" action="/impots/simulations" method="POST">
.......................
           </form> 
</center>
    <hr>
    <%
                // هل توجد أخطاء؟
      if(erreurs!=null){
..................
      }else if(simulations.size()!=0){
          // نتائج عمليات المحاكاة
        out.println("<h3>Résultats des simulations<h3>");
        out.println("<table \"border=\"1\">");
        out.println("<tr><td>Marié</td><td>Enfants</td><td>Salaire annuel (F)</td><td>Impôts à payer (F)</td></tr>");
        for(int i=0;i<simulations.size();i++){
            String[] simulation=(String[])simulations.get(i);
            out.println("<tr><td>"+simulation[0]+"</td><td>"+simulation[1]+"</td><td>"+simulation[2]+"</td><td>"+simulation[3]+"</td></tr>");
        }
        out.println("</table>");
      }
   %>
 </body>
</html>

5.5. الإصدار 4

سنقوم الآن بإنشاء تطبيق مستقل سيكون عميلاً ويبياً للتطبيق السابق /impots/simulations. سيكون لهذا التطبيق واجهة المستخدم الرسومية التالية:

رقم
النوع
الاسم
الدور
1
JTextField
txtUrlServiceImpots
URL الخاص بخدمة محاكاة حساب الضرائب
2
JRadioButton
rdOui
ضع علامة إذا كنت متزوجًا
3
JRadioButton
rdNon
ضع علامة إذا كنت غير متزوج
4
JSpinner
spinEnfants
عدد أطفال المكلف (الحد الأدنى = 0، الحد الأقصى = 20، الزيادة = 1)
5
JTextField
txtSalaire
الراتب السنوي للمكلف بالضريبة بالفرنك الفرنسي
6
JList في JScrollPane
lstSimulations
قائمة عمليات المحاكاة

تتألف قائمة «الضرائب» من الخيارات التالية:

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

قواعد التشغيل

  • يظل خيار القائمة Calculer معطلاً إذا كان أحد الحقول 1 أو 5 فارغًا
  • تم الكشف عن وجود URL غير صحيح من الناحية النحوية في الحقل 1

Image

  • تم الكشف عن راتب غير صحيح

Image

  • يتم الإبلاغ عن أي خطأ في الاتصال بالخادم (في المثال 1 أدناه، المنفذ غير صحيح - في المثال 2، لا توجد القاعدة المطلوبة - في المثال 3، لم تكن قاعدة البيانات قيد التشغيل)

Image

Image

Image

  • إذا كان كل شيء صحيحًا، فسيتم عرض عمليات المحاكاة

Image

عند كتابة عميل ويب مبرمج، من الضروري معرفة ما يرسله الخادم بالضبط استجابةً للطلبات المختلفة المحتملة من العميل. يرسل الخادم مجموعة من الأسطر HTML تتضمن معلومات مفيدة وأخرى موجودة فقط من أجل التنسيق HTML. يمكن أن تساعدنا التعبيرات النمطية في لغة Java في العثور على المعلومات المفيدة ضمن تدفق الأسطر التي يرسلها الخادم. وللقيام بذلك، نحتاج إلى معرفة التنسيق الدقيق للردود المختلفة الصادرة عن الخادم. سنستخدم هنا عميل الويب الذي سبق أن تناولناه، والذي يتيح عرض رد الخادم على طلب URL على الشاشة. سيكون URL المطلوب هو خدمة محاكاة حسابات الضرائب الخاصة بنا http://localhost:8080/impots/simulations، والتي يمكننا تمرير المعلمات إليها بالصيغة http://localhost:8080/impots/simulations?param1=vam1&param2=val2&...

لنطلب URL في حين أن قاعدة MySQL التي تسمح بإنشاء كائن من النوع impots لم يتم تشغيلها:


Dos>java clientweb http://localhost:8080/impots/simulations GET

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Fri, 16 Aug 2002 16:31:04 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=9DEC8B27966A1FBE3D4968A7B9DF3331;Path=/impots


<!-- dÚbut من الصفحة HTML -->
<html>
  <head>
        <title>impots</title>
  </head>
  <body>
        <h3>calcul d'impôts</h3>
        <hr>
    Application indisponible([TCX][MyODBC]Can't connect to MySQL server on 'localhost' (10061))
  </body>
</html>

لاسترداد الخطأ، سيتعين على عميل الويب البحث في استجابة خادم الويب عن السطر الذي يحتوي على النص "التطبيق غير متاح". والآن، دعونا نطلق قاعدة البيانات MySQL ونطلب نفس القاعدة URL مع تمرير القيم التي تتوقعها (تتوقعها دون تمييز سواء في شكل GET أو POST - انظر كود جافا الخاص بالخادم):


Dos>java clientweb "http://localhost:8080/impots/simulations?ptMarie=oui&txtEnfants=2&txtSalaire=200000" GET

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Fri, 16 Aug 2002 16:42:36 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=C2A707600E98A37A343611D80DD5C8A2;Path=/impots


<html>

        <head>
        <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
...................................................
      }//مسح

      function calculer(){
...................................................
      }//حساب
                </script>
  </head>

  <body background="/impots/images/standard.jpg">
        <center>
        Calcul d'imp¶ts
        <hr>
      <form name="frmImpots" action="/impots/simulations" method="POST">
...................................................
      </form>
    </center>
    <hr>
    <h3>Résultats des simulations<h3>
<table "border="1">
<tr><td>Marié</td><td>Enfants</td><td>Salaire annuel (F)</td><td>Impôts à payer (F)</td></tr>
<tr><td>oui</td><td>2</td><td>200000</td><td>22504</td></tr>
</table>

 </body>
</html>

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

"<tr>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*</tr>"

حيث تمثل التعبيرات الأربعة الموضوعة بين أقواس المعلومات الأربع المطلوب استرجاعها.

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

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

الرمز المرتبط بالقائمة calculer هو التالي:

  void mnuCalculer_actionPerformed(ActionEvent e) {
    // حساب الضريبة
                // التحقق URL الخدمة
        URL urlImpots=null;
        try{
            urlImpots=new URL(txtURLServiceImpots.getText().trim());
            String query=urlImpots.getQuery();
            if(query!=null) throw new Exception();
        }catch (Exception ex){
                        // رسالة خطأ
            JOptionPane.showMessageDialog(this,"URL incorrecte. Recommencez","Erreur",JOptionPane.ERROR_MESSAGE);
                        // التركيز على الحقل الخاطئ
            txtURLServiceImpots.requestFocus();
                        // العودة إلى الواجهة
            return;
        }
        // التحقق من الراتب
    int salaire=0;
    try{
      salaire=Integer.parseInt(txtSalaire.getText().trim());
      if(salaire<0) throw new Exception();
    }catch (Exception ex){
            // رسالة خطأ
            JOptionPane.showMessageDialog(this,"Salaire incorrect. Recommencez","Erreur",JOptionPane.ERROR_MESSAGE);
            // التركيز على الحقل الذي يحتوي على خطأ
      txtSalaire.requestFocus();
            // العودة إلى الواجهة
      return;
    }
        // عدد الأطفال
    Integer nbEnfants=(Integer)spinEnfants.getValue();

    try{
                        // يتم حساب الضريبة
            calculerImpots(urlImpots,rdOui.isSelected(),nbEnfants.intValue(),salaire);
    }catch (Exception ex){
                        // يتم عرض الخطأ
            JOptionPane.showMessageDialog(this,"L'erreur suivante s'est produite : " + ex.getMessage(),"Erreur",JOptionPane.ERROR_MESSAGE);
        }
  }//mnuCalculer


    public void calculerImpots(URL urlImpots,boolean marié, int nbEnfants, int salaire)
          throws Exception{
                // حساب الضريبة
                // urlImpots: URL من مصلحة الضرائب
                // متزوج: true إذا كان متزوجًا، false إذا لم يكن كذلك
                // nbEnfants: عدد الأبناء
                // الراتب: الراتب السنوي

                // يتم استخراج المعلومات اللازمة للاتصال بخادم الضرائب من urlImpots
        String path=urlImpots.getPath();
        if(path.equals("")) path="/";
        String query="?"+"optMarie="+(marié ? "oui":"non")+"&txtEnfants="+nbEnfants+"&txtSalaire="+salaire;
        String host=urlImpots.getHost();
        int port=urlImpots.getPort();
        if(port==-1) port=urlImpots.getDefaultPort();

                // البيانات المحلية
        Socket  client=null;                        // العميل
        BufferedReader IN=null;                    // تدفق القراءة الخاص بالعميل
        PrintWriter OUT=null;                        // دفق الكتابة الخاص بالعميل
        String réponse=null;                        // استجابة الخادم
                // النمط المطلوب في الرؤوس HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // نموذج الرد الصحيح
        Pattern réponseOK=Pattern.compile("^.*? 200 OK");
                // نتيجة المقارنة بالنموذج
        Matcher résultat=null;

        try{
                        // يتم الاتصال بالخادم
            client=new Socket(host,port);

            // يتم إنشاء تدفقات الإدخال والإخراج للعميل TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // يتم طلب URL - إرسال الرؤوس HTTP
            OUT.println("GET " + path + query + " HTTP/1.1");
            OUT.println("Host: " + host + ":" + port);
            if(! JSESSIONID.equals("")){
                OUT.println("Cookie: JSESSIONID="+JSESSIONID);
            }
            OUT.println("Connection: close");
            OUT.println("");

                        // يتم قراءة السطر الأول من الرد
            réponse=IN.readLine();
                        // مقارنة السطر HTTP بنموذج الرد الصحيح
            résultat=réponseOK.matcher(réponse);
            if(! résultat.find()){
                // هناك مشكلة في URL
                throw new Exception("Le serveur a répondu : URL ["+ txtURLServiceImpots.getText().trim() + "] inconnue");
            }//if(النتيجة)

                        // يتم قراءة الإجابة حتى نهاية الرؤوس بحثًا عن ملف تعريف الارتباط المحتمل
            while((réponse=IN.readLine())!=null){
                                // سطر فارغ؟
                if(réponse.equals("")) break;
                // سطر HTTP غير فارغ
                                // إذا لم يكن رمز الجلسة موجودًا، يتم البحث عنه
                if (JSESSIONID.equals("")){
                    // نقارن السطر HTTP بنموذج ملف تعريف الارتباط
                    résultat=modèleCookie.matcher(réponse);
                    if(résultat.find()){
                        // تم العثور على ملف تعريف الارتباط الخاص برمز الجلسة
                        JSESSIONID=résultat.group(1);
                    }//if(النتيجة)
                }//إذا (JSESSIONID)
            }//أثناء

                        // انتهى الأمر بالنسبة للرؤوس HTTP - ننتقل إلى الرمز HTML
                        // لاسترداد عمليات المحاكاة
            ArrayList listeSimulations=getSimulations(IN,OUT,simulations);
            simulations.clear();
            for (int i=0;i<listeSimulations.size();i++){
                simulations.addElement(listeSimulations.get(i));
            }

            // انتهى الأمر
            client.close();
        }catch (Exception ex){
            throw new Exception(ex.getMessage());
        }
    }//calculerImpots

    private ArrayList getSimulations(BufferedReader IN, PrintWriter OUT, DefaultListModel simulations) throws Exception{

        // نموذج لسطر واحد من جدول المحاكاة
        Pattern ptnSimulation=Pattern.compile("<tr>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*</tr>");
        // نموذج لسطر من قائمة الأخطاء
        Pattern ptnErreur=Pattern.compile("(Application indisponible.*?)\\s*$");
                // نتيجة المقارنة بالنموذج
        Matcher résultat=null;
                // المحاكاة
        ArrayList listeSimulations=new ArrayList();

                // يتم قراءة جميع الأسطر حتى النهاية
        String ligne=null;
        boolean simulationRéussie=false;
        while((ligne=IN.readLine())!=null){
                        // المتابعة
                        // يتم مقارنة السطر بنموذج الأخطاء إذا لم يتم الوصول إلى جزء المحاكاة بعد
            if(! simulationRéussie){
                résultat=ptnErreur.matcher(ligne);
                if(résultat.find()){
                    // رسالة خطأ
                    JOptionPane.showMessageDialog(this,résultat.group(1),"Erreur",JOptionPane.ERROR_MESSAGE);
                    // انتهى
                    return listeSimulations;
                }//إذا
            }//إذا
                        // يتم مقارنة السطر بنموذج المحاكاة
            résultat=ptnSimulation.matcher(ligne);
            if(résultat.find()){
                // تم العثور على صف في الجدول
                listeSimulations.add(résultat.group(1)+":"+résultat.group(2)+":"+résultat.group(3)+
                                                ":"+résultat.group(4));
                                // نجحت المحاكاة
                simulationRéussie=true;
            }//if
        }//بينما
        // نهاية
        return listeSimulations;
    }

دعونا نوضح هذا الرمز قليلاً:

  • تتحقق الإجراء mnuCalculer_actionPerformed من صحة بيانات الواجهة. إذا لم تكن صحيحة، يتم إصدار رسالة خطأ ويتم إنهاء الإجراء. إذا كانت صحيحة، يتم تنفيذ الإجراء calculerImpots.
  • يبدأ الإجراء calculerImpots بإنشاء الإجراء URL الذي يجب أن يطلبه
                // يتم استخراج المعلومات اللازمة للاتصال بخادم الضرائب من urlImpots
        String path=urlImpots.getPath();
        if(path.equals("")) path="/";
        String query="?"+"optMarie="+(marié ? "oui":"non")+"&txtEnfants="+nbEnfants+"&txtSalaire="+salaire;
        String host=urlImpots.getHost();
        int port=urlImpots.getPort();
        if(port==-1) port=urlImpots.getDefaultPort();
........................
  • ثم يتصل بـ URL هذا عن طريق إرسال الرؤوس المناسبة HTTP:
                        // يتم الاتصال بالخادم
            client=new Socket(host,port);

            // يتم إنشاء تدفقات الدخول والخروج للعميل TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // يتم طلب URL - إرسال الرؤوس HTTP
            OUT.println("GET " + path + query + " HTTP/1.1");
            OUT.println("Host: " + host + ":" + port);
            if(! JSESSIONID.equals("")){
                OUT.println("Cookie: JSESSIONID="+JSESSIONID);
            }
            OUT.println("Connection: close");
            OUT.println("");
  • بمجرد إرسال الطلب، ينتظر عميل الويب الخاص بنا الرد. ويحصل أولاً على رؤوس الرسالة HTTP من رد الخادم. ويقوم بتفحص هذه الرؤوس للعثور على رمز الجلسة. في الواقع، سيتعين عليه إعادة إرسال هذا الرمز إلى الخادم حتى يتمكن الأخير من تتبع المحاكاة المختلفة التي تم إجراؤها. تجدر الإشارة هنا إلى أنه كان من الأسهل أن يقوم العميل بنفسه بتخزين المحاكاة المختلفة التي أجراها المستخدم. ومع ذلك، فإننا نحتفظ بفكرة إعادة إرسال الرمز لتقديم مثال جديد على إدارة الجلسة. يتم معالجة السطر الأول من الرد بشكل منفصل. في الواقع، يجب أن يكون على الشكل HTTP/version 200 OK للإشارة إلى أن URL المطلوب موجود بالفعل. لذا، إذا لم تكن بالصيغة المذكورة، نستنتج أن المستخدم قد طلب رمز URL غير صحيح ونبلغه بذلك.
                // النمط المطلوب في الرؤوس HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // نموذج الإجابة الصحيحة
        Pattern réponseOK=Pattern.compile("^.*? 200 OK");
..........

                        // يتم قراءة السطر الأول من الإجابة
            réponse=IN.readLine();
                        // مقارنة السطر HTTP بنموذج الإجابة الصحيحة
            résultat=réponseOK.matcher(réponse);
            if(! résultat.find()){
                // هناك مشكلة في URL
                throw new Exception("Le serveur a répondu : URL ["+ txtURLServiceImpots.getText().trim() + "] inconnue");
            }//if(النتيجة)

                        // يتم قراءة الإجابة حتى نهاية الرؤوس بحثًا عن ملف تعريف الارتباط المحتمل
            while((réponse=IN.readLine())!=null){
                                // سطر فارغ؟
                if(réponse.equals("")) break;
                // سطر HTTP غير فارغ
                                // إذا لم يكن رمز الجلسة موجودًا، يتم البحث عنه
                if (JSESSIONID.equals("")){
                    // نقارن السطر HTTP بنموذج ملف تعريف الارتباط
                    résultat=modèleCookie.matcher(réponse);
                    if(résultat.find()){
                        // تم العثور على ملف تعريف الارتباط الخاص برمز الجلسة
                        JSESSIONID=résultat.group(1);
                    }//if(النتيجة)
                }//إذا (JSESSIONID)
            }//أثناء
  • بعد معالجة الرؤوس HTTP، ننتقل إلى الجزء HTML من الرد
                        // انتهى الأمر بالنسبة للرؤوس HTTP - ننتقل إلى الرمز HTML
                        // لاسترداد عمليات المحاكاة
            ArrayList listeSimulations=getSimulations(IN,OUT,simulations);
            simulations.clear();
            for (int i=0;i<listeSimulations.size();i++){
                simulations.addElement(listeSimulations.get(i));
            }
  • تُرجع الإجراء getSimulations قائمة بالمحاكاة إن وجدت، وستكون هذه القائمة فارغة إذا أرسل الخادم رسالة خطأ. في هذه الحالة، يتم عرض الرسالة في مربع رسالة. إذا كانت القائمة غير فارغة، يتم عرضها في القائمة المنسدلة للواجهة الرسومية.
  • يقوم الإجراء getSimulations بمقارنة كل سطر من الرد HTML بالتعبير النمطي الذي يمثل رسالة الخطأ (التطبيق غير متاح...) والتعبير النمطي الذي يمثل محاكاة. في حالة العثور على رسالة الخطأ، يتم عرضها وينتهي الإجراء. وإذا تم العثور على نتيجة محاكاة، يتم إضافتها إلى قائمة المحاكاة. وفي نهاية الإجراء، يتم إرجاع هذه القائمة كنتيجة.
    private ArrayList getSimulations(BufferedReader IN, PrintWriter OUT, DefaultListModel simulations) throws Exception{

        // نموذج سطر واحد من جدول المحاكاة
        Pattern ptnSimulation=Pattern.compile("<tr>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*</tr>");
        // نموذج لسطر من قائمة الأخطاء
        Pattern ptnErreur=Pattern.compile("(Application indisponible.*?)\\s*$");

5.6. الإصدار 5

هنا، نقوم بتحويل التطبيق الرسومي المستقل السابق إلى تطبيق جافا صغير. تختلف الواجهة الرسومية قليلاً. في التطبيق المستقل، كان المستخدم يحدد بنفسه معرّف خدمة محاكاة حسابات الضرائب (URL)، ثم يتصل التطبيق بهذا المعرّف (URL). هنا، يكون متصفح الويب هو التطبيق العميل، وسيقوم المستخدم بطلب معرّف URL الخاص بالوثيقة HTML التي تحتوي على التطبيق الصغير. يجب أن نتذكر الآن أن تطبيق Java الصغير لا يمكنه فتح اتصال شبكي إلا مع الخادم الذي تم تنزيله منه. وبالتالي، فإن معرّف URL الخاص بخدمة المحاكاة سيكون موجودًا على نفس الخادم الذي يوجد عليه المستند HTML الذي يحتوي على التطبيق الصغير. في مثالنا هذا، سيكون معلمة تهيئة التطبيق الصغير هي التي ستُوضع في الحقل txtUrlServiceImpots، وهو حقل لن يكون قابلاً للتعديل من قبل المستخدم. وبذلك نحصل على العميل التالي:

Image

المستند HTML الذي يحتوي على التطبيق الصغير يُسمى simulations.htm وهو كما يلي:

<html>
    <head>
      <title>Simulations de calculs d'impôts</title>
  </head>
  <body background="/impots/images/standard.jpg">
      <center>
          <h3>Simulations de calculs d'impôts</h3>
        <hr>
        <applet code="appletImpots.class" width="400" height="360">
            <param name="urlServiceImpots" value="simulations">
            </applet>
    </center>
  </body>
</html>  

يحتوي التطبيق الصغير على معلمة urlServiceImpots وهي URL الخاصة بخدمة محاكاة حساب الضرائب. هذا المعامل URL هو معامل نسبي ويُقاس مقارنةً بمعامل URL الخاص بالوثيقة HTML simulations.htm. وبالتالي، إذا حصل متصفح على هذا المستند مع الرمز URL http://localhost:8080/impots/simulations.htm، فإن ملف URL الخاص بخدمة المحاكاة سيكون http://localhost:8080/impots/simulations.. لو كان ملف URL هو http://stahe:8080/impots/simulations.htm فإن الرمز URL الخاص بخدمة المحاكاة كان سيصبح http://stahe:8080/impots/simulations.

تستعيد التطبيق الصغير appletImpots.java بالكامل كود التطبيق الرسومي المستقل السابق مع الالتزام بقواعد تحويل التطبيق الرسومي إلى تطبيق صغير.

public class appletImpots extends JApplet {
  // مكونات النافذة
  JPanel contentPane;
  JMenuBar jMenuBar1 = new JMenuBar();
  JMenu jMenu1 = new JMenu();
  JMenuItem mnuCalculer = new JMenuItem();
.............


    //إنشاء الإطار
  public void init() {
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
        // عمليات تهيئة أخرى
    moreInit();
  }

    // تهيئة النموذج
  private void moreInit(){
                // يتم استرداد المعلمة urlServiceImpots
        String urlServiceImpots=getParameter("urlServiceImpots");
        if(urlServiceImpots==null){
            // المعلمة مفقودة
            JOptionPane.showMessageDialog(this,"Le paramètre urlServiceImpots de l'applet n'a pas été défini","Erreur",JOptionPane.ERROR_MESSAGE);
                        // النهاية
            return;
        }
                // يتم إدخال URL في الحقل المخصص له
        String codeBase=""+getCodeBase();
        if(codeBase.endsWith("/"))
           txtURLServiceImpots.setText(codeBase+urlServiceImpots);
      else txtURLServiceImpots.setText(codeBase+"/"+urlServiceImpots);
    // القائمة «حساب» معطلة
    mnuCalculer.setEnabled(false);
        // مؤشر التحديد «الأطفال» - بين 0 و20 طفلاً
    spinEnfants=new JSpinner(new SpinnerNumberModel(0,0,20,1));
    spinEnfants.setBounds(new Rectangle(130,140,50,27));
    contentPane.add(spinEnfants);
  }//moreInit

    //تهيئة المكون
  private void jbInit() throws Exception  {
    contentPane = (JPanel) this.getContentPane();
    contentPane.setLayout(null);
...............
  }

عندما يقوم متصفح بتحميل تطبيق صغير، فإنه يقوم أولاً بتنفيذ إجراء init الخاص به. وفي هذا الإجراء، قمنا باسترداد قيمة المعلمة urlServiceImpots الخاصة بالتطبيق الصغير، ثم قمنا بحساب الاسم الكامل لـ URL الخاص بخدمة المحاكاة، ووضعنا هذه القيمة في الحقل txtURLServiceImpots كما لو أن المستخدم هو من قام بكتابتها. وبذلك، لم يعد هناك أي فرق بين التطبيقين. وعلى وجه الخصوص، فإن الكود المرتبط بالقائمة Calculer متطابق. وفيما يلي مثال على التنفيذ:

Image

5.7. الخلاصة

لقد عرضنا إصدارات مختلفة من تطبيقنا العميل-الخادم لحساب الضرائب:

  • الإصدار 1: يتم توفير الخدمة من خلال مجموعة من السيرفلتات وصفحات JSP، والعميل هو متصفح. يقوم المتصفح بإجراء محاكاة واحدة فقط ولا يحتفظ بسجل للمحاكاة السابقة.
  • الإصدار 2: تمت إضافة بعض القدرات من جانب المتصفح عن طريق إدراج نصوص جافا سكريبت في المستند HTML الذي يتم تحميله بواسطة المتصفح. ويتحقق المتصفح من صحة معلمات النموذج.
  • الإصدار 3: نسمح للخدمة بتذكر المحاكاة المختلفة التي أجراها العميل من خلال إدارة جلسة عمل. يتم تعديل الواجهة HTML وفقًا لذلك لعرض هذه المحاكاة.
  • الإصدار 4: أصبح العميل الآن تطبيقًا رسوميًا مستقلًا. وهذا يسمح لنا بالعودة إلى كتابة برامج العملاء على الويب.
  • الإصدار 5: يتحول العميل إلى تطبيق Java صغير (applet). وبذلك يصبح لدينا تطبيق عميل-خادم مبرمج بالكامل بلغة Java سواء من جانب الخادم أو من جانب العميل.

في هذه المرحلة، يمكننا إبداء بعض الملاحظات:

  • تسمح الإصدارات من 1 إلى 3 باستخدام متصفحات لا تمتلك أي قدرات أخرى سوى القدرة على تنفيذ نصوص جافا سكريبت. وتجدر الإشارة إلى أن المستخدم لديه دائمًا إمكانية منع تنفيذ هذه البرامج النصية. وعندئذٍ لن يعمل التطبيق إلا جزئيًا في الإصدار 1 (لن تعمل خيار «حذف») ولن يعمل على الإطلاق في الإصدارين 2 و3 (لن تعمل خيارات «حذف» و«حساب»). وقد يكون من المفيد توفير إصدار من الخدمة لا يستخدم نصوص جافا سكريبت.
  • يتطلب الإصدار 4 أن يكون جهاز العميل مزودًا بآلة افتراضية Java 2.
  • يتطلب الإصدار 5 أن يكون لدى جهاز العميل متصفح مزود بجهاز افتراضي Java 2.

عند كتابة خدمة ويب، يجب أن نسأل أنفسنا عن أنواع العملاء التي نستهدفها. إذا أردنا استهداف أكبر عدد ممكن من العملاء، فسنكتب تطبيقًا لا يرسل إلى المتصفحات سوى HTML (بدون جافا سكريبت أو أبلت). أما إذا كنا نعمل ضمن شبكة إنترانت ونتحكم في تكوين أجهزة المستخدمين فيها، فيمكننا عندئذٍ أن نكون أكثر صرامةً فيما يتعلق بمتطلبات العميل، وقد تصبح الإصدار 5 السابق مقبولاً في هذه الحالة.

الإصداران 4 و5 هما عميلان ويب يستخرجان المعلومات التي يحتاجانها من تدفق البيانات HTML الذي يرسله الخادم. في كثير من الأحيان، لا نتمتع بالسيطرة على هذا التدفق. وهذا هو الحال عندما نكتب عميلاً لخدمة ويب موجودة على الشبكة ويديرها شخص آخر. لنأخذ مثالاً. لنفترض أن خدمة محاكاة حساب الضرائب الخاصة بنا قد تمت برمجتها بواسطة شركة X. حالياً، ترسل الخدمة المحاكاة في جدول HTML، ويستفيد عميلنا من هذه الحقيقة لاستردادها. وبذلك، يقارن كل سطر من رد الخادم بالتعبير النمطي:

                // نموذج لسطر واحد من جدول المحاكاة
        Pattern ptnSimulation=Pattern.compile("<tr>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*</tr>");

لنفترض الآن أن مطور التطبيق قام بتغيير الشكل المرئي للاستجابة، بحيث لم يعد يضع المحاكاة في مصفوفة بل في قائمة بالشكل التالي:

Résultat des simulations
<ul>
    <li>oui,2,200000,22504
  <li>non,2,200000,33388
</ul>  

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

  • بدلاً من إنشاء HTML، ستقوم خدمة المحاكاة بإنشاء XML. في مثالنا، قد يكون ذلك
<simulations>
    <entetes marie="marié" enfants="enfants" salaire="salaire" impot="impôt"/>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504" />
  <simulation marie="non" enfants="2" salaire="200000" impot="33388" />
</simulations>
  • يمكن ربط ورقة أنماط بهذا الرد لتوجيه المتصفحات إلى الشكل المرئي الذي يجب أن يتخذه هذا الرد XML
  • ستتجاهل برامج الويب هذه ورقة الأنماط وستسترد المعلومات مباشرةً من تدفق XML في الرد

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

  • الإصدار 6: تقدم الخدمة استجابة XML مصحوبة بورقة أنماط مخصصة للمتصفحات
  • الإصدار 7: العميل هو تطبيق رسومي مستقل يستخدم الرد XML الوارد من الخادم
  • الإصدار 8: العميل عبارة عن تطبيق جافا صغير يستخدم الرد XML من الخادم