Skip to content

4. Monitoraggio delle sessioni

4.1. Il problema

Un'applicazione web può consistere in diversi scambi di moduli tra il server e il client. Il processo funziona come segue:

  • Passo 1
    • Il client C1 apre una connessione con il server ed effettua la sua richiesta iniziale.
    • Il server invia il modulo F1 al client C1 e chiude la connessione aperta nel passo 1.
  • Fase 2
    • Il client C1 lo compila e lo rinvia al server. Per farlo, il browser apre una nuova connessione con il server.
    • Il server elabora i dati del modulo 1, ne ricava l'informazione I1, invia il modulo F2 al client C1 e chiude la connessione aperta nel passaggio 3.
  • Fase 3
    • Il ciclo dei passaggi 3 e 4 si ripete nei passaggi 5 e 6. Al termine del passaggio 6, il server avrà ricevuto due moduli, F1 e F2, e avrà calcolato le informazioni I1 e I2 da essi.

Il problema in questione è: in che modo il server tiene traccia delle informazioni I1 e I2 associate al client C1? Questo problema è chiamato tracciamento della sessione del client C1. Per comprenderne la causa principale, esaminiamo il diagramma di un'applicazione server TCP-IP che serve più client contemporaneamente:

In una classica applicazione client-server TCP-IP:

  • il client stabilisce una connessione con il server
  • scambia dati con il server tramite questa connessione
  • la connessione viene chiusa da una delle due parti

I due punti chiave di questo meccanismo sono:

  1. viene creata una singola connessione per ogni client
  2. questa connessione viene utilizzata per l'intera durata del dialogo tra il server e il suo client

Ciò che permette al server di sapere in ogni momento con quale client sta interagendo è la connessione — o, in altre parole, il "canale" — che lo collega al suo client. Poiché questo canale è dedicato a un client specifico, tutto ciò che passa attraverso questo canale proviene da quel client, e tutto ciò che viene inviato attraverso questo canale raggiunge il client.

Il meccanismo client-server HTTP segue da vicino il modello precedente, con l'eccezione che il dialogo client-server è limitato a un singolo scambio tra il client e il server:

  • il client apre una connessione al server ed effettua la sua richiesta
  • il server invia la sua risposta e chiude la connessione

Se al tempo T1, un client C effettua una richiesta al server, ottiene una connessione C1 che verrà utilizzata per il singolo scambio richiesta-risposta. Se, al tempo T2, questo stesso client effettua una seconda richiesta al server, otterrà una connessione C2 diversa dalla connessione C1. Per il server, non vi è quindi alcuna differenza tra questa seconda richiesta dell’utente C e la sua richiesta iniziale: in entrambi i casi, il server tratta il cliente come un nuovo cliente. Affinché vi sia un collegamento tra le diverse connessioni del cliente C al server, il cliente C deve essere “riconosciuto” dal server come un “cliente abituale” e il server deve recuperare le informazioni in suo possesso su tale cliente abituale.

Immaginiamo un sistema che funziona come segue:

  • C'è una sola coda
  • Ci sono diversi sportelli di servizio. Ciò significa che più clienti possono essere serviti contemporaneamente. Quando uno sportello si libera, un cliente lascia la fila per essere servito a quello sportello
  • Se questa è la prima visita del cliente, l’addetto allo sportello gli consegna un biglietto con un numero. Il cliente può porre una sola domanda. Una volta ricevuta la risposta, deve lasciare lo sportello e tornare in fondo alla fila. L’addetto allo sportello registra le informazioni del cliente in un file contrassegnato dal numero.
  • Quando è di nuovo il suo turno, il cliente potrebbe essere servito da un addetto diverso rispetto alla volta precedente. L'addetto chiede il suo gettone e recupera il fascicolo con il numero del gettone. Ancora una volta, il cliente fa una richiesta, riceve una risposta e le informazioni vengono aggiunte al suo fascicolo.
  • E così via... Nel corso del tempo, il cliente riceverà le risposte a tutte le sue richieste. Il tracciamento tra le diverse richieste avviene tramite il gettone e la scheda ad esso associata.

Il meccanismo di tracciamento delle sessioni in un'applicazione web client-server funziona in modo simile:

  • Al momento della prima richiesta, al client viene rilasciato un token dal server web
  • presenterà questo token ad ogni richiesta successiva per identificarsi

Il token può assumere varie forme:

  • un campo nascosto in un modulo
    • il client effettua la sua prima richiesta (il server la riconosce perché il client non ha alcun token)
    • Il server invia la sua risposta (un modulo) e inserisce il token in un campo nascosto al suo interno. A questo punto, la connessione viene chiusa (il client esce dalla sessione con il proprio token). Il server potrebbe avere delle informazioni associate a questo token.
    • Il client effettua una seconda richiesta inviando nuovamente il modulo. Il server recupera il token dal modulo. Può quindi elaborare la seconda richiesta del client accedendo, tramite il token, alle informazioni calcolate durante la prima richiesta. Nuove informazioni vengono aggiunte al file associato al token, una seconda risposta viene inviata al client e la connessione viene chiusa per la seconda volta. Il token è stato reinserito nel modulo di risposta in modo che l'utente possa presentarlo durante la sua prossima richiesta.
    • e così via...

