Skip to content

5. L'application IMPOTS

5.1. Introduction

Nous reprenons ici l'application IMPOTS qui est utilisée à de nombreuses reprises dans le polycopié Java du même auteur. Rappelons-en la problématique. Il s'agit d'écrire une application permettant de calculer l'impôt d'un contribuable. On se place dans le cas simplifié d'un contribuable n'ayant que son seul salaire à déclarer :

  • on calcule le nombre de parts du salarié nbParts=nbEnfants/2 +1 s'il n'est pas marié, nbEnfants/2+2 s'il est marié, où nbEnfants est son nombre d'enfants.
  • s'il a au moins trois enfants, il a une demi-part de plus
  • on calcule son revenu imposable R=0.72*S où S est son salaire annuel
  • on calcule son coefficient familial QF=R/nbParts
  • on calcule son impôt I. Considérons le tableau suivant :

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

Chaque ligne a 3 champs. Pour calculer l'impôt I, on recherche la première ligne où QF<=champ1. Par exemple, si QF=23000 on trouvera la ligne

    24740       0.15        2072.5

L'impôt I est alors égal à 0.15*R - 2072.5*nbParts. Si QF est tel que la relation QF<=champ1 n'est jamais vérifiée, alors ce sont les coefficients de la dernière ligne qui sont utilisés. Ici :

    0               0.65        49062

ce qui donne l'impôt I=0.65*R - 49062*nbParts.

Les données définissant les différentes tranches d'impôt sont dans une base de données ODBC-MySQL. MySQL est un SGBD du domaine public utilisable sur différentes plate-formes dont Windows et Linux. Avec ce SGBD, une base de données dbimpots a été créée avec dedans une unique table appelée impots. L'accès à la base est contrôlé par un login/motdepasse ici admimpots/mdpimpots. La copie d'écran suivante montre comment utiliser la base dbimpots avec 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

La base de données dbimpots est transformée en source de données ODBC de la façon suivante :

  • on lance le gestionnaire des sources de données ODBC 32 bits

Image

  • on utilise le bouton [Add] pour ajouter une nouvelle source de données ODBC

Image

  • on désigne le pilote MySQL et on fait [Terminer]

Image

  • le pilote MySQL demande un certain nombre de renseignements :

1

le nom DSN à donner à la source de données ODBC - peut-être quelconque

2

la machine sur laquelle s'exécute le SGBD MySQL - ici localhost. Il est intéressant de noter que la base de données pourrait être une base de données distante. Les applications locales utilisant la source de données ODBC ne s'en apercevraient pas. Ce serait le cas notamment de notre application Java.

3

la base de données MySQL à utiliser. MySQL est un SGBD qui gère des bases de données relationnelles qui sont des ensembles de tables reliées entre-elles par des relations. Ici, on donne le nom de la base gérée.

4

le nom d'un utilisateur ayant un droit d'accès à cette base

5

son mot de passe

Deux classes ont été définies pour calculer l'impôt : impots et impotsJDBC. Une instance de la classe impots est construite avec les données des tranches d'impôts passées en paramètres dans des tableaux :

// création d'une classe impots

public class impots{

  // les données nécessaires au calcul de l'impôt
  // proviennent d'une source extérieure

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

  // constructeur vide
  protected impots(){}

  // constructeur
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
    // on vérifie que les 3 tableaux ont la même taille
    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+")");
    // c'est bon
    this.limites=LIMITES;
    this.coeffR=COEFFR;
    this.coeffN=COEFFN;
  }//constructeur

  // calcul de l'impôt
  public long calculer(boolean marié, int nbEnfants, int salaire){
    // calcul du nombre de parts
    double nbParts;
    if (marié) nbParts=(double)nbEnfants/2+2;
    else nbParts=(double)nbEnfants/2+1;
    if (nbEnfants>=3) nbParts+=0.5;
        // calcul revenu imposable & Quotient familial
        double revenu=0.72*salaire;
        double QF=revenu/nbParts;
        // calcul de l'impôt
        limites[limites.length-1]=QF+1;
        int i=0;
        while(QF>limites[i]) i++;
        // retour résultat
        return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
    }//calculer
}//classe

La classe impotsJDBC dérive de la classe impots précédente. Une instance de la classe impotsJDBC est construite avec les données des tranches d'impôts stockées dans une base de données. Les informations nécessaires pour accéder à cette base de données sont passées en paramètres au constructeur :

// paquetages importés
import java.sql.*;
import java.util.*;

public class impotsJDBC extends impots{
  // rajout d'un constructeur permettant de construire
  // les tableaux limites, coeffr, coeffn à partir de la table
  // impots d'une base de données
  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
    throws SQLException,ClassNotFoundException{

    // dsnIMPOTS : nom DSN de la base de données
    // userIMPOTS, mdpIMPOTS : login/mot de passe d'accès à la base

    // les tableaux de données
    ArrayList aLimites=new ArrayList();
    ArrayList aCoeffR=new ArrayList();
    ArrayList aCoeffN=new ArrayList();

    // connexion à la base
    Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
    Connection connect=DriverManager.getConnection("jdbc:odbc:"+dsnIMPOTS,userIMPOTS,mdpIMPOTS);
    // création d'un objet Statement
    Statement S=connect.createStatement();
    // requête select
    String select="select limites, coeffr, coeffn from impots";
    // exécution de la requête
    ResultSet RS=S.executeQuery(select);
    while(RS.next()){
      // exploitation de la ligne courante
      aLimites.add(RS.getString("limites"));
      aCoeffR.add(RS.getString("coeffr"));
      aCoeffN.add(RS.getString("coeffn"));
    }// ligne suivante
    // fermeture ressources
    RS.close();
    S.close();
    connect.close();
    // transfert des données dans des tableaux bornés
    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
  }//constructeur
}//classe

