Skip to content

4. Suivi de session

4.1. Le problème

Une application web peut consister en plusieurs échanges de formulaires entre le serveur et le client. On a alors le fonctionnement suivant :

  • étape 1
    • le client C1 ouvre une connexion avec le serveur et fait sa requête initiale.
    • le serveur envoie le formulaire F1 au client C1 et ferme la connexion ouverte en 1.
  • étape 2
    • le client C1 le remplit et le renvoie au serveur. Pour ce faire le navigateur ouvre une nouvelle connexion avec le serveur.
    • celui-ci traite les données du formulaire 1, calcule des informations I1 à partir de celles-ci, envoie un formulaire F2 au client C1 et ferme la connexion ouverte en 3.
  • étape 3
    • le cycle des étapes 3 et 4 se répète dans des étapes 5 et 6. A l'issue de l'étape 6, le serveur aura reçu deux formulaires F1 et F2 et à partir de ceux-ci aura calculé des informations I1 et I2.

Le problème posé est : comment le serveur fait-il pour conserver les informations I1 et I2 liées au client C1 ? On appelle ce problème le suivi de la session du client C1. Pour comprendre sa racine, examinons le schéma d'une application serveur TCP-IP servant simultanément plusieurs clients :

Image

Dans une application client-serveur TCP-IP classique :

  • le client crée une connexion avec le serveur
  • échange à travers celle-ci des données avec le serveur
  • la connexion est fermée par l'un des deux partenaires

Les deux points importants de ce mécanisme sont :

  1. une connexion unique est créée pour chacun des clients
  2. cette connexion est utilisée pendant toute la durée du dialogue du serveur avec son client

Ce qui permet au serveur de savoir à un moment donné avec quel client il travaille, c'est la connexion ou dit autrement le "tuyau" qui le relie à son client. Ce tuyau étant dédié à un client donné, tout ce qui arrive de ce tuyau vient de ce client et tout ce qui est envoyé dans ce tuyau arrive au client.

Le mécanisme du client-serveur HTTP suit bien le schéma précédent avec cependant la particularité que le dialogue client-serveur est limité à un unique échange entre le client et le serveur :

  • le client ouvre une connexion vers le serveur et fait sa demande
  • le serveur fait sa réponse et ferme la connexion

Si au temps T1, un client C fait une demande au serveur, il obtient une connexion C1 qui va servir à l'échange unique demande-réponse. Si au temps T2, ce même client fait une seconde demande au serveur, il va obtenir une connexion C2 différente de la connexion C1. Pour le serveur, il n'y a alors aucune différence entre cette seconde demande de l'utilisateur C et sa demande intiale : dans les deux cas, le serveur considère le client comme un nouveau client. Pour qu'il y ait un lien entre les différentes connexions du client C au serveur, il faut que le client C se fasse "reconnaître" par le serveur comme un "habitué" et que le serveur récupère les informations qu'il a sur cet habitué.

Imaginons une administration qui fonctionnerait de la façon suivante :

  • Il y a une unique file d'attente
  • Il y a plusieurs guichets. Donc plusieurs clients peuvent être servis simultanément. Lorsqu'un guichet se libère, un client quitte la file d'attente pour être servi à ce guichet
  • Si c'est la première fois que le client se présente, la personne au guichet lui donne un jeton avec un numéro. Le client ne peut poser qu'une question. Lorsqu'il a sa réponse il doit quitter le guichet et passer à la fin de la file d'attente. Le guichetier note les informations de ce client dans un dossier portant son numéro.
  • Lorsqu'arrive à nouveau son tour, le client peut être servi par un autre guichetier que la fois précédente. Celui-ci lui demande son jeton et récupère le dossier ayant le numéro du jeton. De nouveau le client fait une demande, a une réponse et des informations sont rajoutées à son dossier.
  • et ainsi de suite... Au fil du temps, le client aura la réponse à toutes ses requêtes. Le suivi entre les différentes requêtes est réalisé grâce au jeton et au dossier associé à celui-ci.

Le mécanisme de suivi de session dans une application client-serveur web est analogue au fonctionnement précédent :

  • lors de sa première demande, un client se voit donner un jeton par le serveur web
  • il va présenter ce jeton à chacune de ses demandes ultérieures pour s'identifier

