Skip to content

4. Monitoraggio della sessione

4.1. Il problema

Un'applicazione web può consistere in diversi scambi di moduli tra il server e il client. Il funzionamento è quindi il seguente:

  • fase 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 al punto 1.
  • Fase 2
    • il client C1 lo compila e lo rinvia al server. A tal fine, il browser apre una nuova connessione con il server.
    • Quest’ultimo elabora i dati del modulo 1, calcola le informazioni I1 sulla base di tali dati, invia un modulo F2 al client C1 e chiude la connessione aperta al punto 3.
  • Fase 3
    • Il ciclo delle fasi 3 e 4 si ripete nelle fasi 5 e 6. Al termine della fase 6, il server avrà ricevuto due moduli F1 e F2 e, sulla base di questi, avrà calcolato le informazioni I1 e I2.

Il problema che si pone è: in che modo il server riesce a conservare le informazioni I1 e I2 relative al cliente C1? Questo problema è noto come tracciamento della sessione del cliente C1. Per comprenderne le cause, esaminiamo lo schema di un’applicazione server TCP-IP che serve contemporaneamente più clienti:

In una classica applicazione client-server TCP-IP:

  • il client stabilisce una connessione con il server
  • scambia dati con il server tramite tale connessione
  • la connessione viene chiusa da uno dei due partner

I due punti salienti di questo meccanismo sono:

  1. per ogni cliente viene creata una connessione unica
  2. questa connessione viene utilizzata per tutta la durata del dialogo tra il server e il suo cliente

Ciò che permette al server di sapere in un dato momento con quale client sta lavorando è la connessione, ovvero il "canale" che lo collega al proprio client. Poiché questo canale è dedicato a un determinato client, tutto ciò che arriva da esso proviene da quel client e tutto ciò che viene inviato attraverso di esso arriva al client.

Il meccanismo client-server HTTP segue fedelmente lo schema precedente, con la particolarità che la comunicazione client-server è limitata a un unico scambio tra il client e il server:

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

Se al momento T1 un cliente C effettua una richiesta al server, ottiene una connessione C1 che servirà per l’unico scambio richiesta-risposta. Se al momento T2, lo stesso client effettua una seconda richiesta al server, otterrà una connessione C2 diversa dalla connessione C1. Per il server, quindi, non vi è alcuna differenza tra questa seconda richiesta dell’utente C e la sua richiesta iniziale: in entrambi i casi, il server considera il cliente come un nuovo cliente. Affinché vi sia un collegamento tra le diverse connessioni del cliente C al server, è necessario che il cliente C venga «riconosciuto» dal server come un «cliente abituale» e che il server recuperi le informazioni in suo possesso su tale cliente abituale.

Immaginiamo un sistema amministrativo che funzionasse nel modo seguente:

  • C'è un'unica coda
  • Ci sono diversi sportelli. Pertanto, più clienti possono essere serviti contemporaneamente. Quando uno sportello si libera, un cliente esce dalla coda per essere servito a quello sportello
  • Se è la prima volta che il cliente si presenta, l’addetto allo sportello gli consegna un gettone con un numero. Il cliente può porre una sola domanda. Una volta ottenuta la risposta, deve lasciare lo sportello e tornare in coda. L’addetto allo sportello annota le informazioni relative a quel cliente in una cartella contrassegnata dal numero del gettone.
  • Quando arriva nuovamente il suo turno, il cliente può essere servito da un addetto allo sportello diverso rispetto alla volta precedente. Quest’ultimo gli chiede il gettone e recupera la cartella con il numero corrispondente al gettone. Il cliente formula nuovamente una richiesta, riceve una risposta e le informazioni vengono aggiunte alla sua cartella.
  • e così via... Nel corso del tempo, il cliente otterrà la risposta a tutte le sue richieste. Il tracciamento tra le diverse richieste avviene grazie al gettone e alla cartella ad esso associata.

Il meccanismo di tracciamento della sessione in un’applicazione web client-server è analogo al funzionamento descritto in precedenza:

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

Il token può assumere diverse forme:

  • quello di un campo nascosto in un modulo
    • il client effettua la sua prima richiesta (il server lo riconosce dal fatto che il client non possiede un token)
    • il server invia la 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 pagina con il proprio token). Il server si è eventualmente premurato di associare delle informazioni a questo token.
    • il client effettua la seconda richiesta inviando nuovamente il modulo. Il server recupera il token dal modulo. Può quindi elaborare la seconda richiesta del cliente avendo accesso, grazie al token, alle informazioni calcolate durante la prima richiesta. Nuove informazioni vengono aggiunte al file associato al token, viene inviata una seconda risposta al cliente e la connessione viene chiusa per la seconda volta. Il token è stato reinserito nel modulo di risposta affinché l’utente possa presentarlo nella sua richiesta successiva.
    • 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 è più utilizzabile.

  • Quello del cookie
    • il client effettua la sua prima richiesta (il server lo riconosce dal fatto che il client non possiede un token)
    • il server invia la risposta aggiungendo un cookie nelle intestazioni HTTP della stessa. Ciò avviene tramite il comando HTTP Set-Cookie:

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