Lo svantaggio principale di questa tecnica è che il token deve essere inserito in un modulo. Se la risposta del server non è un modulo, il metodo del campo nascosto non può più essere utilizzato.

  • Il metodo del cookie
    • Il client effettua la sua prima richiesta (il server la riconosce perché il client non ha alcun token)
    • Il server risponde aggiungendo un cookie alle intestazioni HTTP. Ciò avviene utilizzando il comando HTTP Set-Cookie:

Set-Cookie: param1=valore1;param2=valore2;....

dove param1, param2, ecc. sono i nomi dei parametri e i valori corrispondenti. Tra i parametri ci sarà il token. Molto spesso, nel cookie viene incluso solo il token, mentre il server memorizza le altre informazioni nella cartella associata al token. Il browser che riceve il cookie lo memorizzerà in un file sul disco. Dopo la risposta del server, la connessione viene chiusa (il client esce dalla sessione con il proprio token).

  • (continua)
    • Il client effettua la sua seconda richiesta al server. Ogni volta che viene effettuata una richiesta a un server, il browser controlla tra tutti i cookie in suo possesso per vedere se ne ha uno proveniente dal server richiesto. In tal caso, lo invia al server, sempre sotto forma di comando HTTP: il comando Cookie, che ha una sintassi simile a quella del comando Set-Cookie utilizzato dal server:

Cookie: param1=valore1;param2=valore2;....

Tra i parametri inviati dal browser, il server troverà il token che gli permette di riconoscere il client e recuperare le informazioni ad esso associate.

Questa è la forma di token più comunemente utilizzata. Presenta un unico svantaggio: un utente può configurare il proprio browser in modo da rifiutare i cookie. Tali utenti non potranno quindi accedere alle applicazioni web che utilizzano i cookie.

  • Riscrittura dell'URL
    • Il client effettua la sua prima richiesta (il server la riconosce perché il client non ha alcun token)
    • Il server invia la sua risposta. Questa risposta contiene dei link che l'utente deve utilizzare per continuare a utilizzare l'applicazione. Nell'URL di ciascuno di questi link, il server aggiunge il token nella forma URL;token=valore.
    • Quando l'utente clicca su uno dei link per continuare a utilizzare l'applicazione, il browser invia una richiesta al server web, includendo l'URL richiesto (URL;token=value) nelle intestazioni HTTP. Il server è quindi in grado di recuperare il token.

4.2. L'API Java per il tracciamento delle sessioni

Di seguito presenteremo i principali metodi utili per il tracciamento delle sessioni:

HttpSession [HttpServletRequest].getSession()
Recupera l'oggetto Session a cui appartiene la richiesta corrente. Se la richiesta non faceva ancora parte di una sessione, ne viene creata una.
String [HttpSession].getId()
identificatore della sessione corrente
long [HttpSession].getCreationTime()
data di creazione della sessione corrente (numero di millisecondi trascorsi dal 1° gennaio 1970, ore 00:00).
long [HttpSession].getLastAccessedTime()
data dell'ultimo accesso del client alla sessione
long [HttpSession].getMaxInactiveInterval()
durata massima in secondi di inattività per una sessione. Dopo questo periodo, la sessione viene invalidata.
[HttpSession].setMaxInactiveInterval(int duration)
Imposta la durata massima di inattività per una sessione in secondi. Trascorso questo periodo, la sessione viene invalidata.
boolean [HttpSession].isNew()
true se la sessione è stata appena creata
[HttpSession].setAttribute(String parameter, Object value)
associa un valore a un parametro in una determinata sessione. Questo meccanismo consente di memorizzare informazioni che rimarranno disponibili per tutta la durata della sessione.
[HttpSession].removeAttribute(String parameter)
Rimuove il parametro dai dati della sessione.
Object [HttpSession].getAttribute(String parameter)
restituisce il valore associato al parametro nella sessione. Restituisce null se il parametro non esiste.
Enumerazione [HttpSession].getAttributeNames()
elenca tutti gli attributi della sessione corrente come enumerazione
[HttpSession].invalidate()
chiude la sessione corrente. Tutte le informazioni ad essa associate vengono eliminate.

4.3. Esempio 1

Presentiamo un esempio tratto dall'eccellente libro "Programming with J2EE" pubblicato da Wrox e distribuito da Eyrolles. Questo libro è una miniera di informazioni di alto livello per gli sviluppatori di soluzioni web Java. L'applicazione presentata in questo libro come singolo servlet Java è stata adattata qui come servlet principale che chiama pagine JSP per visualizzare le varie risposte possibili al client.

L'applicazione si chiama "sessions" ed è configurata come segue nel file <tomcat>\conf\server.xml:

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

Nella cartella docBase sopra indicata, troverete i seguenti elementi:

Image