Une fois qu'une instance de la classe impotsJDBC a été construite, on peut appeler de façon répétée sa méthode calculer afin de calculer l'impôt :

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

L'acquisition des trois données nécessaires peut se faire de multiple façons. L'intérêt de la classe impotsJDBC est qu'on a juste à se préoccuper de cette acquisition. Une fois les trois information (statut marital, nombre d'enfants, salaire annuel) obtenues, l'appel à la méthode calculer de la classe impotsJDBC nous donne l'impôt à payer.

5.2. Version 1

On se place dans le contexte d'une application web qui présenterait une interface HTML à un utilisateur afin d'obtenir les trois paramètres nécessaires au calcul de l'impôt :

  • l'état marital (marié ou non)
  • le nombre d'enfants
  • le salaire annuel

Image

L'affichage du formulaire est réalisé par la page JSP suivante :

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

<%
    // on récupère les attributs passés par la servlet principale
  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(){
        // raz du formulaire
        with(document.frmImpots){
            optMarie[0].checked=false;
          optMarie[1].checked=true;
          txtEnfants.value="";
          txtSalaire.value="";
          txtImpots.value="";
        }//with
      }//effacer
        </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>
    <%
        // y-a-t-il des erreurs
      if(erreurs!=null){
        // affichage des erreurs
        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>

La page JSP se contente d'afficher des informations qui lui sont passées par la servlet principale de l'application :

    // on récupère les attributs passés par la servlet principale
  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

attributs des boutons radio oui, non - ont pour valeurs possibles "checked" ou "" afin d'allumer ou non le bouton radio correspondant


txtEnfants

le nombre d'enfants du contribuable


txtSalaire

son salaire annuel


txtImpots

le montant de l'impôt à payer


erreurs

une liste éventuelle d'erreurs si erreurs!=null

La page envoyée au client contient un script javascript contenant une fonction effacer associée au bouton "Effacer" dont le rôle est de remettre le formulaire dans son état initial : bouton non coché, champs de saisie vides. On notera que ce résultat ne pouvait pas être obtenu avec un bouton HTML de type "reset". En effet, lorsque ce type de bouton est utilisé, le navigateur remet le formulaire dans l'état où il l'a reçu. Or dans notre application, le navigateur reçoit des formulaires qui peuvent être non vides.

La servlet principale de l'application s'appelle main.java et son code est le suivant :

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

public class main extends HttpServlet{

    // variables d'instance
    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{

        // l'initialisation s'est-elle bien passée ?
        if(msgErreur!=null){
            // on passe la main à la page d'erreur
            request.setAttribute("msgErreur",msgErreur);
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

        // des attributs de la requête
        String chkoui=null;
        String chknon=null;
        String txtImpots=null;

        // on récupère les paramètres de la requête
        String optMarie=request.getParameter("optMarie");              // statut marital
        String txtEnfants=request.getParameter("txtEnfants");       // nbre d'enfants
        if(txtEnfants==null) txtEnfants="";
        String txtSalaire=request.getParameter("txtSalaire");       // salaire annuel
        if(txtSalaire==null) txtSalaire="";

        // a-t-on tous les paramètres attendus
        if(optMarie==null || txtEnfants==null || txtSalaire==null){
            // il manque des paramètres
            request.setAttribute("chkoui","");
            request.setAttribute("chknon","checked");
            request.setAttribute("txtEnfants","");
            request.setAttribute("txtSalaire","");
            request.setAttribute("txtImpots","");
            // on passe la main à l'url d'affichage de l'impôt
            getServletContext().getRequestDispatcher(urlAffichageImpots).forward(request,response);
        }

        // on a tous les paramètres - on les vérifie
        ArrayList erreurs=new ArrayList();
        // état marital
        if( ! optMarie.equals("oui") && ! optMarie.equals("non")){
            // erreur
            erreurs.add("Etat marital incorrect");
            optMarie="non";
        }
        // nombre d'enfants
        txtEnfants=txtEnfants.trim();
        if(! Pattern.matches("^\\d+$",txtEnfants)){
            // erreur
            erreurs.add("Nombre d'enfants incorrect");
        }
        // salaire
        txtSalaire=txtSalaire.trim();
        if(! Pattern.matches("^\\d+$",txtSalaire)){
            // erreur
            erreurs.add("Salaire incorrect");
        }

        // s'il y a des erreurs, on les passe en attribut de la requête
        if(erreurs.size()!=0){
            request.setAttribute("erreurs",erreurs);
            txtImpots="";
        }else{
            // on peut calculer l'impôt à payer
            try{
                int nbEnfants=Integer.parseInt(txtEnfants);
                int salaire=Integer.parseInt(txtSalaire);
                txtImpots=""+impots.calculer(optMarie.equals("oui"),nbEnfants,salaire);
            }catch(Exception ex){}
        }

        // les autres attributs de la requête
        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);

        // on passe la main à l'url d'affichage de l'impôt
        getServletContext().getRequestDispatcher(urlAffichageImpots).forward(request,response);
    }

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

    //-------- INIT
    public void init(){
        // on récupère les paramètres d'initialisation
        ServletConfig config=getServletConfig();
        urlAffichageImpots=config.getInitParameter("urlAffichageImpots");
        urlErreur=config.getInitParameter("urlErreur");
        DSNimpots=config.getInitParameter("DSNimpots");
        admimpots=config.getInitParameter("admimpots");
        mdpimpots=config.getInitParameter("mdpimpots");

        // paramètres ok ?
        if(urlAffichageImpots==null || DSNimpots==null || admimpots==null || mdpimpots==null){
            msgErreur="Configuration incorrecte";
            return;
        }

        // on crée une instance d'impotsJDBC
        try{
            impots=new impotsJDBC(DSNimpots,admimpots,mdpimpots);
        }catch(Exception ex){
            msgErreur=ex.getMessage();
        }
    }
}
  • la méthode init de la servlet fait deux choses :
    • elle récupère ses paramètres d'initialisation. Ceux-ci lui permettent de se connecter à la base de données ODBC contenant les données des différentes transches d'impôts (DSNimpots, admimpots, mdpimpots) et les URL des pages associées à l'application : urlAffichageImpots pour le formulaire, urlErreur pour la page d'erreur.
    • elle crée une instance de la classe impotsJDBC

Dans les deux cas, les erreurs possibles sont gérées et le message d'erreur placé dans la variable msgErreur.

  • la méthode doGET
    • vérifie tout d'abord que la servlet s'est initialisée correctement. Si ce n'est pas le cas, la page d'erreur est affichée
    • récupère les paramètres attendus du formulaire d'impôts : optMarie, txtEnfants, txtSalaire. Si l'un d'eux manque (==null) un formulaire d'impôts vide est envoyé. On pourrait se dire que la vérification de la validité du paramètre optMarie est inutile. Celui-ci est la valeur d'un bouton radio et ne peut avoir ici que l'une des valeurs "oui" et "non". C'est oublier que rien n'empêche un programme d'interroger directement la servlet en lui envoyant les paramètres qu'il veut. On ne peut jamais être assuré d'être réellement connecté à un navigateur. Oublier ce point peut amener à des trous de sécurité dans l'application et il est d'ailleurs fréquent d'en trouver même dans des applications commerciales.
    • la validité de chacun des trois paramètres récupérés est vérifié. Chaque erreur trouvée est ajoutée à une liste d'erreurs (ArrayList erreurs). S'il n'y a pas d'erreurs, le montant de l'impôt est calculé sinon il ne l'est pas.
    • les informations nécessaires à l'affichage de la page sont mis en attributs de la requête et le formulaire d'impôts est ensuite affiché

La page JSP d'erreur est la suivante :

<%
    // jspService
  // une erreur s'est produite
  String msgErreur= (String)request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- début de la page HTML -->
<html>
  <head>
    <title>impots</title>
  </head>
  <body>
    <h3>calcul d'impots</h3>
    <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

L'aplication web s'appelle impots et est configuré dans le fichier server.xml de Tomcat de la façon suivante :

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

Le dossier de l'application contient les dossiers et fichiers suivants :

Image

Image

Image

Image

Le fichier web.xml de configuration de l'application impots est le suivant :

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

La servlet principale s'appelle main et a l'alias /main. Elle est donc accessible via l'URL http://localhost:8080/impots/main.

Voici quelques exemples d'application :

Pour s'initialiser correctement, la servlet doit avoir accès à la base de données mysql-dbimpots. Voici la page obtenue si par exemple le serveur MySQL n'est pas lancé et la base mysql-dbimpots en conséquence inaccessible :

Image

La page obtenue en cas de saisie incorrecte est la suivante :

Image

Si les saisie sont correctes, l'impôt est calculé :

Image

5.3. Version 2

Dans l'exemple précédent, la validité des paramètres txtEnfants, txtSalaire du formulaire est vérifiée par le serveur. On se propose ici de la vérifier par un script Javascript inclus dans la page du formulaire. C'est alors le navigateur qui fait la vérification des paramètres. Le serveur n'est alors sollicité que si ceux-ci sont valides. On économise ainsi de la "bande passante". La page JSP d'affichage devient la suivante :

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

<html>

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

      function calculer(){
        // vérification des paramètres avant de les envoyer au serveur
        with(document.frmImpots){
          //nbre d'enfants
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // le modéle n'est pas vérifié
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            nbEnfants.focus();
            return;
          }//if
          //salaire
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // le modéle n'est pas vérifié
            alert("Le salaire n'a pas été donné ou est incorrect");
            salaire.focus();
            return;
          }//if
          // c'est bon - on envoie le formulaire au serveur
          submit();
        }//with
      }//calculer        
        </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>