dove parami sono i nomi dei parametri e valeursi i relativi valori. Tra i parametri sarà presente il token. Molto spesso, nel cookie è presente solo il token, mentre le altre informazioni vengono registrate dal server 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 pagina 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 se ne possiede uno proveniente dal server richiesto. In caso affermativo, lo invia al server sempre sotto forma di comando HTTP, il comando Cookie che ha una sintassi analoga a quella del comando Set-Cookie utilizzato dal server:

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

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

Si tratta della forma di token più utilizzata. Presenta tuttavia uno svantaggio: un utente può configurare il proprio browser in modo che non accetti i cookie. Questo tipo di utente non avrà quindi accesso alle applicazioni web che utilizzano i cookie.

  • riscrittura di URL
    • il client effettua la sua prima richiesta (il server lo riconosce dal fatto che il client non possiede alcun token)
    • il server invia la sua risposta. Questa contiene dei link che l’utente deve utilizzare per proseguire nell’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 proseguire con l’applicazione, il browser invia la richiesta al server web inserendo nelle intestazioni HTTP il token richiesto URL URL;token=valore. Il server è quindi in grado di recuperare il token.

4.2. Java per il tracciamento delle sessioni

Presentiamo ora i principali metodi utili per il tracciamento della sessione:

HttpSession [HttpServletRequest].getSession()
ottiene l’oggetto Session a cui appartiene la richiesta in corso. Se questa non faceva ancora parte di una sessione, quest’ultima viene creata.
String [HttpSession].getId()
identificativo della sessione in corso
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 alla sessione da parte del cliente
long [HttpSession].getMaxInactiveInterval()
durata massima in secondi di inattività di una sessione. Trascorso questo lasso di tempo, la sessione viene invalidata.
[HttpSession].setMaxInactiveInterval(int durée)
imposta in secondi la durata massima di inattività di una sessione. Trascorso tale lasso di tempo, la sessione viene invalidata.
boolean [HttpSession].isNew()
vero se la sessione è stata appena creata
[HttpSession].setAttribute(String paramètre, Object valeur)
associa un valore a un parametro in una determinata sessione. È questo meccanismo che consente di memorizzare informazioni che rimarranno disponibili per tutta la durata della sessione.
[HttpSession].removeAttribute(String paramètre)
rimuove parametre dai dati della sessione.
Object [HttpSession].getAttribute(String paramètre)
valore associato al parametro paramètre della sessione. Restituisce null se quest'ultimo non esiste.
Enumeration [HttpSession].getAttributeNames()
elenco sotto forma di enumerazione di tutti gli attributi della sessione corrente
[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 "Programmazione con J2EE", edito da Wrox e distribuito da Eyrolles. Questo libro è una miniera di informazioni di alto livello per gli sviluppatori di soluzioni Web in Java. L'applicazione presentata nel libro sotto forma di un unico servlet Java è stata qui riproposta come servlet principale che utilizza pagine JSP per visualizzare le varie risposte possibili al cliente.

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 si trovano i seguenti elementi:

Image

I file erreur.jsp, invalide.jsp e valide.jsp sono tutti e tre associati all'applicazione sessions. Nella cartella WEB-INF sopra indicata si trovano:

Image

Sopra è visibile il file di configurazione dell'applicazione "sessions" web.xml. Nella cartella classes si trova il file di classe del 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 di classe cycledevie.class (servlet-class). Ha un alias /cycledevie (servlet-mapping) che consente di richiamarla tramite URL e http://localhost:8080/sessions/cycledevie. Ha tre parametri di inizializzazione:

urlSessionValide
URL della pagina che presenta le caratteristiche della sessione in corso
urlSessionInvalide
URL della pagina visualizzata dopo l’invalidazione della sessione in corso
urlErreur
URL della pagina visualizzata in caso di errore di inizializzazione del servlet principale cycledevie

I componenti dell'applicazione «sessions» sono i seguenti:

cycledevie
servlet principale - analizza la richiesta del client:
  • se questa fa parte di una sessione, passa il controllo alla pagina valide.jsp che visualizzerà le caratteristiche di tale sessione. Da questa pagina, l'utente può:
    • ricaricarla
    • invalidarla
  • se la richiesta prevede l'invalidazione della sessione in corso, il servlet passa il controllo alla pagina invalide.jsp, che proporrà all'utente di ricreare una nuova sessione
  • se durante l’inizializzazione la servlet riscontra degli errori, passa il controllo alla pagina erreur.jsp, che visualizzerà un messaggio di errore.
valide.jsp
  • visualizza le caratteristiche della sessione in corso e propone due link:
    • uno per ricaricare la pagina e vedere così l’evoluzione del parametro dell’ultimo accesso alla sessione corrente
    • l’altro per invalidare la sessione in corso
invalide.jsp
visualizzata quando l'utente ha invalidato la sessione corrente. Propone quindi di crearne una nuova.
erreur.jsp
visualizzata quando il servlet principale riscontra degli errori durante l’inizializzazione.

La servlet principale cycledevie è la seguente:

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

public class cycledevie extends HttpServlet{

    // variabili di istanza
    String msgErreur=null;
    String urlSessionInvalide=null;
    String urlSessionValide=null;
    String urlErreur=null;

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

        // L'inizializzazione è andata a buon fine?
        if(msgErreur!=null){
             // si passa alla pagina di errore
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // si recupera la sessione corrente
        HttpSession session=request.getSession();

         // si analizza l'azione da eseguire
        String action=request.getParameter("action");
        // invalidare la sessione corrente
        if(action!=null && action.equals("invalider")){
            // si invalida la sessione corrente
            session.invalidate();
             // si passa il controllo all'URL urlSessionInvalide
            getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
        }
         // altri casi
         // si passa il controllo all'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(){
         // si recuperano i parametri di inizializzazione
        ServletConfig config=getServletConfig();
        urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
        urlSessionValide=config.getInitParameter("urlSessionValide");
        urlErreur=config.getInitParameter("urlErreur");

        // parametri 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, la servlet:
    • verifica innanzitutto che non si siano verificati errori durante l’inizializzazione. Se ce ne sono stati, passa il controllo alla pagina erreur.jsp.
    • verifica il valore del parametro action. Se quest’ultimo ha il valore "invalider", la servlet passa il controllo alla pagina invalide.jsp, altrimenti alla pagina valide.jsp.

La pagina JSP valide.jsp che mostra le caratteristiche della sessione corrente:

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

<%
     // jspService
   // in questo caso si deve descrivere la sessione in corso
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- inizio della pagina 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";

viene utilizzato un oggetto di sessione che sembra provenire dal nulla. In realtà, questo oggetto fa parte degli oggetti impliciti messi a disposizione delle pagine JSP, così come gli oggetti request, response, out, config (ServletConfig), context (ServletContext) già menzionati. I due link presenti nella pagina rimandano al servlet cycledevie 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=invalider che consentirà al servlet cycledevie di riconoscere che l'utente desidera invalidare la sessione corrente. L'altro link consente di ricaricare la pagina. Affinché il browser non la recuperi dalla cache, la direttiva HTML:

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

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

La pagina invalide.jsp è la seguente:

<!-- inizio della pagina 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 che punta al servlet cycledevie senza il parametro action. Questo collegamento farà sì che il servlet cycledevie crei una nuova sessione.

La pagina erreur.jsp è la seguente:

<%
     // jspService
   // in questo caso si deve descrivere la sessione in corso
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- inizio della pagina 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 le è stato trasmesso dal servlet cycledevie. Vediamo ora alcuni esempi di esecuzione. Il servlet viene richiesto una prima volta:

Image

La pagina sopra riportata indica che ci si trova in una nuova sessione. Si utilizza il link «Aggiorna la pagina»:

Image

Il risultato precedente indica che ci si trova 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 «Annulla la sessione»:

Image

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

Image

Si nota che è stata avviata una nuova sessione. Negli esempi precedenti, la sessione si basa sul meccanismo dei cookie. Disattiviamo ora l’uso dei cookie sul nostro browser e ripetiamo i test. Gli esempi seguenti sono stati realizzati con Netscape Communicator. Per una ragione inspiegabile, i test effettuati con IE6 davano risultati inaspettati, come se IE6 continuasse a utilizzare i cookie nonostante questi fossero stati disattivati. La servlet cycledevie viene richiesta una prima volta:

Image

Ora utilizziamo il link «Aggiorna la pagina»:

Image

Si possono notare due cose:

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

Il server Tomcat offre una soluzione al problema degli utenti che disabilitano l’uso dei cookie sul proprio browser. Utilizza due meccanismi per implementare il token di cui abbiamo parlato all’inizio di questo paragrafo: i cookie e la riscrittura di URL. Se il cookie di sessione non è disponibile, cercherà di ottenere il token dall'URL richiesto dal client. A tal fine, è necessario che quest’ultima contenga il token. In generale, tutti i link generati in un documento HTML verso l’applicazione web devono contenere il token di quest’ultima. Ciò può essere fatto con il metodo encodeURL:

String [HttpResponse].encodeURL(String URL)
aggiunge il token della sessione corrente al parametro URL passato sotto la forma URL;jsessionid=xxxx

Modifichiamo la nostra applicazione nel modo seguente:

  • nel servlet cycledevie.java, i URL vengono codificati:
             // si passa alla pagina di errore
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // si passa all'URL urlSessionInvalide
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // si passa all'URL urlSessionInvalide
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • nella pagina valide.jsp sono codificati i URL:
<%
     // jspService
   // in questo caso si deve descrivere la sessione in corso
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
   // codifica URL ciclo di vita
  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 sono codificati i URL:
<%
     // jspservice - si invalida la sessione in corso
  session.invalidate();
   // codifica URL ciclo di vita
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

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

Image

e ricarichiamo la pagina tramite il link «Ricarica la pagina»:

Image

Possiamo notare che:

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

4.4. Esempio 2

Presentiamo ora un esempio che illustra come memorizzare informazioni nella sessione di un cliente. In questo caso l’unica informazione sarà un contatore che verrà incrementato ogni volta che l’utente richiamerà l’URL del servlet. Quando questa viene richiamata per la prima volta, si ottiene la seguente pagina:

Image

Se si fa clic sul link «Aggiorna la pagina» qui sopra, si ottiene la seguente nuova pagina:

Image

L'applicazione è composta da tre elementi:

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

Queste tre componenti sono installate nell'applicazione web «sessions» già in uso. Il file web.xml di quest'ultima è 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>
  • la servlet si chiama contatore (servlet-name) ed è collegata al file di classe compteur.class (servlet-class)
  • ha due parametri di inizializzazione:
    • urlAffichageCompteur: URL della pagina JSP di visualizzazione del contatore
    • urlErreur: URL della pagina JSP di visualizzazione di un eventuale errore
  • e un alias /contatore che fa sì che venga richiamato tramite 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{

     // variabili di istanza
    String msgErreur=null;
    String urlAffichageCompteur=null;
    String urlErreur=null;

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

        // L'inizializzazione è andata a buon fine?
        if(msgErreur!=null){
             // si passa alla pagina di errore
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // si recupera la sessione corrente
        HttpSession session=request.getSession();
         // e il contatore
        String compteur=(String)session.getAttribute("compteur");
        if(compteur==null) compteur="0";
         // incremento del contatore
        try{
            compteur=""+(Integer.parseInt(compteur)+1);
        }catch(Exception ex){}
         // memorizzazione del contatore nella sessione
        session.setAttribute("compteur",compteur);
         // e nella richiesta
        request.setAttribute("compteur",compteur);

         // si passa all'URL di visualizzazione del contatore
        getServletContext().getRequestDispatcher(urlAffichageCompteur).forward(request,response);
    }

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

     //-------- INIT
    public void init(){
         // si recuperano i parametri di inizializzazione
        ServletConfig config=getServletConfig();
        urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
        urlErreur=config.getInitParameter("urlErreur");

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

Questo servlet ha la stessa struttura dei servlet già visti. Si noti semplicemente la gestione del contatore:

  • la sessione viene recuperata tramite request.getSession()
  • il contatore viene recuperato in questa sessione tramite session.getAttribute("contatore")
  • se si recupera un valore null, significa che la sessione è appena iniziata. Il contatore viene quindi azzerato.
  • il contatore viene incrementato, reinserito nella sessione (session.setAttribute("contatore",contatore)) e inserito nella richiesta che verrà trasmessa al servlet di visualizzazione (request.setAttribute("contatore",contatore)).

La pagina di visualizzazione compteur.jsp è la seguente:

<%
     // jspService
   // si recupera il contatore
  String compteur= (String) request.getAttribute("compteur");
  if(compteur==null) compteur="inconnu";
%>
<!-- inizio della pagina 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 si limita a recuperare l'attributo compteur (request.getAttribute("contatore")) che le è stato passato dal servlet principale e lo visualizza.

La pagina di errore erreurcompteur.jsp è la seguente:

<%
     // jspService
   // si è verificato un errore
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- inizio della pagina 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

Ci proponiamo di scrivere un'applicazione Java che funga da client dell'applicazione compteur precedente. Essa la chiamerebbe N volte di seguito, dove N verrebbe passato come parametro. Il nostro obiettivo è mostrare un client web programmato e come gestire i cookie. Il nostro punto di partenza sarà un client web generico presentato nella dispensa su Java dello stesso autore. Viene chiamato nel modo seguente:

clientweb URL GET/HEAD

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

Ecco un esempio con URL e http://localhost:8080/sessions/compteur:


E:\data\serge\JAVA\SOCKETS\client web>java clientweb http://localhost:8080/sessions/contatore 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


<!-- inizio della pagina 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 è riportato il comando HTTP Set-cookie con cui il server invia un cookie al proprio client. In questo caso il cookie contiene due informazioni:

  • JSESSIONID, che è il token di sessione
  • Path, che definisce la URL a cui appartiene il cookie. Path=/sessions indica al browser che dovrà rinviare il cookie al server ogni volta che richiederà una URL che inizi con /sessions. Nell’applicazione sessions abbiamo utilizzato diversi servlet, tra cui i servlet /sessions/cycledevie e /sessions/compteur. Se si richiama il servlet /sessions/cycledevie, il browser riceverà un token J. Se, con lo stesso browser, si richiama successivamente la servlet /sessions/compteur, il browser rinvierà al server il token J poiché questo riguarda tutte le servlet URL a partire da /sessions. Nel nostro esempio, i servlet cycledevie e compteur non devono condividere lo stesso token di sessione. Non avrebbero quindi dovuto essere inseriti nella stessa applicazione web. È un punto da tenere presente: tutti i servlet di una stessa applicazione condividono lo stesso token di sessione.
  • Un cookie può anche definire una durata di validità. In questo caso tale informazione è assente. Il cookie verrà quindi eliminato alla chiusura del browser. Un cookie può avere una durata di validità di N giorni, ad esempio. Finché è valido, il browser lo invierà ogni volta che verrà consultata una delle pagine URL del suo dominio (Path). Prendiamo ad esempio un sito di vendita online di CD. Questo può tracciare il percorso del cliente nel proprio catalogo e determinare gradualmente le sue preferenze: la musica classica, ad esempio. 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. Quest’ultima, in base alle informazioni contenute nel cookie, potrà quindi adattare le pagine generate alle preferenze del cliente.

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

// pacchetti importati
import java.io.*;
import java.net.*;

public class clientweb{

    // richiede un URL
     // visualizza il contenuto di quest'ultima sullo schermo

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

        // numero di argomenti
        if(args.length != 2)
            erreur(syntaxe,1);

         // si prende nota dell'URI richiesto
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // verifica della validità di URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI non corretto
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // verifica dell'ordine
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // ordine non corretto
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // si estraggono le informazioni utili da 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();

         // si può procedere
        Socket  client=null;                        // il cliente
        BufferedReader IN=null;                    // il flusso di lettura del cliente
        PrintWriter OUT=null;                        // il flusso di scrittura del client
        String réponse=null;                        // risposta del server
        try{
             // ci si connette al server
            client=new Socket(host,port);

            // si creano i flussi di ingresso-uscita del cliente TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // richiesta di URL - invio delle intestazioni HTTP
            OUT.println(commande + " " + path + query + " HTTP/1.1");   
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // si legge la risposta
            while((réponse=IN.readLine())!=null){
                 // si elabora la risposta
                System.out.println(réponse);
            }//while
             // è terminato
            client.close();
        } catch(Exception e){
            // si gestisce l'eccezione
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // visualizzazione degli errori
    public static void erreur(String msg, int exitCode){
         // visualizzazione dell'errore
        System.err.println(msg);
         // interruzione con errore
        System.exit(exitCode);
    }//errore
}//classe

Ora creiamo il programma clientCompteur, che viene richiamato come segue:

clientCompteur URL N [JSESSIONID]

  • URL: URL del servlet contatore
  • N: numero di chiamate da effettuare a questa servlet
  • JSESSIONID: parametro facoltativo - token di una sessione

Lo scopo del programma è quello di richiamare N volte la servlet contatore 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 pari a N. Ecco un primo esempio di esecuzione:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/contatore 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 sotto forma di -->
  • le intestazioni HTTP che riceve
  • il valore del contatore dopo ogni chiamata

Si nota che durante la prima chiamata:

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

Per le chiamate successive:

  • il client rinvia sistematicamente il cookie che ha ricevuto dal server durante la prima richiesta. Questo permetterà al server di riconoscerlo e di incrementare il proprio contatore.
  • il server, dal canto suo, non invia più alcun cookie

Rilanciamo il programma precedente passando il token sopra indicato come terzo parametro:


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

Si nota qui che già dalla prima richiesta del client, il server riceve un cookie di sessione valido. È importante sapere che per Tomcat la durata massima di inattività di una sessione è impostata di default a 20 minuti (è in realtà configurabile). Se la seconda richiesta del programma invia abbastanza rapidamente il cookie ricevuto durante la prima richiesta, per il server si tratta quindi della stessa sessione. Qui si evidenzia una potenziale falla di sicurezza. Se riesco a intercettare in rete un token di sessione, posso quindi spacciarmi per chi l’ha avviata. Nel nostro esempio, la prima richiesta rappresenta chi avvia la sessione (magari con un nome utente e una password che gli danno il diritto di ottenere un token) e la seconda richiesta rappresenta chi ha “rubato” il token di sessione della prima richiesta. Se l’operazione in corso è un’operazione bancaria, la situazione può diventare molto grave...

Il codice del client è il seguente:

// pacchetti importati
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientCompteur{

     // richiede un URL
     // visualizza il contenuto di quest'ultima sullo schermo

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

         // numero di argomenti
        if(args.length !=2 && args.length != 3)
            erreur(syntaxe,1);

         // si prende nota dell'URL richiesto
        String URLString=args[0];

        // verifica della validità di URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI non corretto
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//intercettazione
         // verifica del numero di chiamate N
        int N=0;
        try{
            N=Integer.parseInt(args[1]);
            if(N<=0) throw new Exception();
        }catch(Exception ex){
             // argomento N non corretto
            erreur("Le nombre d'appels N doit être un entier >0",3);
        }
         // il token JSESSIONID è stato passato come parametro?
        String JSESSIONID="";
        if (args.length==3) JSESSIONID=args[2];

        // si estraggono le informazioni utili da 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();

         // si può procedere
        Socket  client=null;                        // il cliente
        BufferedReader IN=null;                    // il flusso di lettura del cliente
        PrintWriter OUT=null;                        // il flusso di scrittura del client
        String réponse=null;                        // risposta del server
         // il modello ricercato nelle intestazioni HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // il modello ricercato nel codice HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
         // il risultato del confronto con il modello
        Matcher résultat=null;
         // un valore booleano che indica il risultato della ricerca del contatore
        boolean compteurTrouvé;

        try{
             // si effettuano N chiamate al server
            for(int i=0;i<N;i++){
                // si effettua la connessione al server
                client=new Socket(host,port);

                // si creano i flussi di entrata-uscita del cliente TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // si richiede l'URL - invio delle intestazioni 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 legge la risposta fino alla fine delle intestazioni, cercando l'eventuale cookie
                while((réponse=IN.readLine())!=null){
                     // monitoraggio della risposta
                    System.out.println(réponse);
                     // riga vuota?
                    if(réponse.equals("")) break;
                     // riga HTTP non vuota
                     // se non si dispone del token di sessione, lo si cerca
                    if (JSESSIONID.equals("")){
                        // si confronta la riga HTTP con il modello del cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // il cookie è stato trovato
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while

                 // la verifica delle intestazioni HTTP è terminata - si passa al codice HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // la riga corrente contiene il contatore?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // il contatore è stato trovato - lo visualizziamo
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while
                 // è finito
                client.close();
            }//for
        } catch(Exception e){
            // si gestisce l'eccezione
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // visualizzazione degli errori
    public static void erreur(String msg, int exitCode){
         // visualizzazione dell'errore
        System.err.println(msg);
         // interruzione con errore
        System.exit(exitCode);
    }//errore

     // monitoraggio scambi client-server
    public static void envoie(PrintWriter OUT,String msg){
        // invio di un messaggio al server
        OUT.println(msg);
         // monitoraggio schermo
        System.out.println("--> "+msg);
    }//errore
}//classe

Analizziamo i punti salienti di questo programma:

  • è necessario effettuare N scambi client-server. Ecco perché questi sono contenuti in un ciclo
            for(int i=0;i<N;i++){
  • ad ogni scambio, il client apre una connessione TCP-IP con il server. Una volta ottenuta la connessione, invia al server le intestazioni HTTP della sua richiesta:
                 // richiesta di URL - invio delle intestazioni 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 sotto forma di cookie, altrimenti no.

  • Una volta inviata la richiesta, il client attende la risposta del server. Inizia analizzando le intestazioni HTTP di tale risposta alla ricerca di un eventuale cookie. Per individuarlo, confronta le righe ricevute con l’espressione regolare del cookie:
         // il modello ricercato nelle intestazioni HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // si legge la risposta fino alla fine delle intestazioni cercando l'eventuale cookie
                while((réponse=IN.readLine())!=null){
                     // monitoraggio della risposta
                    System.out.println(réponse);
                     // riga vuota?
                    if(réponse.equals("")) break;
                     // riga HTTP non vuota
                     // se non si dispone del token di sessione, lo si cerca
                    if (JSESSIONID.equals("")){
                        // si confronta la riga HTTP con il modello del cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // il cookie è stato trovato
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • una volta individuato il token per la prima volta, non verrà più ricercato nelle successive richieste al server. Una volta elaborati gli header HTTP della risposta, si passa al codice HTML della stessa risposta. In quest’ultima, si cerca la riga che fornisce il valore del contatore. Anche questa ricerca viene effettuata utilizzando un’espressione regolare:
         // il modello del contatore ricercato nel codice HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // fine delle intestazioni HTTP - si passa al codice HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // la riga corrente contiene il contatore?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // il contatore è stato trovato - lo visualizziamo
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. Esempio 4

Nell'esempio precedente, il client web rinvia il token sotto forma di cookie. Abbiamo visto che può anche rinviarlo all'interno della stessa richiesta URL nella forma URL;jsessionid=xxx. Verifichiamolo. Il programma clientCompteur.java viene trasformato in clientCompteur2.java e modificato come segue:

....
                 // si richiede l'URL - invio delle intestazioni 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 cliente richiede quindi l'URL del contatore tramite GET URL;jsessionid=xx HTTP/1.1 e non invia più alcun cookie. Questa è l'unica modifica. Ecco i risultati di una prima richiesta:


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

Al primo accesso, il client richiede l'URL senza token di sessione. Il server risponde inviandogli il token. Il client quindi interroga nuovamente lo stesso URL allegando ad esso il token ricevuto. Si nota che il contatore viene correttamente incrementato, a dimostrazione del fatto che il server ha effettivamente riconosciuto che si trattava della stessa sessione.

4.7. Esempio 5

Questo esempio mostra un'applicazione composta da tre pagine che chiameremo page0, page1 e page2. L'utente deve visualizzarle in questo ordine:

  • page0 è un modulo che richiede un'informazione: un nome
  • la pagina1 è un modulo ottenuto in risposta all’invio del modulo della pagina0. Richiede una seconda informazione: un’età
  • la pagina2 è un documento HTML che visualizza il nome ottenuto dalla pagina0 e l’età ottenuta dalla pagina1.

Ci sono tre scambi client-server:

  • nel primo scambio il modulo di pagina0 viene richiesto dal cliente e inviato dal server
  • nel secondo scambio il modulo 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 durante il secondo scambio e da allora è stato "dimenticato". Si utilizza una sessione per memorizzare il nome durante lo scambio 2, in modo che sia disponibile durante lo scambio 3.

La pagina page0 ottenuta durante il primo scambio è la seguente:

Image

Si compila il campo del nome:

Image

Si utilizza il pulsante Suite e si ottiene quindi la seguente pagina page1:

Image

Si compila il campo dell'età:

Image

Si utilizza il pulsante Suite e si ottiene quindi la seguente pagina page2:

Image

Quando si invia la pagina page0 al server, quest’ultimo può restituirla con un codice di errore se il nome è vuoto:

Image

Quando si invia la pagina page1 al server, quest’ultimo può restituirla con un codice di errore se l’età non è valida:

Image

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

page0.jsp
visualizza pagina0
page1.jsp
visualizza pagina1
page2.jsp
visualizza pagina 2
erreur.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 dell'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 URL http://localhost:8080/suitedepages/main. Ha quattro parametri di inizializzazione che corrispondono ai URL delle quattro pagine JSP utilizzate per le diverse visualizzazioni. Il codice del 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{

    // variabili di istanza
    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'inizializzazione è andata a buon fine?
        if(msgErreur!=null){
             // si passa alla pagina di errore
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }
         // si recupera il parametro di fase
        String étape=request.getParameter("etape");
         // si recupera la sessione corrente
        HttpSession session=request.getSession();
         // si elabora la fase in corso
        if(étape==null) étape0(request,response,session);
        if(étape.equals("1")) étape1(request,response,session);
        if(étape.equals("2")) étape2(request,response,session);
         // gli altri casi non sono validi
        étape0(request,response,session);
    }

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

     //-------- INIT
    public void init(){
         // si recuperano i parametri di inizializzazione
        ServletConfig config=getServletConfig();
        urlPage0=config.getInitParameter("urlPage0");
        urlPage1=config.getInitParameter("urlPage1");
        urlPage2=config.getInitParameter("urlPage2");
        urlErreur=config.getInitParameter("urlErreur");

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

     //-------- fase 0
    public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // si impostano alcuni attributi
        request.setAttribute("nom","");
         // viene visualizzata la pagina 0
        request.getRequestDispatcher(urlPage0).forward(request,response);
    }

     //-------- fase 1
    public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // si recupera il nome dalla richiesta
        String nom=request.getParameter("nom");
        // nome impostato?
        if(nom==null) étape0(request,response,session);
         // si rimuovono eventuali spazi dal nome
        nom=nom.trim();
         // lo si inserisce in un attributo della query
        request.setAttribute("nom",nom);
         // nome vuoto?
        if(nom.equals("")){
             // è un errore
            ArrayList erreurs=new ArrayList();
            erreurs.add("Nous n'avez pas indiqué de nom");
             // si inseriscono gli errori nella query
            request.setAttribute("erreurs",erreurs);
             // ritorno alla pagina 0
            étape0(request,response,session);
        }
         // nome valido - lo si memorizza nella sessione corrente
        session.setAttribute("nom",nom);
         // si imposta l'attributo «età» nella richiesta
        request.setAttribute("age","");
         // si visualizza la pagina 1
        request.getRequestDispatcher(urlPage1).forward(request,response);
    }

     //-------- fase 2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // si recupera il nome dalla sessione
        String nom=(String)session.getAttribute("nom");
         // nome impostato?
        if(nom==null) étape0(request,response,session);
         // lo si inserisce in un attributo della query
        request.setAttribute("nom",nom);
         // si recupera l'età dalla query
        String age=request.getParameter("age");
        // età inserita?
        if(age==null){
            // ritorno alla pagina 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // si memorizza l'età nella query
        age=age.trim();
        request.setAttribute("age",age);
        // età valida?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // È un errore
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // si inseriscono gli errori nella query
            request.setAttribute("erreurs",erreurs);
             // ritorno alla pagina 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // età valida - si visualizza la pagina 2
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • il metodo init recupera i quattro parametri di inizializzazione e genera un messaggio di errore se uno di essi è mancante
  • abbiamo visto che la richiesta comprendeva tre scambi. Per sapere a che punto siamo in questi scambi, i moduli page0 e page1 contengono una variabile nascosta etape che ha il valore 1 (page0) o 2 (page1). In questo contesto, tale numero potrebbe 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 di page0 e invia page1 oppure nuovamente page0 in caso di errore
    • La fase 2 elabora il modulo page1 e invia page2 oppure, in caso di errore, nuovamente page1
  • fase 0
    • visualizza page0 con un nome vuoto
  • fase 1
    • recupera il parametro nom dal modulo page0.
    • verifica che il nome esista (non sia nullo). In caso contrario, visualizza nuovamente page0 come se fosse la prima chiamata.
    • Verifica che il nome non sia vuoto. In caso contrario, visualizza nuovamente page0 con un messaggio di errore.
    • memorizza il nome nella sessione corrente e visualizza page1 se il nome è valido.
  • fase 2
    • recupera il parametro nom dalla sessione corrente.
    • Verifica che il nome esista (non sia nullo). In caso contrario, viene visualizzato nuovamente page0 come se fosse la prima chiamata.
    • recupera il parametro age nella richiesta corrente inviata da page1.
    • Verifica che l’età sia valida. In caso contrario, visualizza nuovamente page1 con un messaggio di errore.
    • memorizza il nome e l'età come attributi della richiesta e visualizza page2 se il nome e l'età sono validi.

La pagina page0.jsp è la seguente:

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

<% // page0.jsp
     // si recuperano gli attributi della richiesta
  String nom=(String)request.getAttribute("nom");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
   // attributi validi?
  if(nom==null){
       // ritorno al 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>
    <% // errori?
      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 di page0 in caso di errore
  • il parametro nom da visualizzare le viene fornito dal servlet principale insieme all'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 (hidden) etape, che indica in quale fase dell'applicazione ci si trova.

La pagina page1.jsp è la seguente:

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

<% // page1.jsp
     // si recuperano gli attributi della richiesta
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // attributi validi?
  if(nom==null || age==null){
      // ritorno al 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>
    <% // errori?
      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 analoga a quella della pagina page0.jsp, con la differenza che ora riceve due attributi dal servlet principale: nom e age. Infine, la pagina page2.jsp è la seguente:

<% 
     // page2.jsp
     // si recuperano gli attributi della richiesta
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  // attributi validi?
  if(nom==null || age==null){
      // ritorno al 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 nom e age dal servlet principale. Si limita a visualizzarli. Infine, la pagina erreur.jsp, incaricata di visualizzare un errore in caso di inizializzazione non corretta del servlet, è la seguente:

<%
     // jspService
   // si è verificato un errore
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- inizio della pagina HTML -->
<html>
  <head>
      <title>Suite de pages</title>
  </head>
  <body>
      <h3>Suite de pages</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

Visualizza l'attributo msgErreur che le è stato passato dal servlet principale.

In conclusione, si può notare che, nel corso delle tre fasi dell’applicazione, è sempre il servlet principale ad essere interpellato per primo dal browser. Tuttavia, non è esso a generare la risposta da visualizzare, bensì una delle quattro pagine JSP. L’utente non si accorge di questo dettaglio, poiché il browser continua a visualizzare nella barra degli indirizzi l’URL URL inizialmente richiesto, ovvero quello del servlet principale.