I file error.jsp, invalid.jsp e valid.jsp sono tutti associati all'applicazione sessions. Nella cartella WEB-INF sopra indicata, troviamo:

Image

Quello mostrato sopra è il file di configurazione web.xml per l'applicazione sessions. Nella cartella classes troverete il file della classe servlet:

Image

Il file web.xml dell'applicazione è il seguente:

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

Il servlet principale si chiama cycledevie (servlet-name) ed è associato al file cycledevie.class (servlet-class). Ha un alias /cycledevie (servlet-mapping) che permette di richiamarlo tramite l'URL http://localhost:8080/sessions/cycledevie. Ha tre parametri di inizializzazione:

urlSessionValide
URL della pagina che mostra le proprietà della sessione corrente
urlSessionInvalid
URL della pagina visualizzata dopo l'invalidazione della sessione corrente
urlErreur
URL della pagina visualizzata in caso di errore di inizializzazione del servlet principale cycledevie

I componenti dell'applicazione delle sessioni sono i seguenti:

lifecycle
servlet principale - analizza la richiesta del client:
  • Se questa pagina fa parte di una sessione, passa il controllo alla pagina valide.jsp, che visualizza i dettagli della sessione. Da questa pagina, l'utente può:
    • ricaricarla
    • invalidarla
  • Se la richiesta chiede di invalidare la sessione corrente, il servlet passa il controllo alla pagina invalide.jsp, che chiederà all'utente di creare una nuova sessione
  • Se il servlet rileva degli errori durante l'inizializzazione, passa il controllo alla pagina error.jsp, che visualizzerà un messaggio di errore.
valide.jsp
  • visualizza i dettagli della sessione corrente e fornisce due link:
    • uno per ricaricare la pagina e vedere come cambia il parametro relativo all'ultimo accesso alla sessione corrente
    • l'altro per invalidare la sessione corrente
invalid.jsp
visualizzata quando l'utente ha invalidato la sessione corrente. Offre quindi la possibilità di crearne una nuova.
error.jsp
visualizzata quando il servlet principale rileva degli errori durante l'inizializzazione.

Il ciclo di vita del servlet principale è il seguente:

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

public class cycledevie extends HttpServlet{

    // instance variables
    String msgErreur=null;
    String urlSessionInvalide=null;
    String urlSessionValide=null;
    String urlErreur=null;

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

        // was the initialization successful?
        if(msgErreur!=null){
             // we hand over to the error page
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // retrieve the current session
        HttpSession session=request.getSession();

         // analyze the action to be taken
        String action=request.getParameter("action");
        // invalidate current session
        if(action!=null && action.equals("invalider")){
            // the current session is invalidated
            session.invalidate();
             // we hand over to the urlSessionInvalide url
            getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
        }
         // other cases
         // we hand over to the urlSessionInvalide url
        getServletContext().getRequestDispatcher(urlSessionValide).forward(request,response);
    }

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