Le jeton peut revêtir différentes formes :

  • celui d'un champ caché dans un formulaire
    • le client fait sa première demande (le serveur le reconnaît au fait que le client n'a pas de jeton)
    • le serveur fait sa réponse (un formulaire) et met le jeton dans un champ caché de celui-ci. A ce moment, la connexion est fermée (le client quitte le guichet avec son jeton). Le serveur a pris soin éventuellement d'associer des informations à ce jeton.
    • le client fait sa seconde demande en renvoyant le formulaire. Le serveur récupère dans celui-ci le jeton. Il peut alors traiter la seconde demande du client en ayant accès, grâce au jeton, aux informations calculées lors de la première demande. De nouvelles informations sont ajoutées au dossier lié au jeton, une seconde réponse est faite au client et la connexion est fermée pour la seconde fois. Le jeton a été mis de nouveau dans le formulaire de la réponse afin que l'utilisateur puisse le présenter lors de sa demande suivante.
    • et ainsi de suite...

L'inconvénient principal de cette technique est que le jeton doit être mis dans un formulaire. Si la réponse du serveur n'est pas un formulaire, la méthode du champ caché n'est plus utilisable.

  • celui du cookie
    • le client fait sa première demande (le serveur le reconnaît au fait que le client n'a pas de jeton)
    • le serveur fait sa réponse en ajoutant un cookie dans les entêtes HTTP de celle-ci. Cela se fait à l'aide de la commande HTTP Set-Cookie :

Set-Cookie: param1=valeur1;param2=valeur2;....

parami sont des noms de paramètres et valeursi leurs valeurs. Parmi les paramètres, il y aura le jeton. Bien souvent, il n'y a que le jeton dans le cookie, les autres informations étant consignées par le serveur dans le dossier lié au jeton. Le navigateur qui reçoit le cookie va le stocker dans un fichier sur le disque. Après la réponse du serveur, la connexion est fermée (le client quitte le guichet avec son jeton).

  • (suite)
    • le client fait sa seconde demande au serveur. A chaque fois qu'une demande à un serveur est faite, le navigateur regarde parmi tous les cookies qu'il a, s'il en a un provenant du serveur demandé. Si oui, il l'envoie au serveur toujours sous la forme d'une commande HTTP, la commande Cookie qui a une syntaxe analogue à celle de la commande Set-Cookie utilisée par le serveur :

Cookie: param1=valeur1;param2=valeur2;....

Parmi les paramètres envoyés par le navigateur, le serveur retrouvera le jeton lui permettant de reconnaître le client et de retrouver les informations qui lui sont liées.

C'est la forme la plus utilisée de jeton. Elle présente un inconvénient : un utilisateur peut configurer son navigateur afin qu'il n'accepte pas les cookies. Ce type d'utilisateur n'a alors pas accès aux applications web avec cookie.

  • réécriture d'URL
    • le client fait sa première demande (le serveur le reconnaît au fait que le client n'a pas de jeton)
    • le serveur envoie sa réponse. Celle-ci contient des liens que l'utilisateur doit utiliser pour continuer l'application. Dans l'URL de chacun de ces liens, le serveur rajoute le jeton sous la forme URL;jeton=valeur.
    • lorsque l'utilisateur clique sur l'un des liens pour continuer l'application, le navigateur fait sa demande au serveur web en lui envoyant dans les entêtes HTTP l'URL URL;jeton=valeur demandée. Le serveur est alors capable de récupérer le jeton.

4.2. L'API Java pour le suivi de session

Nous présentons maintenant les principales méthodes utiles au suivi de session :


HttpSession [HttpServletRequest].getSession()

obtient l'objet Session auquel appartient la requête en cours. Si celle-ci ne faisait pas encore partie d'une session, cette dernière est créée.


String [HttpSession].getId()

idntifiant de la session en cours


long [HttpSession].getCreationTime()

date de création de la session en cours (nombre de millisecondes écoulées depuis le 1er janvier 1970, 0h).


long [HttpSession].getLastAccessedTime()

date du dernier accès à la session par le client


long [HttpSession].getMaxInactiveInterval()

durée maximale en secondes d'inactivité d'une session. Au bout de ce laps de temps, la session est invalidée.


[HttpSession].setMaxInactiveInterval(int durée)

fixe en secondes la durée maximale d'inactivité d'une session. Au bout de ce laps de temps, la session est invalidée.


boolean [HttpSession].isNew()

vrai si la session vient d'être créée


[HttpSession].setAttribute(String paramètre, Object valeur)

associe une valeur à un paramètre dans une session donnée. C'est ce mécanisme qui permet de mémoriser des informations qui resteront disponibles tout au long de la session.


[HttpSession].removeAttribute(String paramètre)

enlève parametre des données de la session.


Object [HttpSession].getAttribute(String paramètre)

valeur associée au paramètre paramètre de la session. Rend null si ce dernier n'existe pas.


Enumeration [HttpSession].getAttributeNames()

liste sous forme d'énumération de tous les attributs de la session en cours


[HttpSession].invalidate()

clôt la session en cours. Toutes les informations associées à celle-ci sont détruites.

4.3. Exemple 1

Nous présentons un exemple tiré de l'excellent livre "Programmation avec J2EE" aux éditions Wrox et distribué par Eyrolles. Ce livre est une mine d'informations de haut niveau pour les développeurs de solutions Web en Java. L'application présentée dans ce livre sous la forme d'une unique servlet Java a été ici reprise ici sous la forme d'une servlet principale faisant appel à des pages JSP pour l'affichage des diverses réponses possibles au client.

L'application s'appelle sessions et est configurée de la façon suivante dans le fichier <tomcat>\conf\server.xml :

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

Dans le dossier docBase ci-dessus, on trouve les éléments suivants :

Image

Les fichiers erreur.jsp, invalide.jsp, valide.jsp sont tous trois associés à l'application sessions. Dans le dossier WEB-INF ci -dessus on trouve :

Image

On voit ci-dessus le fichier web.xml de configuration de l'application sessions. Dans le dossier classes on trouve le fichier classe de la servlet :

Image

Le fichier web.xml de l'application 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>cycledevie</servlet-name>
    <servlet-class>cycledevie</servlet-class>
    <init-param>
        <param-name>urlSessionValide</param-name>
        <param-value>/valide.jsp</param-value>
    </init-param>
    <init-param>
        <param-name>urlSessionInvalide</param-name>
        <param-value>/invalide.jsp</param-value>
    </init-param>
    <init-param>
        <param-name>urlErreur</param-name>
        <param-value>/erreur.jsp</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>cycledevie</servlet-name>
    <url-pattern>/cycledevie</url-pattern>
  </servlet-mapping>
</web-app>

La servlet principale s'appelle cycledevie (servlet-name) et est associée au fichier classe cycledevie.class (servlet-class). Elle a un alias /cycledevie (servlet-mapping) qui permet de l'appeler via l'URL http://localhost:8080/sessions/cycledevie. Elle a trois paramètres d'initialisation :


urlSessionValide

url de la page qui présente les caractéristiques de la session en cours


urlSessionInvalide

url de la page présentée après une invalidation de la session en cours


urlErreur

url de la page présentée en cas d'erreur d'initialisation de la servlet principale cycledevie

Les composantes de l'application sessions sont les suivantes :


cycledevie

servlet principale - analyse la requête du client :

  • si celle-ci fait partie d'une session, passe la main à la page valide.jsp qui va afficher les caractéristiques de cette session. A partir de cette page, l'utilisateur peut :
    • la recharger
    • l'invalider
  • si la requête demande à invalider la session en cours, la servlet passe la main à la page invalide.jsp qui va proposer à l'utilisateur de recréer une nouvelle session
  • si lors de son initialisation, la servlet rencontre des erreurs, elle passe la main à la page erreur.jsp qui affichera un message d'erreur.

valide.jsp
  • affiche les caractéristiques de la session en cours et propose deux liens :
    • un pour recharger la page et voir ainsi évoluer le paramètre du dernier accès à la session courante
    • l'autre pour invalider la session en cours

invalide.jsp

affichée lorsque l'utilisateur a invalidé la session en cours. Propose alors d'en recréer une nouvelle.


erreur.jsp

affichée lorsque la servlet principale rencontre des erreurs lors de son initialisation.

La servlet principale cycledevie est la suivante :

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

public class cycledevie extends HttpServlet{

    // variables d'instance
    String msgErreur=null;
    String urlSessionInvalide=null;
    String urlSessionValide=null;
    String urlErreur=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
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

        // on récupère la session en cours
        HttpSession session=request.getSession();

        // on analyse l'action à faire
        String action=request.getParameter("action");
        // invalider la session courante
        if(action!=null && action.equals("invalider")){
            // on invalide la session courante
            session.invalidate();
            // on passe la main à l'url urlSessionInvalide
            getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
        }
        // autres cas
        // on passe la main à l'url urlSessionInvalide
        getServletContext().getRequestDispatcher(urlSessionValide).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();
        urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
        urlSessionValide=config.getInitParameter("urlSessionValide");
        urlErreur=config.getInitParameter("urlErreur");

        // paramètres ok ?
        if(urlSessionValide==null || urlSessionInvalide==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

On notera les points suivants :

  • dans sa méthode d'initialisation, la servlet récupère ses trois paramètres
  • dans le traitement (doGet) d'une requête, la servlet :
    • vérifie tout d'abord qu'il n'y a pas eu d'erreur lors de l'initialisation. S'il y en a eu, elle passe la main à la page erreur.jsp.
    • vérifie la valeur du paramètre action. Si ce dernier a la valeur "invalider", la servlet passe la main à la page invalide.jsp sinon à la page valide.jsp.

La page JSP valide.jsp d'affichage des caractéristiques de la session courante :

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

<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- début de la page HTML -->
  <html>
    <meta http-equiv="pragma" content="no-cache">
    <head>
        <title>Cycle de vie d'une session</title>
    </head>
    <body>
        <h3>Cycle de vie d'une session</h3>
        <hr>
        <br>Etat session : <%= etat %>
      <br>ID session : <%= session.getId() %>
      <br>Heure de création : <%= new Date(session.getCreationTime()) %>
      <br>Heure du dernier accès : <%= new Date(session.getLastAccessedTime()) %>
      <br>Intervalle maximum d'inactivité : <%= session.getMaxInactiveInterval() %>
      <br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
      <br><a href="/sessions/cycledevie">Recharger la page</a>
    <body>
  </html>

On notera que dans la ligne

  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";

on utilise un objet session venant de nulle part. En fait cet objet fait partie des objets implicites mis à la disposition des pages JSP comme le sont les objets request, response, out, config (ServletConfig), context (ServletContext) déjà rencontrés. Les deux liens de la page référencent la servlet cycledevie présentée précédemment :

      <br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
      <br><a href="/sessions/cycledevie">Recharger la page</a>

Le lien pour invalider la session comprend le paramètre action=invalider qui permettra à la servlet cycledevie de reconnaître le fait que l'utilisateur veut invalider la session courante. L'autre lien permet de recharger la page. Pour que le navigateur n'aille pas chercher celle-ci dans un cache, la directive HTML :

    <meta http-equiv="pragma" content="no-cache">

a été utilisée. Elle indique au navigateur de ne pas utiliser de cache pour la page qu'il reçoit.

La page invalide.jsp est la suivante :

<!-- début de la page HTML -->
<html>
  <head>
    <title>Cycle de vie d'une session</title>
  </head>
  <body>
    <h3>Cycle de vie d'une session</h3>
    <hr>
    Votre session a été invalidée
    <a href="/sessions/cycledevie">Créer une nouvelle session</a>
  </body>
</html>

Elle offre un lien pointant sur la servlet cycledevie sans le paramètre action. Ce lien amènera la servlet cycledevie à créer une nouvelle session.

La page erreur.jsp est la suivante :

<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- début de la page HTML -->
<html>
  <head>
    <title>Cycle de vie d'une session</title>
  </head>
  <body>
    <h3>Cycle de vie d'une session</h3>
    <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

Elle a pour rôle d'afficher le message d'erreur que lui a transmis la servlet cycledevie. Voyons maintenant des exemples d'exécution. La servlet est demandée une première fois :

Image

La page ci-dessus indique qu'on est dans une nouvelle session. On utilise le lien "Recharger la page" :

Image

Le résultat précédent indique qu'on est toujours dans la même session que dans la page précédente (même ID). On remarquera que l'heure du dernier accès à cette session a changé. Maintenant utilisons le lien "Invalider la session" :

Image

On remarquera l'URL de cette nouvelle page avec le paramètre action=invalider. Utilisons le lien "Créer une nouvelle session" pour créer une nouvelle session :

Image

On remarque qu'une nouvelle session a démarré. Dans les exemples précédents, la session s'appuie sur le mécanisme des cookies. Inhibons maintenant l'utilisation des cookies sur notre navigateur et refaisons les tests. Les exemples suivants ont été réalisés avec Netscape Communicator. Pour une raison inexpliquée les tests réalisés avec IE6 donnaient des résultats inattendus comme si IE6 continuait à utiliser des cookies alors que ceux-ci avaient été désactivés. La servlet cycledevie est demandée une première fois :

Image

Nous utilisons maintenant le lien "Recharger la page" :

Image

On peut voir deux choses :

  • l'ID de la session a changé
  • la servlet détecte la session comme une nouvelle session

Le serveur Tomcat apporte une solution au problème des utilisateurs qui inhibent l'utilisation des cookies sur leur navigateur. Il utilise deux mécanismes pour implémenter le jeton dont on a parlé au début de ce paragraphe : les cookies et la réécriture d'URL. Si le cookie de session n'est pas disponible, il essaiera d'obtenir le jeton à partir de l'URL demandée par le client. Pour cela, il faut que celle-ci contienne le jeton. De façon générale, il faut que tous les liens générés dans un document HTML vers l'application web contiennent le jeton de celle-ci. Cela peut se faire avec la méthode encodeURL :


String [HttpResponse].encodeURL(String URL)

ajoute le jeton de la session encours à l'URL passée en paramètre sous la forme URL;jsessionid=xxxx

Nous modifions notre application de la façon suivante :

  • dans la servlet cycledevie.java les URL sont encodées :
            // on passe la main à la page d'erreur
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
            // on passe la main à l'url urlSessionInvalide
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
        // on passe la main à l'url urlSessionInvalide
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • dans la page valide.jsp les URL sont encodées :
<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
  // encodage URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");  
%>
............
      <br><a href="<%= URLcycledevie %>?action=invalider">Invalider la session</a>
      <br><a href="<%= URLcycledevie %>">Recharger la page</a>
  • dans la page invalide.jsp les URL sont encodées :
<%
    // jspservice - on invalide la session en cours
  session.invalidate();
  // encodage URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

Maintenant nous sommes prêts pour les tests. Nous utilisons Netscape 4.5 et les coookies ont été inhibés. Nous demandons une première fois la servlet cycledevie :

Image

et nous rechargeons la page avec le lien "Recharger la page" :

Image

Nous pouvons voir que :

  • la session n'a pas changé (même ID)
  • l'URL de la servlet cycledevie contient bien le jeton comme le montre le champ Adresse ci-dessus
  • le serveur Tomcat récupère donc le jeton de session dans l'URL demandée (si le développeur a pris soin d'encoder celle-ci).

4.4. Exemple 2

Nous présentons maintenant un exemple montrant comment stocker des informations dans la session d'un client. Ici l'unique information sera un compteur qui sera incrémenté à chaque fois que l'utilisateur appellera l'URL de la servlet. Lorsque celle-ci est appelée la première fois, on a la page suivante :

Image

Si on clique sur le lien "Recharger la page" ci-dessus, on obtient la nouvelle page suivante :

Image

L'application a trois composantes :

  • une servlet qui traite la requête du client
  • une page jsp qui affiche la valeur du compteur
  • une page jsp qui affiche une éventuelle erreur

Ces trois composantes sont installées dans l'application web sessions déjà utilisée. Le fichier web.xml de celle-ci a été modifié pour configurer les nouvelles servlets :

<?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>compteur</servlet-name>
    <servlet-class>compteur</servlet-class>
    <init-param>
        <param-name>urlAffichageCompteur</param-name>
        <param-value>/compteur.jsp</param-value>
    </init-param>
    <init-param>
        <param-name>urlErreur</param-name>
        <param-value>/erreurcompteur.jsp</param-value>
    </init-param>
  </servlet>
...
  <servlet-mapping>
    <servlet-name>compteur</servlet-name>
    <url-pattern>/compteur</url-pattern>
  </servlet-mapping>
</web-app>
  • la servlet s'appelle compteur (servlet-name) et est liée au fichier classe compteur.class (servlet-class)
  • elle a deux paramètres d'initialisation :
    • urlAffichageCompteur : URL de la page JSP d'affichage du compteur
    • urlErreur : URL de la page JSP d'affichage d'une éventuelle erreur
  • et un alias /compteur qui fait qu'on l'appellera via l'URL http://localhost:8080/sessions/compteur

La servlet compteur.java est la suivante :

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

public class compteur extends HttpServlet{

    // variables d'instance
    String msgErreur=null;
    String urlAffichageCompteur=null;
    String urlErreur=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
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

        // on récupère la session en cours
        HttpSession session=request.getSession();
        // et le compteur
        String compteur=(String)session.getAttribute("compteur");
        if(compteur==null) compteur="0";
        // incrémentation du compteur
        try{
            compteur=""+(Integer.parseInt(compteur)+1);
        }catch(Exception ex){}
        // mémorisation compteur dans la session
        session.setAttribute("compteur",compteur);
        // et dans la requête
        request.setAttribute("compteur",compteur);

        // on passe la main à l'url d'affichage du compteur
        getServletContext().getRequestDispatcher(urlAffichageCompteur).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();
        urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
        urlErreur=config.getInitParameter("urlErreur");

        // paramètres ok ?
        if(urlAffichageCompteur==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Cette servlet a la structure des servlets déjà rencontrées. On notera simplement la gestion du compteur :

  • la session est récupérée via request.getSession()
  • le compteur est récupéré dans cette session via session.getAttribute("compteur")
  • si on récupère une valeur null, c'est que la session vient de commencer. Le compteur est alors mis à 0.
  • le compteur est incrémenté, remis dans la session (session.setAttribute("compteur",compteur)) et placé dans la requête qui va être passée à la servlet d'affichage (request.setAttribute("compteur",compteur)).

La page d'affichage compteur.jsp est la suivante :

<%
    // jspService
  // on récupère le compteur
  String compteur= (String) request.getAttribute("compteur");
  if(compteur==null) compteur="inconnu";
%>
<!-- début de la page HTML -->
<html>
  <head>
    <title>Comptage au fil d'une session</title>
  </head>
  <body>
    <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
    <hr>
    compteur = (<%= compteur %>)
    <br><a href="/sessions/compteur">Recharger la page</a>
  </body>
</html>

La page ci-dessus se contente de récupérer l'attribut compteur (request.getAttribute("compteur")) que lui a passé la servlet principale et l'affiche.

La page d'erreur erreurcompteur.jsp est la suivante :

<%
    // jspService
  // une erreur s'est produite
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- début de la page HTML -->
<html>
  <head>
    <title>Comptage au fil d'une session</title>
  </head>
  <body>
    <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
    <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

4.5. Exemple 3

Nous nous proposons d'écrire une application java qui serait cliente de l'application compteur précédente. Elle l'appellerait N fois de suite où N serait passé en paramètre. Notre but est de montrer un client web programmé et la façon de gérer les cookies. Notre point de départ sera un client web générique présenté dans le polycopié Java du même auteur. Il est appellé de la façon suivante :

clientweb URL GET/HEAD

  • URL : url demandée
  • GET/HEAD : GET pour demander le code HTML de la page, HEAD pour se limiter aux seuls entêtes HTTP

Voici un exemple avec l'URL http://localhost:8080/sessions/compteur :


E:\data\serge\JAVA\SOCKETS\client web>java clientweb http://localhost:8080/sessions/compteur GET

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 14:21:18 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=B8A9076E552945009215C34A97A0EC5D;Path=/sessions


<!-- début de la page HTML -->
<html>
  <head>
        <title>Comptage au fil d'une session</title>
  </head>
  <body>
        <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
        <hr>
    compteur = (1)
    <br><a href="/sessions/compteur">Recharger la page</a>
  </body>
</html>

Le programme clientweb affiche tout ce qu'il reçoit du serveur. On voit ci-dessus la commande HTTP Set-cookie avec laquelle le serveur envoie un cookie à son client. Ici le cookie contient deux informations :

  • JSESSIONID qui est le jeton de la session
  • Path qui définit l'URL à laquelle appartient le cookie. Path=/sessions indique au navigateur qu'il devra renvoyer le cookie au serveur à chaque fois qu'il demandera une URL commençant par /sessions. Dans l'application sessions, nous avons utilisé différentes servlets dont les servlets /sessions/cycledevie et /sessions/compteur. Si on appelle la servlet /sessions/cycledevie le navigateur va recevoir un jeton J. Si avec ce même navigateur, on appelle ensuite la servlet /sessions/compteur, le navigateur va renvoyer au serveur le jeton J car celui-ci concerne toutes les URL commençant par /sessions. Dans notre exemple, les servlets cycledevie et compteur n'ont pas à partager le même jeton de session. Elles n'auraient donc pas du être mises dans la même application web. C'est un point à retenir : toutes les servlets d'une même application partagent le même jeton de session.
  • un cookie peut définir également une durée de validité. Ici cette information est absente. Le cookie sera donc détruit à la fermeture du navigateur. Un cookie peut avoir une durée de validité de N jours par exemple. Tant qu'il est valide, le navigateur le renverra à chaque fois que l'une des URL de son domaine (Path) sera consultée. Prenons un site de vente en ligne de CD. Celui-ci peut suivre le cheminement de son client dans son catalogue et déterminer peu à peu ses préférences : la musique classique par exemple. Ces préférences peuvent être rangées dans un cookie ayant une durée de vie de 3 mois. Si ce même client revient au bout d'un mois sur le site, le navigateur renverra le cookie à l'application serveur. Celle-ci d'après les informations renfermées dans le cookie pourra alors adapter les pages générées aux préférences de son client.

Le code du client web suit. Il sera ultérieurement le point de départ d'un autre client.

// paquetages importés
import java.io.*;
import java.net.*;

public class clientweb{

    // demande une URL
    // affiche le contenu de celle-ci à l'écran

    public static void main(String[] args){
        // syntaxe
        final String syntaxe="pg URI GET/HEAD";

        // nombre d'arguments
        if(args.length != 2)
            erreur(syntaxe,1);

        // on note l'URI demandée
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // vérification validité de l'URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
            // URI incorrecte
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
        // vérification de la commande
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // commande incorrecte
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

        // on extrait les infos utiles de l'URL
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

        // on peut travailler
        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
        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(commande + " " + path + query + " HTTP/1.1");   
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
            // on lit la réponse
            while((réponse=IN.readLine())!=null){
                // on traite la réponse
                System.out.println(réponse);
            }//while
            // c'est fini
            client.close();
        } catch(Exception e){
            // on gère l'exception
            erreur(e.getMessage(),4);
        }//catch
    }//main

    // affichage des erreurs
    public static void erreur(String msg, int exitCode){
        // affichage erreur
        System.err.println(msg);
        // arrêt avec erreur
        System.exit(exitCode);
    }//erreur
}//classe

Nous créons maintenant le programme clientCompteur appelé de la façon suivante :

clientCompteur URL N [JSESSIONID]

  • URL : url de la servlet compteur
  • N : nombre d'appels à faire à cette servlet
  • JSESSIONID : paramètre facultatif - jeton d'une session

Le but du programme est d'appelet N fois la servlet compteur en gérant le cookie de session et en affichant à chaque fois la valeur du compteur renvoyée par le serveur. A la fin des N appels, la valeur de celui-ci doit être N. Voici un premier exemple d'exécution :


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/compteur 3
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A;Path=/sessions
cookie trouvÚ : 92DB3808CE8FCB47D47D997C8B52294A

compteur : 1

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 2

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 3

Le programme affiche :

  • les entêtes HTTP qu'il envoie au serveur sous la forme -->
  • les entêtes HTTP qu'il reçoit
  • la valeur du compteur après chaque appel

On voit que lors du premier appel :

  • le client n'envoie pas de cookie
  • le serveur en envoie un

Pour les appels suivants :

  • le client renvoie systématiquement le cookie qu'il a reçu du serveur lors du 1er appel. C'est ce qui va permettre au serveur de le reconnaître et d'incrémenter son compteur.
  • le serveur lui ne renvoie plus de cookie

Nous relançons le programme précédent en passant le jeton ci-dessus comme troisième paramètre :


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/compteur 3 92DB3808CE8FCB47D47D997C8B52294A

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 4

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 5

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 6

On voit ici que dès le 1er appel du client, le serveur reçoit un cookie de session valide. Il faut savoir que pour Tomcat la durée d'inactivité maximale d'une session est par défaut de 20 mn (c'est en fait configurable). Si le second appel du programme envoie assez vite le cookie reçu lors du premier appel, pour le serveur il s'agit alors de la même session. On pointe ici un trou de sécurité potentiel. Si je suis capable d'intercepter sur le réseau un jeton de session, je suis alors capable de me faire passer pour celui qui avait initié celle-ci. Dans notre exemple, le premier appel représente celui qui initie la session (peut-être avec un login et mot de passe qui vont lui donner le droit d'avoir un jeton) et le second appel représente celui qui a "piraté" le jeton de session du premier appel. Si l'opération en cours est une opération bancaire cela peut devenir très ennuyeux...

Le code du client est le suivant :

// paquetages importés
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientCompteur{

    // demande une URL
    // affiche le contenu de celle-ci à l'écran

    public static void main(String[] args){
        // syntaxe
        final String syntaxe="pg URL-COMPTEUR N [JSESSIONID]";

        // nombre d'arguments
        if(args.length !=2 && args.length != 3)
            erreur(syntaxe,1);

        // on note l'URL demandée
        String URLString=args[0];

        // vérification validité de l'URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
            // URI incorrecte
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
        // vérification du nombre d'appels N
        int N=0;
        try{
            N=Integer.parseInt(args[1]);
            if(N<=0) throw new Exception();
        }catch(Exception ex){
            // argument N incorrect
            erreur("Le nombre d'appels N doit être un entier >0",3);
        }
        // le jeton JSESSIONID a-t-il été passé en paramètre ?
        String JSESSIONID="";
        if (args.length==3) JSESSIONID=args[2];

        // on extrait les infos utiles de l'URL
        String path=url.getPath();
        if(path.equals("")) path="/";
        String query=url.getQuery();
        if(query!=null) query="?"+query; else query="";
        String host=url.getHost();
        int port=url.getPort();
        if(port==-1) port=url.getDefaultPort();

        // on peut travailler
        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 recherché dans le code HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
        // le résultat de la comparaison au modèle
        Matcher résultat=null;
        // un booléen donnant le résultat de la recherche du compteur
        boolean compteurTrouvé;

        try{
            // on fait les N appels au serveur
            for(int i=0;i<N;i++){
                // 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
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

                // on lit la réponse jusqu'à la fin des entêtes en cherchant l'éventuel cookie
                while((réponse=IN.readLine())!=null){
                    // suivi réponse
                    System.out.println(réponse);
                    // 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
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while

                // c'est fini pour les entêtes HTTP - on passe au code HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                    // la ligne courante contient-elle le compteur ?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // on a trouvé le compteur - on l'affiche
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while
                // c'est fini
                client.close();
            }//for
        } catch(Exception e){
            // on gère l'exception
            erreur(e.getMessage(),4);
        }//catch
    }//main

    // affichage des erreurs
    public static void erreur(String msg, int exitCode){
        // affichage erreur
        System.err.println(msg);
        // arrêt avec erreur
        System.exit(exitCode);
    }//erreur

    // suivi échanges client-serveur
    public static void envoie(PrintWriter OUT,String msg){
        // envoie message au serveur
        OUT.println(msg);
        // suivi écran
        System.out.println("--> "+msg);
    }//erreur
}//classe

Décortiquons les points importants de ce programme :

  • on doit faire N échanges client-serveur. C'est pourquoi ceux-ci sont dans une boucle
            for(int i=0;i<N;i++){
  • à chaque échange, le client ouvre une connexion TCP-IP avec le serveur. Une fois celle-ci obtenue, il envoie au serveur les entêtes HTTP de sa requête :
                // on demande l'URL - envoi des entêtes HTTP
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

Si le jeton JSESSIONID est disponible, il est envoyé sous la forme d'un cookie, sinon il ne l'est pas.

  • Une fois sa requête envoyée, le client attend la réponse du serveur. Il commence par exploiter les entêtes HTTP de cette réponse à la recherche d'un éventuel cookie. Pour le trouver, il compare les lignes qu'il reçoit à l'expression régulière du cookie :
        // le modèle recherché dans les entêtes HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                // on lit la réponse jusqu'à la fin des entêtes en cherchant l'éventuel cookie
                while((réponse=IN.readLine())!=null){
                    // suivi réponse
                    System.out.println(réponse);
                    // 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
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • lorsque le jeton aura été trouvé une première fois, il ne sera plus cherché lors des appels suivants au serveur. Lorsque les entêtes HTTP de la réponse ont été traités, on passe au code HTML de cette même réponse. Dans celle-ci, on cherche la ligne qui donne la valeur du compteur. Cette recherche est faite là aussi avec une expression régulière :
        // le modèle du compteur recherché dans le code HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                // c'est fini pour les entêtes HTTP - on passe au code HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                    // la ligne courante contient-elle le compteur ?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // on a trouvé le compteur - on l'affiche
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. Exemple 4

Dans l'exemple précédent, le client web renvoie le jeton sous la forme d'un cookie. Nous avons vu qu'il pouvait aussi le renvoyer au sein même de l'URL demandée sous la forme URL;jsessionid=xxx. Vérifions-le. Le programme clientCompteur.java est transformé en clientCompteur2.java et modifié de la façon suivante :

....
                // on demande l'URL - envoi des entêtes HTTP
                if(JSESSIONID.equals(""))
                    envoie(OUT,"GET " + path + query + " HTTP/1.1");
                else envoie(OUT,"GET " + path + query + ";jsessionid=" + JSESSIONID + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                envoie(OUT,"Connection: close");
                envoie(OUT,"");
....

Le client demande donc l'URL du compteur par GET URL;jsessionid=xx HTTP/1.1 et n'envoie plus de cookie. C'est la seule modification. Voici les résultats d'un premier appel :


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/compteur 2

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=48A6DBA8357D808EC012AAF3A2AFDA63;Path=/sessions
cookie trouvÚ : 48A6DBA8357D808EC012AAF3A2AFDA63

compteur : 1

--> GET /sessions/compteur;jsessionid=48A6DBA8357D808EC012AAF3A2AFDA63 HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 2

Lors du premier appel le client demande l'URL sans jeton de session. Le serveur lui répond en lui envoyant le jeton. Le client réinterroge alors la même URL en adjoignant le jeton reçu à celle-ci. On voit que le compteur est bien incrémenté preuve que le serveur a bien reconnu qu'il s'agissait de la même session.

4.7. Exemple 5

Cet exemple montre une application composée de trois pages qu'on appellera page0, page1 et page2. L'utilisateur doit les obtenir dans cet ordre :

  • page0 est un formulaire demandant une information : un nom
  • page1 est un formulaire obtenu en réponse à l'envoi du formulaire de page0. Il demande une seconde information : un age
  • page2 est un document HTML qui affiche le nom obtenu par page0 et l'âge obtenu par page1.

Il y a là trois échanges client-serveur :

  • au premier échange le formulaire page0 est demandé par le client et envoyé par le serveur
  • au second échange le formulaire page1 est demandé par le client et envoyé par le serveur. Le client envoie le nom au serveur.
  • au troisième échange le document page3 est demandé par le client et envoyé par le serveur. Le client envoie l'âge au serveur. Le document page3 doit afficher le nom et l'âge. Le nom a été obtenu par le serveur au second échange et "oublié" depuis. On utilise une session pour enregistrer le nom à l'échange 2 afin qu'il soit disponible lors de l'échange 3.

La page page0 obtenue au premier échange est la suivante :

Image

On remplit le champ du nom :

Image

On utilise le bouton Suite et on obtient alors la page page1 suivante :

Image

On remplit le champ de l'âge :

Image

On utilise le bouton Suite et on obtient alors la page page2 suivante :

Image

Lorsqu'on soumet la page page0 au serveur, celui-ci peut la renvoyer avec un code d'erreur si le nom est vide :

Image

Lorsqu'on soumet la page page1 au serveur, celui-ci peut la renvoyer avec un code d'erreur si l'âge est invalide :

Image

L'application est composée d'une servlet et de quatre pages JSP :


page0.jsp

affiche page0


page1.jsp

affiche page1


page2.jsp

affiche page2


erreur.jsp

affiche une page d'erreur

L'application web s'appelle suitedepages et est configurée comme suit dans le fichier server.xml de Tomcat :

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

Le fichier de configuration web.xml de l'application suitedepages 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>urlPage0</param-name>
        <param-value>/page0.jsp</param-value>
    </init-param>
    <init-param>
        <param-name>urlPage1</param-name>
        <param-value>/page1.jsp</param-value>
    </init-param>
    <init-param>
        <param-name>urlPage2</param-name>
        <param-value>/page2.jsp</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 grâce à son alias (servlet-mapping) est accessible via l'URL http://localhost:8080/suitedepages/main. Elle a quatre paramètres d'initialisation qui sont les URL des quatre pages JSP utilisées pour les différents affichages. Le code de la servlet main est le suivant :

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

public class main extends HttpServlet{

    // variables d'instance
    String msgErreur=null;
    String urlPage0=null;
    String urlPage1=null;
    String urlPage2=null;
    String urlErreur=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
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }
        // on récupère le paramètre étape
        String étape=request.getParameter("etape");
        // on récupère la session en cours
        HttpSession session=request.getSession();
        // on traite l'étape en cours
        if(étape==null) étape0(request,response,session);
        if(étape.equals("1")) étape1(request,response,session);
        if(étape.equals("2")) étape2(request,response,session);
        // autres cas sont invalides
        étape0(request,response,session);
    }

    //-------- 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();
        urlPage0=config.getInitParameter("urlPage0");
        urlPage1=config.getInitParameter("urlPage1");
        urlPage2=config.getInitParameter("urlPage2");
        urlErreur=config.getInitParameter("urlErreur");

        // paramètres ok ?
        if(urlPage0==null || urlPage1==null || urlPage2==null){
            msgErreur="Configuration incorrecte";
        }
    }

    //-------- étape0
    public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // on fixe quelques attributs
        request.setAttribute("nom","");
        // on présente la page 0
        request.getRequestDispatcher(urlPage0).forward(request,response);
    }

    //-------- étape1
    public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // on récupère le nom dans la requête
        String nom=request.getParameter("nom");
        // nom positionné ?
        if(nom==null) étape0(request,response,session);
        // on enlève les éventuels espaces du nom
        nom=nom.trim();
        // on le met dans un attribut de la requête
        request.setAttribute("nom",nom);
        // nom vide ?
        if(nom.equals("")){
            // c'est une erreur
            ArrayList erreurs=new ArrayList();
            erreurs.add("Nous n'avez pas indiqué de nom");
            // on met les erreurs dans la requête
            request.setAttribute("erreurs",erreurs);
            // retour à la page 0
            étape0(request,response,session);
        }
        // nom valide - on le mémorise dans la session en cours
        session.setAttribute("nom",nom);
        // on fixe l'attribut age dans la requête
        request.setAttribute("age","");
        // on présente la page 1
        request.getRequestDispatcher(urlPage1).forward(request,response);
    }

    //-------- étape2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // on récupère le nom dans la session
        String nom=(String)session.getAttribute("nom");
        // nom positionné ?
        if(nom==null) étape0(request,response,session);
        // on le met dans un attribut de la requête
        request.setAttribute("nom",nom);
        // on récupère l'âge dans la requête
        String age=request.getParameter("age");
        // age positionné ?
        if(age==null){
            // retour à la page 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
        // on mémorise l'âge dans la requête
        age=age.trim();
        request.setAttribute("age",age);
        // age valide ?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // c'est une erreur
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // on met les erreurs dans la requête
            request.setAttribute("erreurs",erreurs);
            // retour à la page 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
        // age valide - on présente la page 2
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • la méthode init récupère les quatre paramètres d'initialisation et positionne un message d'erreur si l'un d'eux est manquant
  • nous avons vu que la requête comprenait trois échanges. Pour savoir où on en est dans ceux-ci, les formulaires page0 et page1 ont une variable cachée etape qui a la valeur 1 (page0) ou 2 (page1). On pourrait ici voir ce numéro comme le numéro de page suivante à afficher. Dans la méthode doGet, ce paramètre est récupéré dans la requête et selon sa valeur le traitement est délégué à trois autres méthodes :
    • étape0 traite la requête initiale et envoie page0
    • étape1 traite le formulaire de page0 et envoie page1 ou de nouveau page0 s'il y a eu erreur
    • étape2 traite le formulaire de page1 et envoie page2 ou de nouveau page1 s'il y a eu erreur
  • étape0
    • affiche page0 avec un nom vide
  • étape1
    • récupère le paramètre nom du formulaire de page0.
    • vérifie que le nom existe (pas null). Si ce n'est pas le cas on affiche de nouveau page0 comme si c'était le premier appel.
    • vérifie que le nom est non vide. Si ce n'est pas le cas on affiche de nouveau page0 avec un message d'erreur.
    • mémorise le nom dans la session courante et affiche page1 si le nom est valide.
  • étape2
    • récupère le paramètre nom dans la session courante.
    • vérifie que le nom existe (pas null). Si ce n'est pas le cas on affiche de nouveau page0 comme si c'était le premier appel.
    • récupère le paramètre age dans la requête courante envoyée par page1.
    • vérifie que l'âge est valide. Si ce n'est pas le cas on affiche de nouveau page1 avec un message d'erreur.
    • mémorise le nom et l'âge comme attributs de requête et affiche page2 si le nom et l'âge sont valides.

La page page0.jsp est la suivante :

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

<% // page0.jsp
    // on récupère les attributs de la requête
  String nom=(String)request.getAttribute("nom");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // attributs valides ?
  if(nom==null){
    // retour à la servlet principale
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  

<html>
  <head>
    <title>page 0</title>
  </head>
  <body>
    <h3>Page 0/2</h3>
    <form name="frmNom" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="1">
      <table>
        <tr>
          <td>Votre nom</td>
          <td><input type="text" name="nom" value="<%= nom %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // erreurs ?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>

  • la page page0.jsp peut être appelée par la servlet principale dans deux cas :
    • lors de la requête initiale
    • après traitement du formulaire de page0 lorsqu'il y a une erreur
  • le paramètre nom à afficher lui est donné par la servlet principale ainsi que l'éventuelle liste d'erreurs. La servlet page0.jsp commence donc par récupérer ces deux informations.
  • le formulaire est "posté" à la servlet principale avec le champ caché (hidden) etape qui indique à quelle étape de l'application on se trouve.

La page page1.jsp est la suivante :

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

<% // page1.jsp
    // on récupère les attributs de la requête
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // attributs valides ?
  if(nom==null || age==null){
    // retour à la servlet principale
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  

<html>
  <head>
    <title>page 1</title>
  </head>
  <body>
    <h3>Page 1/2</h3>
    <form name="frmAge" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="2">    
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><%= nom %></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><input type="text" name="age" size="3" value="<%= age %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // erreurs ?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>

La page page1.jsp a une structure analogue à celle de la page page0.jsp au détail près qu'elle reçoit maintenant deux attributs de la servlet principale : nom et age. Enfin la page page2.jsp est la suivante :

<% 
    // page2.jsp
    // on récupère les attributs de la requête
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  // attributs valides ?
  if(nom==null || age==null){
    // retour à la servlet principale
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  


<html>
  <head>
    <title>page 2</title>
  </head>
  <body>
    <h3>Page 2/2</h3>
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><%= nom %></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><font color="green"><%= age %></font></td>
        </tr>
      </table>
  </body>
</html>

La page page2.jsp reçoit elle aussi les attributs nom et age de la servlet principale. Elle se contente de les afficher. Pour terminer la page erreur.jsp chargée d'afficher une erreur en cas d'initialisation incorrecte de la servlet est la suivante :

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

Elle affiche l'attribut msgErreur que lui a passé la servlet principale.

En conclusion, on pourra remarquer qu'au cours des trois étapes de l'application, c'est toujours la servlet principale qui est interrogée en premier par le navigateur. Mais ce n'est pas elle qui génère la réponse à afficher mais l'une des quatre pages JSP. L'utilisateur ne voit pas ce point, le navigateur continuant à afficher dans son champ "Adresse" l'URL initialement demandée donc celle de la servlet principale.