On notera les changements suivants :

  • le bouton Calculer n'est plus de type submit mais de type button associé à une fonction appelée calculer. C'est celle-ci qui fera l'analyse des champs txtEnfants et txTsalaire. S'ils sont corrects, les valeurs du formulaire seront envoyées au serveur (submit) sinon un message d'erreur sera affiché.

Voici un exemple d'affichage en cas d'erreur :

Image

5.4. Version 3

Nous modifions légèrement l'application pour y introduire la notion de session. Nous considérons maintenant que l'application est une application de simulation de calcul d'impôts. Un utilisateur peut alors simuler différentes "configurations" de contribuable et voir quelle serait pour chacune d'elles l'impôt à payer. La page WEb ci-dessous donne un exemple de ce qui pourrait être obtenu :

Image

La servlet principale est modifiée et s'appelle désormais simulations. Elle est configurée comme suit au sein de l'application 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>

La servlet principale s'appelle simulations et s'appuie sur le fichier classe simulations.class. Elle a l'alias /simulations qui la rend accessible via l'URL http://localhost:8080/impots/simulations. Elle a les mêmes paramètres d'initialisation que la servlet main étudiée précédemment pour ce qui est de l'accès à la base de données. Un nouveau paramètre apparaît, urlSimulationsImpots qui est l'URL de la page JSP de simulation (celle qui vient d'être présentée un peu plus haut).