     //-------- INIT
    public void init(){
         // retrieve initialization parameters
        ServletConfig config=getServletConfig();
        urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
        urlSessionValide=config.getInitParameter("urlSessionValide");
        urlErreur=config.getInitParameter("urlErreur");

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

Si notino i seguenti punti:

  • Nel suo metodo di inizializzazione, il servlet recupera i suoi tre parametri
  • Durante l'elaborazione (doGet) di una richiesta, il servlet:
    • verifica innanzitutto che non si siano verificati errori durante l'inizializzazione. Se ce ne sono stati, reindirizza alla pagina error.jsp.
    • verifica il valore del parametro action. Se questo parametro ha il valore "invalid", il servlet reindirizza alla pagina invalid.jsp; in caso contrario, alla pagina valid.jsp.

La pagina JSP **valide.jsp** visualizza le caratteristiche della sessione corrente:

<%@ 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";
%>
<!-- top of 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>

Si noti che nella riga

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

utilizziamo un oggetto session che sembra apparire dal nulla. In realtà, questo oggetto è uno degli oggetti impliciti messi a disposizione delle pagine JSP, proprio come gli oggetti request, response, out, config (ServletConfig) e context (ServletContext) che abbiamo già incontrato. I due link presenti nella pagina fanno riferimento al servlet lifecycle presentato in precedenza:

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

Il link per invalidare la sessione include il parametro action=invalidate, che permette al servlet del ciclo di vita di riconoscere che l'utente vuole invalidare la sessione corrente. L'altro link ricarica la pagina. Per impedire al browser di recuperare la pagina dalla cache, la direttiva HTML:

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

. Essa indica al browser di non utilizzare la cache per la pagina ricevuta.

La pagina invalidate.jsp è la seguente:

<!-- top of 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>

Fornisce un collegamento al servlet cycledevie senza il parametro action. Questo collegamento farà sì che il servlet cycledevie crei una nuova sessione.

La pagina error.jsp è la seguente:

<%
    // 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)";
%>
<!-- top of 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>

Il suo ruolo è quello di visualizzare il messaggio di errore che gli viene inviato dal servlet del ciclo di vita. Vediamo ora alcuni esempi di esecuzione. Il servlet viene richiesto per la prima volta:

Image

La pagina sopra indica che siamo in una nuova sessione. Utilizziamo il link "Ricarica pagina":

Image

Il risultato precedente indica che siamo ancora nella stessa sessione della pagina precedente (stesso ID). Si noti che l'ora dell'ultimo accesso a questa sessione è cambiata. Ora utilizziamo il link “Invalida sessione”:

Image

Si noti l'URL di questa nuova pagina con il parametro action=invalidate. Utilizziamo il link "Crea una nuova sessione" per creare una nuova sessione:

Image

Possiamo vedere che è stata avviata una nuova sessione. Negli esempi precedenti, la sessione si basa sul meccanismo dei cookie. Disattiviamo ora i cookie nel nostro browser e ripetiamo i test. Gli esempi seguenti sono stati eseguiti utilizzando Netscape Communicator. Per qualche motivo inspiegabile, i test eseguiti con IE6 hanno prodotto risultati inaspettati, come se IE6 continuasse a utilizzare i cookie anche se erano stati disattivati. Il servlet cycledevie viene richiesto per la prima volta:

Image

Ora utilizziamo il link "Ricarica pagina":

Image

Possiamo notare due cose:

  • l'ID della sessione è cambiato
  • il servlet rileva la sessione come una nuova sessione

Il server Tomcat fornisce una soluzione al problema degli utenti che disabilitano i cookie nel proprio browser. Utilizza due meccanismi per implementare il token menzionato all'inizio di questo paragrafo: i cookie e la riscrittura degli URL. Se il cookie di sessione non è disponibile, tenterà di recuperare il token dall'URL richiesto dal client. Affinché ciò funzioni, l'URL deve contenere il token. In generale, tutti i link generati in un documento HTML che puntano all'applicazione web devono contenere il token dell'applicazione. Ciò può essere fatto utilizzando il metodo encodeURL:

String [HttpResponse].encodeURL(String URL)
aggiunge il token della sessione corrente all'URL passato come parametro nella forma URL;jsessionid=xxxx

Modifichiamo la nostra applicazione come segue:

  • Nel servlet cycledevie.java, gli URL sono codificati:
             // we hand over to the error page
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // we hand over to the urlSessionInvalide url
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // we hand over to the urlSessionInvalide url
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • Nella pagina valide.jsp, gli URL sono codificati:
<%
    // 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>
  • Nella pagina invalide.jsp, gli URL sono codificati:
<%
    // 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>

Ora siamo pronti per il test. Stiamo utilizzando Netscape 4.5 e i cookie sono stati disabilitati. Richiediamo il servlet cycledevie per la prima volta:

Image

e ricarichiamo la pagina utilizzando il link "Ricarica pagina":

Image

Possiamo vedere che:

  • la sessione non è cambiata (stesso ID)
  • l'URL del servlet cycledevie contiene effettivamente il token, come mostrato nel campo Indirizzo sopra
  • il server Tomcat recupera quindi il token di sessione dall'URL richiesto (se lo sviluppatore ha avuto cura di codificarlo).

4.4. Esempio 2

Presentiamo ora un esempio che mostra come memorizzare informazioni nella sessione di un client. In questo caso, l'unica informazione sarà un contatore che viene incrementato ogni volta che l'utente richiama l'URL del servlet. Quando viene richiamato per la prima volta, appare la seguente pagina:

Image

Se si fa clic sul link "Ricarica pagina" in alto, si ottiene la seguente nuova pagina:

Image

L'applicazione è composta da tre elementi:

  • un servlet che elabora la richiesta del client
  • una pagina JSP che visualizza il valore del contatore
  • una pagina JSP che visualizza eventuali errori

Questi tre componenti sono installati nell'applicazione web "sessions" già in uso. Il suo file web.xml è stato modificato per configurare i nuovi servlet:

<?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>
  • Il servlet si chiama counter (servlet-name) ed è collegato al file di classe counter.class (servlet-class)
  • Ha due parametri di inizializzazione:
    • displayCounterURL: URL della pagina JSP che visualizza il contatore
    • urlErreur: URL della pagina JSP che visualizza eventuali errori
  • e un alias /compteur, il che significa che verrà richiamato tramite l'URL http://localhost:8080/sessions/compteur

Il servlet compteur.java è il seguente:

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

public class compteur extends HttpServlet{

     // instance variables
    String msgErreur=null;
    String urlAffichageCompteur=null;
    String urlErreur=null;

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

        // was the initialization successful?
        if(msgErreur!=null){
             // we hand over to the error page
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // retrieve the current session
        HttpSession session=request.getSession();
         // and the
        String compteur=(String)session.getAttribute("compteur");
        if(compteur==null) compteur="0";
         // counter incrementation
        try{
            compteur=""+(Integer.parseInt(compteur)+1);
        }catch(Exception ex){}
         // save counter in session
        session.setAttribute("compteur",compteur);
         // and in the query
        request.setAttribute("compteur",compteur);

         // hand over to counter display url
        getServletContext().getRequestDispatcher(urlAffichageCompteur).forward(request,response);
    }

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

     //-------- INIT
    public void init(){
         // retrieve initialization parameters
        ServletConfig config=getServletConfig();
        urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
        urlErreur=config.getInitParameter("urlErreur");

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

Questo servlet ha la stessa struttura dei servlet che abbiamo già visto. Da notare la gestione del contatore:

  • la sessione viene recuperata tramite request.getSession()
  • il contatore viene recuperato da questa sessione tramite session.getAttribute("counter")
  • se viene recuperato un valore nullo, significa che la sessione è appena iniziata. Il contatore viene quindi impostato a 0.
  • il contatore viene incrementato, salvato nuovamente nella sessione (session.setAttribute("counter", counter)) e inserito nella richiesta che verrà passata al servlet di visualizzazione (request.setAttribute("counter", counter)).

La pagina di visualizzazione, compteur.jsp, è la seguente:

<%
    // jspService
  // on récupère le compteur
  String compteur= (String) request.getAttribute("compteur");
  if(compteur==null) compteur="inconnu";
%>
<!-- top of 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 pagina sopra riportata recupera semplicemente l'attributo contatore (request.getAttribute("counter")) che le viene passato dal servlet principale e lo visualizza.

La pagina di errore, *erreurcompteur.jsp,* è la seguente:

<%
    // jspService
  // une erreur s'est produite
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- top of 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. Esempio 3

Proponiamo di scrivere un'applicazione Java che funga da client per la precedente applicazione contatore. La chiamerebbe N volte di seguito, dove N viene passato come parametro. Il nostro obiettivo è dimostrare un client web programmato e come gestire i cookie. Il nostro punto di partenza sarà un client web generico presentato nella dispensa Java dello stesso autore. Viene chiamato come segue:

webclient URL GET/HEAD

  • URL: URL richiesto
  • GET/HEAD: GET per richiedere il codice HTML della pagina, HEAD per limitare la risposta alle sole intestazioni HTTP

Ecco un esempio che utilizza 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
 
 
<!-- top of 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>

Il programma clientweb visualizza tutto ciò che riceve dal server. Sopra, vediamo il comando HTTP Set-cookie, che il server utilizza per inviare un cookie al proprio client. In questo caso, il cookie contiene due informazioni:

  • JSESSIONID, che è il token di sessione
  • Path, che definisce l'URL a cui appartiene il cookie. Path=/sessions indica al browser che deve inviare il cookie al server ogni volta che richiede un URL che inizia con /sessions. Nell'applicazione sessions abbiamo utilizzato vari servlet, tra cui i servlet /sessions/lifecycle e /sessions/counter. Se chiamiamo il servlet /sessions/cycledevie, il browser riceverà un token J. Se, utilizzando lo stesso browser, chiamiamo poi il servlet /sessions/compteur, il browser invierà il token J al server perché si applica a tutti gli URL che iniziano con /sessions. Nel nostro esempio, i servlet cycledevie e compteur non hanno bisogno di condividere lo stesso token di sessione. Non avrebbero quindi dovuto essere inseriti nella stessa applicazione web. Questo è un punto importante da ricordare: tutti i servlet all'interno della stessa applicazione condividono lo stesso token di sessione.
  • Un cookie può anche avere una data di scadenza. In questo caso, tale informazione manca. Il cookie verrà quindi cancellato alla chiusura del browser. Un cookie può avere una durata di N giorni, ad esempio. Finché è valido, il browser lo invierà ogni volta che si accede a uno degli URL nel suo dominio (Path). Si consideri un negozio di CD online. Esso può tracciare il percorso di navigazione di un cliente attraverso il proprio catalogo e determinare gradualmente le sue preferenze, ad esempio la musica classica. Queste preferenze possono essere memorizzate in un cookie con una durata di 3 mesi. Se lo stesso cliente torna sul sito dopo un mese, il browser invierà il cookie all'applicazione server. Sulla base delle informazioni contenute nel cookie, l'applicazione server potrà quindi personalizzare le pagine generate in base alle preferenze del cliente.

Di seguito è riportato il codice del client web. In seguito servirà come punto di partenza per un altro client.

// imported packages
import java.io.*;
import java.net.*;

public class clientweb{

    // requests a URL
     // displays its contents on the screen

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

        // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the URI required
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // URI validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // order verification
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // incorrect order
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // extract useful information from 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();

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
        try{
             // connect to the server
            client=new Socket(host,port);

            // create customer input/output flows TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // request URL - send HTTP headers
            OUT.println(commande + " " + path + query + " HTTP/1.1");   
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // we read the answer
            while((réponse=IN.readLine())!=null){
                 // the answer is processed
                System.out.println(réponse);
            }//while
             // it's over
            client.close();
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

Ora creiamo il programma clientCounter, che viene chiamato come segue:

clientCounter URL N [JSESSIONID]

  • URL: URL del servlet contatore
  • N: numero di chiamate da effettuare a questo servlet
  • JSESSIONID: parametro opzionale — token di sessione

Lo scopo del programma è chiamare il servlet contatore N volte gestendo il cookie di sessione e visualizzando ogni volta il valore del contatore restituito dal server. Al termine delle N chiamate, il valore del contatore deve essere N. Ecco un primo esempio di esecuzione:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/counter 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
 

Il programma visualizza:

  • le intestazioni HTTP che invia al server nel modulo -->
  • le intestazioni HTTP che riceve
  • il valore del contatore dopo ogni chiamata

Possiamo vedere che durante la prima chiamata:

  • il client non invia un cookie
  • il server ne invia uno

Per le richieste successive:

  • il client rinvia sistematicamente il cookie ricevuto dal server durante la prima richiesta. Questo è ciò che permette al server di riconoscerlo e di incrementare il proprio contatore.
  • il server non invia più un cookie

Eseguiamo nuovamente il programma precedente passando il token sopra indicato come terzo parametro:


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

Qui vediamo che non appena il client effettua la sua prima richiesta, il server riceve un cookie di sessione valido. È importante notare che per Tomcat, il tempo massimo di inattività predefinito per una sessione è di 20 minuti (questo è in realtà configurabile). Se la seconda richiesta del programma invia il cookie ricevuto durante la prima richiesta abbastanza rapidamente, il server la tratterà come la stessa sessione. Ciò evidenzia una potenziale vulnerabilità di sicurezza. Se riesco a intercettare un token di sessione sulla rete, posso quindi impersonare l'utente che ha avviato la sessione. Nel nostro esempio, la prima chiamata rappresenta l'utente che avvia la sessione (forse con un nome utente e una password che gli garantiscono il diritto di ricevere un token), mentre la seconda chiamata rappresenta l'utente che ha "hackerato" il token di sessione dalla prima chiamata. Se l'operazione in corso è una transazione bancaria, ciò può diventare molto problematico...

Il codice client è il seguente:

// imported packages
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientCompteur{

     // requests a URL
     // displays its contents on the screen

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

         // number of arguments
        if(args.length !=2 && args.length != 3)
            erreur(syntaxe,1);

         // note the URL required
        String URLString=args[0];

        // URL validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // check number of calls N
        int N=0;
        try{
            N=Integer.parseInt(args[1]);
            if(N<=0) throw new Exception();
        }catch(Exception ex){
             // incorrect N argument
            erreur("Le nombre d'appels N doit être un entier >0",3);
        }
         // has the JSESSIONID token been passed as a parameter?
        String JSESSIONID="";
        if (args.length==3) JSESSIONID=args[2];

        // extract useful information from 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();

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
         // the model searched for in HTTP headers
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // the model searched for in the HTML code
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
         // the result of the model comparison
        Matcher résultat=null;
         // a Boolean giving the result of the counter search
        boolean compteurTrouvé;

        try{
             // we make N calls to the server
            for(int i=0;i<N;i++){
                // connect to the server
                client=new Socket(host,port);

                // create customer input/output flows TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // request URL - send HTTP headers
                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,"");

                 // we read the response through to the end of the headers, looking for any cookies
                while((réponse=IN.readLine())!=null){
                     // follow-up response
                    System.out.println(réponse);
                     // empty line?
                    if(réponse.equals("")) break;
                     // line HTTP not empty
                     // if you don't have the session token, look for it
                    if (JSESSIONID.equals("")){
                        // compare the HTTP line with the cookie template
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // we found the cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while

                 // that's it for HTTP headers - move on to HTML code
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // does the current line contain the counter?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // counter found - displayed
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while
                 // it's over
                client.close();
            }//for
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error

     // monitoring client-server exchanges
    public static void envoie(PrintWriter OUT,String msg){
        // sends message to server
        OUT.println(msg);
         // screen tracking
        System.out.println("--> "+msg);
    }//error
}//class

Analizziamo i punti chiave di questo programma:

  • Dobbiamo eseguire N scambi client-server. Ecco perché sono in un ciclo
            for(int i=0;i<N;i++){
  • Per ogni scambio, il client apre una connessione TCP/IP con il server. Una volta stabilita la connessione, invia le intestazioni HTTP della sua richiesta al server:
                // 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,"");

Se il token JSESSIONID è disponibile, viene inviato come cookie; in caso contrario, non viene inviato.

  • Una volta inviata la richiesta, il client attende la risposta del server. Inizia esaminando le intestazioni HTTP di questa risposta per cercare un possibile cookie. Per trovarlo, confronta le righe ricevute con l'espressione regolare del cookie:
         // the model searched in HTTP headers
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // we read the response through to the end of the headers, looking for any cookies
                while((réponse=IN.readLine())!=null){
                     // follow-up response
                    System.out.println(réponse);
                     // empty line?
                    if(réponse.equals("")) break;
                     // line HTTP not empty
                     // if you don't have the session token, look for it
                    if (JSESSIONID.equals("")){
                        // compare the HTTP line with the cookie template
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // we found the cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • Una volta individuato il token per la prima volta, non verrà più cercato nelle successive richieste al server. Una volta elaborate le intestazioni HTTP della risposta, passiamo al codice HTML della stessa risposta. Al suo interno, cerchiamo la riga che fornisce il valore del contatore. Anche questa ricerca viene effettuata utilizzando un'espressione regolare:
         // the counter model searched for in the HTML code
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // that's it for HTTP headers - move on to HTML code
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // does the current line contain the counter?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // counter found - displayed
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. Esempio 4

Nell'esempio precedente, il client web restituisce il token come cookie. Abbiamo visto che potrebbe anche restituirlo all'interno dello stesso URL richiesto nella forma URL;jsessionid=xxx. Verifichiamolo. Il programma clientCompteur.java viene rinominato clientCompteur2.java e modificato come segue:

....
                // 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,"");
....

Il client richiede quindi l'URL del contatore tramite GET URL;jsessionid=xx HTTP/1.1 e non invia più un cookie. Questa è l'unica modifica. Ecco i risultati di una chiamata iniziale:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/counter 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

Alla prima richiesta, il client richiede l'URL senza un token di sessione. Il server risponde inviando il token. Il client quindi richiede nuovamente lo stesso URL, aggiungendovi il token ricevuto. Possiamo vedere che il contatore viene incrementato, a dimostrazione del fatto che il server ha correttamente riconosciuto che si trattava della stessa sessione.

4.7. Esempio 5

Questo esempio mostra un'applicazione composta da tre pagine, che chiameremo pagina0, pagina1 e pagina2. L'utente deve accedervi in questo ordine:

  • pagina0 è un modulo che richiede informazioni: un nome
  • pagina1 è un modulo ricevuto in risposta all'invio del modulo su pagina0. Richiede una seconda informazione: un'età
  • pagina2 è un documento HTML che visualizza il nome ottenuto da pagina0 e l'età ottenuta da pagina1.

Qui ci sono tre scambi client-server:

  • Nel primo scambio, il modulo di pagina0 viene richiesto dal client e inviato dal server
  • Nel secondo scambio, il modulo di page1 viene richiesto dal client e inviato dal server. Il client invia il nome al server.
  • Nel terzo scambio, il documento page3 viene richiesto dal client e inviato dal server. Il client invia l'età al server. Il documento page3 deve visualizzare il nome e l'età. Il nome è stato ottenuto dal server nel secondo scambio e da allora è stato "dimenticato". Viene utilizzata una sessione per memorizzare il nome dello scambio 2 in modo che sia disponibile durante lo scambio 3.

Il documento page0 ottenuto nel primo scambio è il seguente:

Image

Compiliamo il campo del nome:

Image

Clicchiamo sul pulsante "Avanti" e otteniamo la seguente pagina 1:

Image

Compiliamo il campo relativo all'età:

Image

Clicchiamo sul pulsante "Avanti" e ci viene quindi presentata la seguente pagina 2:

Image

Quando la pagina 0 viene inviata al server, quest'ultimo potrebbe restituirla con un codice di errore se il campo del nome è vuoto:

Image

Quando invii la pagina 1 al server, questo potrebbe restituire un codice di errore se l'età non è valida:

Image

L'applicazione è composta da un servlet e quattro pagine JSP:

page0.jsp
visualizza page0
page1.jsp
visualizza la pagina 1
page2.jsp
visualizza pagina2
error.jsp
visualizza una pagina di errore

L'applicazione web si chiama suitedepages ed è configurata come segue nel file server.xml di Tomcat:

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

Il file di configurazione web.xml per l'applicazione suitedepages è il seguente:

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

Il servlet principale si chiama main e, grazie al suo alias (servlet-mapping), è accessibile tramite l'URL http://localhost:8080/suitedepages/main*. Ha quattro parametri di inizializzazione, che sono gli URL delle quattro pagine JSP utilizzate per le diverse viste. Il codice per il servlet main* è il seguente:

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

public class main extends HttpServlet{

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

        // was the initialization successful?
        if(msgErreur!=null){
             // we hand over to the error page
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }
         // we retrieve the step parameter
        String étape=request.getParameter("etape");
         // retrieve the current session
        HttpSession session=request.getSession();
         // the current step is processed
        if(étape==null) étape0(request,response,session);
        if(étape.equals("1")) étape1(request,response,session);
        if(étape.equals("2")) étape2(request,response,session);
         // other cases are invalid
        étape0(request,response,session);
    }

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

     //-------- INIT
    public void init(){
         // retrieve initialization parameters
        ServletConfig config=getServletConfig();
        urlPage0=config.getInitParameter("urlPage0");
        urlPage1=config.getInitParameter("urlPage1");
        urlPage2=config.getInitParameter("urlPage2");
        urlErreur=config.getInitParameter("urlErreur");

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

     //-------- step0
    public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // we set a few attributes
        request.setAttribute("nom","");
         // we present page 0
        request.getRequestDispatcher(urlPage0).forward(request,response);
    }

     //-------- step1
    public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // retrieve the name from the query
        String nom=request.getParameter("nom");
        // name positioned?
        if(nom==null) étape0(request,response,session);
         // remove any spaces from the name
        nom=nom.trim();
         // we put it in a query attribute
        request.setAttribute("nom",nom);
         // empty name?
        if(nom.equals("")){
             // it's a mistake
            ArrayList erreurs=new ArrayList();
            erreurs.add("Nous n'avez pas indiqué de nom");
             // put the errors in the query
            request.setAttribute("erreurs",erreurs);
             // back to page 0
            étape0(request,response,session);
        }
         // valid name - stored in the current session
        session.setAttribute("nom",nom);
         // set the age attribute in the query
        request.setAttribute("age","");
         // we present page 1
        request.getRequestDispatcher(urlPage1).forward(request,response);
    }

     //-------- step2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // retrieve the name from the session
        String nom=(String)session.getAttribute("nom");
         // name positioned?
        if(nom==null) étape0(request,response,session);
         // we put it in a query attribute
        request.setAttribute("nom",nom);
         // the age is retrieved from the query
        String age=request.getParameter("age");
        // age positioned?
        if(age==null){
            // back to page 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // the age is stored in the query
        age=age.trim();
        request.setAttribute("age",age);
        // valid age?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // this is a mistake
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // put the errors in the query
            request.setAttribute("erreurs",erreurs);
             // back to page 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // age valid - page 2 is presented
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • Il metodo init recupera i quattro parametri di inizializzazione e imposta un messaggio di errore se uno di essi è mancante
  • Abbiamo visto che la richiesta consiste in tre scambi. Per tenere traccia della fase corrente di questi scambi, i moduli page0 e page1 hanno una variabile nascosta "etape" con un valore pari a 1 (page0) o 2 (page1). Questo numero può essere interpretato come il numero della pagina successiva da visualizzare. Nel metodo doGet, questo parametro viene recuperato dalla richiesta e, a seconda del suo valore, l'elaborazione viene delegata ad altri tre metodi:
    • étape0 elabora la richiesta iniziale e invia page0
    • étape1 elabora il modulo su page0 e invia page1 o nuovamente page0 se si è verificato un errore
    • étape2 elabora il modulo nella pagina1 e invia la pagina2 o nuovamente la pagina1 se si è verificato un errore
  • step0
    • visualizza la pagina0 con un nome vuoto
  • Passo 1
    • Recupera il parametro "name" dal modulo della pagina0.
    • Verifica che il nome esista (non sia nullo). In caso contrario, la pagina 0 viene visualizzata nuovamente come se fosse la prima chiamata.
    • Verifica che il nome non sia vuoto. Se lo è, viene visualizzata nuovamente la pagina0 insieme a un messaggio di errore.
    • memorizza il nome nella sessione corrente e visualizza la pagina 1 se il nome è valido.
  • passaggio2
    • Recupera il parametro name dalla sessione corrente.
    • Verifica che il nome esista (non sia nullo). In caso contrario, visualizza nuovamente la pagina 0 come se fosse la prima chiamata.
    • Recupera il parametro age dalla richiesta corrente inviata da page1.
    • Verifica che l'età sia valida. In caso contrario, visualizza nuovamente la pagina 1 con un messaggio di errore.
    • Memorizza il nome e l'età come attributi della richiesta e visualizza la pagina 2 se il nome e l'età sono validi.

La pagina page0.jsp è la seguente:

<%@ 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 pagina page0.jsp può essere richiamata dal servlet principale in due casi:
    • durante la richiesta iniziale
    • dopo l'elaborazione del modulo page0 quando si verifica un errore
  • Il parametro `nameToDisplay` viene fornito dal servlet principale, insieme a un eventuale elenco di errori. Il servlet page0.jsp inizia quindi recuperando queste due informazioni.
  • Il modulo viene "inviato" al servlet principale con il campo nascosto "etape", che indica in quale fase dell'applicazione si trova l'utente.

La pagina page1.jsp è la seguente:

<%@ 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 pagina page1.jsp ha una struttura simile a quella della pagina page0.jsp, con l'eccezione che ora riceve due attributi dal servlet principale: name e age. Infine, la pagina page2.jsp è la seguente:

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

Anche la pagina page2.jsp riceve gli attributi name e age dal servlet principale. Si limita a visualizzarli. Infine, la pagina error.jsp, che ha il compito di visualizzare un errore in caso di inizializzazione errata del servlet, è la seguente:

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

Visualizza l'attributo msgError che gli è stato passato dal servlet principale.

In conclusione, possiamo vedere che durante le tre fasi dell'applicazione, è sempre il servlet principale ad essere interrogato per primo dal browser. Tuttavia, non è il servlet principale a generare la risposta da visualizzare, ma una delle quattro pagine JSP. L'utente non se ne accorge, poiché il browser continua a visualizzare l'URL inizialmente richiesto — quello del servlet principale — nel campo "Indirizzo".