La servlet simulations.java est proche de la servlet main.java. Elle en diffère par les points principaux suivants :

  • la servlet main calcule une valeur txtImpots à partir des paramètres optmarie, txtEnfants et txtSalaire et passe cette valeur à la page JSP d'affichage
  • la servlet simulations calcule de la même façon la valeur txtImpots, enregistre les paramètres (optMarie, txtEnfants, txtsalaire, txtImpots) dans une liste appelée simulations. C'est cette liste qui est passée en paramètre à la page JSP d'affichage. Afin que cette liste contienne toutes les simulations effectuées par l'utilisateur, elle est enregistrée comme attribut de la session courante.

La servlet simulations est la suivante ( n'ont été conservées que les lignes de code qui diffèrent de celles de l'application précédente) :

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

public class simulations extends HttpServlet{

    // variables d'instance
    String msgErreur=null;
    String urlSimulationImpots=null;
    String urlErreur=null;
...........

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

...........

        // on récupère les simulations précédentes de la session
        HttpSession session=request.getSession();
        ArrayList simulations=(ArrayList)session.getAttribute("simulations");
        if(simulations==null) simulations=new ArrayList();
        // on met les simulations dans la requête courante
        request.setAttribute("simulations",simulations);

        // d'autres attributs de la requête
...........

        // a-t-on tous les paramètres attendus
        if(optMarie==null || txtEnfants==null || txtSalaire==null){
........
            // on passe la main à l'url d'affichage des simulations de calculs d'impôts
            getServletContext().getRequestDispatcher(urlSimulationImpots).forward(request,response);
        }

        // on a tous les paramètres - on les vérifie
...........

        // s'il y a des erreurs, on les passe en attributs de la requête
        if(erreurs.size()!=0){
            request.setAttribute("erreurs",erreurs);
        }else{
            try{
                // on peut calculer l'impôt à payer
                int nbEnfants=Integer.parseInt(txtEnfants);
                int salaire=Integer.parseInt(txtSalaire);
                txtImpots=""+impots.calculer(optMarie.equals("oui"),nbEnfants,salaire);
                // on ajoute le résultat courant aux simulations précédentes
                String[] simulation={optMarie.equals("oui") ? "oui" : "non",txtEnfants, txtSalaire, txtImpots};
                simulations.add(simulation);
                // la nouvelle valeur de simulations est remise dans la session
                session.setAttribute("simulations",simulations);
            }catch(Exception ex){}
        }

        // autres attributs de la requête
..........

        // on passe la main à l'url d'affichage des simulations
        getServletContext().getRequestDispatcher(urlSimulationImpots).forward(request,response);
    }

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

    //-------- INIT
    public void init(){
        // on récupère les paramètres d'initialisation
        ServletConfig config=getServletConfig();
        urlSimulationImpots=config.getInitParameter("urlSimulationImpots");
        urlErreur=config.getInitParameter("urlErreur");
        DSNimpots=config.getInitParameter("DSNimpots");
        admimpots=config.getInitParameter("admimpots");
        mdpimpots=config.getInitParameter("mdpimpots");

        // paramètres ok ?
.........................
    }
}

La page JSP d'affichage simulationsImpots.jsp est devenue la suivante (n'a été conservé que le code qui diffère de la page JSP d'affichage de l'application précédente).

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

<%
    // on récupère les attributs passés par la servlet principale
...........
  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>
    <%
        // y-a-t-il des erreurs ?
      if(erreurs!=null){
..................
      }else if(simulations.size()!=0){
        // résultats des simulations
        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. Version 4

Nous allons maintenant créer une application autonome qui sera un client web de l'application /impots/simulations précédente. Cette application aura l'interface graphique suivante :

Image

type

nom

rôle

1

JTextField

txtUrlServiceImpots

URL du service de simulation du calcul d'impôts

2

JRadioButton

rdOui

coché si marié

3

JRadioButton

rdNon

coché si non marié

4

JSpinner

spinEnfants

nombre d'enfants du contribuable (minimum=0, maximum=20, increment=1)

5

JTextField

txtSalaire

salaire annuel en F du contribuable

6

JList dans JScrollPane

lstSimulations

liste des simulations

Le menu Impôts est composé des options suivantes :

option

principale

option

secondaire

nom

rôle

Impôts

     
 

Calculer

mnuCalculer

calcule l'impôt à payer lorsque toutes les données nécessaires au calcul sont présentes et correctes

 

Effacer

mnuEffacer

remet le formulaire dans son état initial

 

Quitter

mnuQuitter

termine l'application

Règles de fonctionnement

  • l'option de menu Calculer reste éteinte si l'un des champs 1 ou 5 est vide
  • une URL syntaxiquement incorrecte en 1 est détectée

Image

  • un salaire incorrect est détecté

Image

  • toute erreur de connexion au serveur est signalée (dans l'exemple 1 ci-dessous, le port est incorrect - dans l'exemple 2, l'URL demandée n'existe pas - dans l'exemple 3 la base de données MySQL n'était pas lancé)

Image

Image

Image

  • Si tout est correct, les simulations sont affichées

Image

Lorsqu'on écrit un client web programmé, il est nécessaire de savoir exactement ce qu'envoie le serveur en réponse aux différentes demandes possibles d'un client. Le serveur envoie un ensemble de lignes HTML comprenant des informations utiles et d'autres qui ne sont là que pour la mise en page HTML. Les expressions régulières de Java peuvent nous aider à trouver les informations utiles dans le flot des lignes envoyées par le serveur. Pour cela, nous avons besoin de connaître le format exact des différentes réponses du serveur. Ici nous allons utiliser le client web déjà rencontré qui permet d'afficher à l'écran la réponse d'un serveur à la demande d'une URL. L'URL demandée sera celle de notre service de simulations de calculs d'impôts http://localhost:8080/impots/simulations à laquelle nous pourrons passer des paramètres sous la forme http://localhost:8080/impots/simulations?param1=vam1&param2=val2&...

Demandons l'URL alors que la base MySQL permettant de créer on objet de type impots n'est pas lancée :


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 de la page 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>

Pour récupérer l'erreur, le client web devra chercher dans la réponse du serveur web la ligne ayant le texte "Application indisponible". Maintenant lançons la base MySQL et demandons la même URL en lui passant les valeurs qu'elle attend (elle les attend indifféremment sous la forme d'un GET ou d'un POST - cf code java du serveur) :


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(){
...................................................
      }//effacer

      function calculer(){
...................................................
      }//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>

On retrouve ici la totalité du document HTML envoyé par le serveur. On trouve le résultat des différentes simulations dans l'unique table présente dans ce document. L'expression régulière nous permettant de récupérer les informations pertinentes du document pourra être la suivante :

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

où les quatre expressions parenthésées représentent les quatre informations à récupérer.

Nous avons maintenant les lignes directrices de ce qu'il faut faire lorsque l'utilisateur demande le calcul de l'impôt à partir de l'interface graphique précédente :

  • vérifier que toutes les données de l'interface sont valides et éventuellement signaler les erreurs.
  • se connecter à l'URL indiquée dans le champ 1. Pour cela on suivra le modèle du client web générique déjà présenté et étudié
  • dans le flux de la réponse du serveur, utiliser des expressions régulières pour soit :
    • trouver le message d'erreur s'il y en a un
    • trouver les résultats des simulations s'il n'y a pas d'erreur

Le code lié au menu calculer est le suivant :

  void mnuCalculer_actionPerformed(ActionEvent e) {
    // calcul de l'impôt
        // vérification URL service
        URL urlImpots=null;
        try{
            urlImpots=new URL(txtURLServiceImpots.getText().trim());
            String query=urlImpots.getQuery();
            if(query!=null) throw new Exception();
        }catch (Exception ex){
            // msg d'erreur
            JOptionPane.showMessageDialog(this,"URL incorrecte. Recommencez","Erreur",JOptionPane.ERROR_MESSAGE);
            // focus sur champ erroné
            txtURLServiceImpots.requestFocus();
            // retour à l'interface
            return;
        }
    // vérification salaire
    int salaire=0;
    try{
      salaire=Integer.parseInt(txtSalaire.getText().trim());
      if(salaire<0) throw new Exception();
    }catch (Exception ex){
      // msg d'erreur
            JOptionPane.showMessageDialog(this,"Salaire incorrect. Recommencez","Erreur",JOptionPane.ERROR_MESSAGE);
      // focus sur champ erroné
      txtSalaire.requestFocus();
      // retour à l'interface
      return;
    }
    // nbre d'enfants
    Integer nbEnfants=(Integer)spinEnfants.getValue();

    try{
            // on calcule l'impôt
            calculerImpots(urlImpots,rdOui.isSelected(),nbEnfants.intValue(),salaire);
    }catch (Exception ex){
            // on affiche l'erreur
            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{
        // calcul de l'impôt
        // urlImpots : URL du service des impôts
        // marié : true si marié, false sinon
        // nbEnfants : nombre d'enfants
        // salaire : salaire annuel

        // on retire d'urlImpots les infos nécessaire à la connexion au serveur d'impôts
        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();

        // données locales
        Socket  client=null;                        // le client
        BufferedReader IN=null;                 // le flux de lecture du client
        PrintWriter OUT=null;                       // le flux d'écriture du client
        String réponse=null;                        // réponse du serveur
        // le modèle recherché dans les entêtes HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // le modèle d'une réponse correcte
        Pattern réponseOK=Pattern.compile("^.*? 200 OK");
        // le résultat de la comparaison au modèle
        Matcher résultat=null;

        try{
            // on se connecte au serveur
            client=new Socket(host,port);

            // on crée les flux d'entrée-sortie du client TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // on demande l'URL - envoi des entêtes 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("");

            // on lit la 1ère ligne de la réponse
            réponse=IN.readLine();
            // on compare la ligne HTTP au modèle de la réponse correcte
            résultat=réponseOK.matcher(réponse);
            if(! résultat.find()){
                // on a un problème d'URL
                throw new Exception("Le serveur a répondu : URL ["+ txtURLServiceImpots.getText().trim() + "] inconnue");
            }//if(résultat)

            // on lit la réponse jusqu'à la fin des entêtes en cherchant l'éventuel cookie
            while((réponse=IN.readLine())!=null){
                // ligne vide ?
                if(réponse.equals("")) break;
                // ligne HTTP non vide
                // si on n'a pas le jeton de la session on le cherche
                if (JSESSIONID.equals("")){
                    // on compare la ligne HTTP au modèle du cookie
                    résultat=modèleCookie.matcher(réponse);
                    if(résultat.find()){
                        // on a trouvé le cookie du jeton
                        JSESSIONID=résultat.group(1);
                    }//if(résultat)
                }//if(JSESSIONID)
            }//while

            // c'est fini pour les entêtes HTTP - on passe au code HTML
            // pour récupérer les simulations
            ArrayList listeSimulations=getSimulations(IN,OUT,simulations);
            simulations.clear();
            for (int i=0;i<listeSimulations.size();i++){
                simulations.addElement(listeSimulations.get(i));
            }

            // c'est fini
            client.close();
        }catch (Exception ex){
            throw new Exception(ex.getMessage());
        }
    }//calculerImpots

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

        // le modèle d'une ligne du tableau des simulations
        Pattern ptnSimulation=Pattern.compile("<tr>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*</tr>");
        // le modèle d'une ligne de la liste des erreurs
        Pattern ptnErreur=Pattern.compile("(Application indisponible.*?)\\s*$");
        // le résultat de la comparaison au modèle
        Matcher résultat=null;
        // les simulations
        ArrayList listeSimulations=new ArrayList();

        // on lit toutes les lignes jusqu'à la fin
        String ligne=null;
        boolean simulationRéussie=false;
        while((ligne=IN.readLine())!=null){
            // suivi
            // on compare la ligne au modèle d'erreur si la partie simulations n'a pas encore été rencontrée
            if(! simulationRéussie){
                résultat=ptnErreur.matcher(ligne);
                if(résultat.find()){
                    // msg d'erreur
                    JOptionPane.showMessageDialog(this,résultat.group(1),"Erreur",JOptionPane.ERROR_MESSAGE);
                    // c'est fini
                    return listeSimulations;
                }//if
            }//if
            // on compare la ligne au modèle de simulation
            résultat=ptnSimulation.matcher(ligne);
            if(résultat.find()){
                // on a trouvé une ligne de la table
                listeSimulations.add(résultat.group(1)+":"+résultat.group(2)+":"+résultat.group(3)+
                                                ":"+résultat.group(4));
                // la simulation a été réussie
                simulationRéussie=true;
            }//if
        }//while
        // fin
        return listeSimulations;
    }

Explicitons un peu ce code :

  • la procédure mnuCalculer_actionPerformed vérifie que les données de l'interface sont valides. Si elles ne le sont pas un message d'erreur est émis et la procédure terminée. Si elles le sont, la procédure calculerImpots est exécutée.
  • la procédure calculerImpots commence par construire l'URL qu'elle doit demander
        // on retire d'urlImpots les infos nécessaire à la connexion au serveur d'impôts
        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();
........................
  • puis se connecte à cette URL en envoyant les entêtes HTTP adéquats :
            // on se connecte au serveur
            client=new Socket(host,port);

            // on crée les flux d'entrée-sortie du client TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // on demande l'URL - envoi des entêtes 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("");
  • une fois la requête faite, notre client web attend la réponse. Il obtient d'abord les entêtes HTTP de la réponse du serveur. Il explore ces entêtes pour y trouver le jeton de la session. En effet, il devra renvoyer celui-ci au serveur pour que ce dernier puisse garder trace des différentes simulations faites. On notera ici qu'il aurait été plus simple que le client mémorise lui-mêmes les différentes simulations opérées par l'utilisateur. Néanmoins, nous gardons l'idée de renvoyer le jeton pour fournir un nouvel exemple de gestion de session. La première ligne de la réponse est traitée à part. En effet elle doit être de la forme HTTP/version 200 OK pour dire que l'URL demandée existe bien. Si donc elle n'est pas de cette forme on en conclut que l'utilisateur a demandé une URL incorrecte et on le lui dit.
        // le modèle recherché dans les entêtes HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // le modèle d'une réponse correcte
        Pattern réponseOK=Pattern.compile("^.*? 200 OK");
..........

            // on lit la 1ère ligne de la réponse
            réponse=IN.readLine();
            // on compare la ligne HTTP au modèle de la réponse correcte
            résultat=réponseOK.matcher(réponse);
            if(! résultat.find()){
                // on a un problème d'URL
                throw new Exception("Le serveur a répondu : URL ["+ txtURLServiceImpots.getText().trim() + "] inconnue");
            }//if(résultat)

            // on lit la réponse jusqu'à la fin des entêtes en cherchant l'éventuel cookie
            while((réponse=IN.readLine())!=null){
                // ligne vide ?
                if(réponse.equals("")) break;
                // ligne HTTP non vide
                // si on n'a pas le jeton de la session on le cherche
                if (JSESSIONID.equals("")){
                    // on compare la ligne HTTP au modèle du cookie
                    résultat=modèleCookie.matcher(réponse);
                    if(résultat.find()){
                        // on a trouvé le cookie du jeton
                        JSESSIONID=résultat.group(1);
                    }//if(résultat)
                }//if(JSESSIONID)
            }//while
  • une fois les entêtes HTTP exploités, on passe à la partie HTML de la réponse
            // c'est fini pour les entêtes HTTP - on passe au code HTML
            // pour récupérer les simulations
            ArrayList listeSimulations=getSimulations(IN,OUT,simulations);
            simulations.clear();
            for (int i=0;i<listeSimulations.size();i++){
                simulations.addElement(listeSimulations.get(i));
            }
  • la procédure getSimulations rend la liste des simulations si elles existent, liste qui sera vide si le serveur renvoie un message d'erreur. Dans ce cas, celui-ci est affiché dans une boîte de message. Si la liste est non vide, elle est affichée dans la liste déroulante de l'interface graphique.

  • la procédure getSimulations va comparer chaque ligne de la réponse HTML à l'expression régulière représentant le message d'erreur (Application indisponible...) et à l'expression régulière représentant une simulation. Si le message d'erreur est rencontré, il est affiché et la procédure terminée. Si le résultat d'une simulation est trouvé, il est ajouté à la liste des simulations. A la fin de la procédure cette liste est rendue comme résultat.

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

        // le modèle d'une ligne du tableau des simulations
        Pattern ptnSimulation=Pattern.compile("<tr>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*</tr>");
        // le modèle d'une ligne de la liste des erreurs
        Pattern ptnErreur=Pattern.compile("(Application indisponible.*?)\\s*$");

5.6. Version 5

Ici, nous transformons l'application graphique autonome précédente en applet java. L'interface graphique est légèrement différente. Avec l'application autonome l'utilisateur donnait lui-même l'URL du service de simulation de calculs d'impôts et ensuite l'application se connectait à cette URL. Ici, l'application cliente est un navigateur et l'utilisateur demandera l'URL du document HTML contenant l'applet. Il faut se rappeler maintenant qu'une applet Java ne peut ouvrir une connexion réseau qu'avec le serveur d'où elle a été téléchargée. L'URL du service de simulation sera donc sur le même serveur que le document HTML contenant l'applet. Dans notre exemple, ce sera un paramètre d'initialisation de l'applet qui sera mis dans le champ txtUrlServiceImpots, champ qui sera non éditable par l'utilisateur. On aura ainsi le client suivant :

Image

Le document HTML contenant l'applet s'appelle simulations.htm et est le suivant :

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

L'applet a un paramètre urlServiceImpots qui est l'URL du service de simulations du calcul d'impôts. Cette URL est relative et mesurée par rapport à l'URL du document HTML simulations.htm. Ainsi si un navigateur obtient ce document avec l'URL http://localhost:8080/impots/simulations.htm, l'URL du service de simulation sera http://localhost:8080/impots/simulations. Si cette URL avait été http://stahe:8080/impots/simulations.htm l'URL du service de simulation aurait été http://stahe:8080/impots/simulations.

L'applet appletImpots.java reprend intégralement le code de l'application graphique autonome précédente en respectant les règles de transformation d'une application graphique en applet.

public class appletImpots extends JApplet {
  // les composants de la fenêtre
  JPanel contentPane;
  JMenuBar jMenuBar1 = new JMenuBar();
  JMenu jMenu1 = new JMenu();
  JMenuItem mnuCalculer = new JMenuItem();
.............


  //Construire le cadre
  public void init() {
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
    // autres initialisations
    moreInit();
  }

  // initialisation formulaire
  private void moreInit(){
        // on récupère le paramètre urlServiceImpots
        String urlServiceImpots=getParameter("urlServiceImpots");
        if(urlServiceImpots==null){
            // paramètre manquant
            JOptionPane.showMessageDialog(this,"Le paramètre urlServiceImpots de l'applet n'a pas été défini","Erreur",JOptionPane.ERROR_MESSAGE);
            // fin
            return;
        }
        // on met l'URL dans son champ
        String codeBase=""+getCodeBase();
        if(codeBase.endsWith("/"))
           txtURLServiceImpots.setText(codeBase+urlServiceImpots);
      else txtURLServiceImpots.setText(codeBase+"/"+urlServiceImpots);
    // menu Calculer inhibé
    mnuCalculer.setEnabled(false);
    // spinner Enfants - entre 0 et 20 enfants
    spinEnfants=new JSpinner(new SpinnerNumberModel(0,0,20,1));
    spinEnfants.setBounds(new Rectangle(130,140,50,27));
    contentPane.add(spinEnfants);
  }//moreInit

  //Initialiser le composant
  private void jbInit() throws Exception  {
    contentPane = (JPanel) this.getContentPane();
    contentPane.setLayout(null);
...............
  }

Lorsque un navigateur charge une applet, il exécute tout d'abord sa procédure init. Dans celle-ci, nous avons récupéré la valeur du paramètre urlServiceImpots de l'applet puis calculé le nom complet de l'URL du service de simulation et placé cette valeur dans le champ txtURLServiceImpots comme si c'était l'utilisateur qui l'avait tapé. Ceci fait, il n'y a plus de différence entre les deux applications. Notamment, le code associé au menu Calculer est identique. Voici un exemple d'exécution :

Image

5.7. Conclusion

Nous avons montré différentes versions de notre application client-serveur de calcul d'impôts :

  • version 1 : le service est assuré par un ensemble de servlets et pages JSP, le client est un navigateur. Il fait une seule simulation et n'a pas de mémoire des précédentes.
  • version 2 : on ajoute quelques capacités côté navigateur en mettant des scripts javascript dans le document HTML chargé par celui-ci. Il contrôle la validité des paramètres du formulaire.
  • version 3 : on permet au service de se souvenir des différentes simulations opérées par un client en gérant une session. L'interface HTML est modifiée en conséquence pour afficher celles-ci.
  • version 4 : le client est désormais une application graphique autonome. Cela nous permet de revenir sur l'écriture de clients web programmés.
  • version 5 : le client se transforme en applet java. On a alors une application client-serveur entièrement programmée en Java que ce soit côté serveur ou côté client.

A ce point, on peut faire quelques remarques :

  • les versions 1 à 3 autorisent des navigateurs sans capacité autre que celle de pouvoir exécuter des scripts javascript. On notera qu'un utilisateur a toujours la possibilité d'inhiber l'exécution de ces derniers. L'application ne fonctionnera alors que partiellement dans sa version 1 (option Effacer ne fonctionnera pas) et pas du tout dans ses versions 2 et 3 (options Effacer et Calculer ne fonctionneront pas). Il pourrait être intéressant de prévoir une version du service n'utilisant pas de scripts javascript.
  • la version 4 nécessite que le poste client ait une machine virtuelle Java 2.
  • la version 5 nécessite que le poste client ait un navigateur ayant une machine virtuelle Java 2.

Lorsqu'on écrit un service Web, il faut se demander quels types de clients on vise. Si on veut viser le plus grand nombre de clients, on écrira une application qui n'envoie aux navigateurs que du HTML (pas de javascript ni d'applet). Si on travaille au sein d'un intranet et qu'on maîtrise la configuration des postes de celui-ci on peut alors se permettre d'être plus exigeant au niveau du client et la version 5 précédente peut alors être acceptable.

Les versions 4 et 5 sont des clients web qui retrouvent l'information dont ils ont besoin au sein du flux HTML envoyé par le serveur. Très souvent on ne maîtrise pas ce flux. C'est le cas lorsqu'on a écrit un client pour un service web existant sur le réseau et géré par quelqu'un d'autre. Prenon un exemple. Supposons que notre service de simulations de calcul d'impôts ait été écrit par une société X. Actuellement le service envoie les simulations dans un tableau HTML et notre client exploite ce fait pour les récupérer. Il compare ainsi chaque ligne de la réponse du serveur à l'expression régulière :

        // le modèle d'une ligne du tableau des simulations
        Pattern ptnSimulation=Pattern.compile("<tr>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*<td>(.*?)</td>\\s*</tr>");

Supposons maintenant que le concepteur de l'application change l'apparence visuelle de la réponse en mettant les simulations non pas dans un tableau mais dans une liste sous la forme :

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

Dans ce cas, notre client web devra être réécrit. C'est là la menace permanente pesant sur les clients web d'applications qu'on ne maîtrise pas soi-même. XML peut apporter une solution à ce problème :

  • au lieu de générer du HTML, le service de simulations va générer du XML. Dans notre exemple, cela pourrait être
<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>
  • une feuille de style pourrait être associée à cette réponse indiquant aux navigateurs la forme visuelle à donner à cette réponse XML
  • les clients web programmés ignoreraient cette feuille de style et récuperaient l'information directement dans le flux XML de la réponse

Si le concepteur du service souhaite modifier la présentation visuelle des résultats qu'il fournit, il modifiera la feuille de style et non le XML. Grâce à la feuille de style, les navigateurs afficheront la nouvelle forme visuelle et les clients web programmés n'auront pas eux à être modifiés. De nouvelles version de notre service de simulations pourraient donc être écrites :

  • version 6 : le service fournit une réponse XML accompagnée d'une feuille de style à destination des navigateurs
  • version 7 : le client est une application graphique autonome exploitant la réponse XML du serveur
  • version 8 : le client est une applet java exploitant la réponse XML du serveur